dev 7 months ago
parent 133699153c
commit 740de6584e

@ -1,7 +1,11 @@
{
"permissions": {
"allow": [
"Bash(npx vitest run src/glob/tests/unit.test.ts)"
"Bash(npx vitest run src/glob/tests/unit.test.ts)",
"Bash(npx tsc --noEmit)"
],
"additionalDirectories": [
"g:\\dev\\TypeScript\\v\\src\\adapters\\svelte"
]
}
}

@ -0,0 +1,14 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VICE — Catalog Editor</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/apps/svelte/main.ts"></script>
</body>
</html>

@ -0,0 +1,173 @@
import type { GlobInstance, GlobConfig } from '../../glob/glob_types.ts';
import type { GlobLocale } from '../../glob/comn_types.ts';
import type { LingNode } from '../../ling/types.ts';
import type { CurrDisplay } from '../../glob/curr_types.ts';
import type { DatiOptions } from '../../glob/dati_types.ts';
import type { UnitDisplay, UnitCategoryInstance } from '../../glob/unit_types.ts';
import { createGlob } from '../../glob/glob_engine';
// ── Tipos ─────────────────────────────────────────────────────────────────────
export interface ReactiveNumr {
f: (n: number, opts?: { minDecimals?: number; maxDecimals?: number }) => string;
}
export interface ReactiveCurr {
f : (amount: number, display?: CurrDisplay) => string;
convert : GlobInstance['curr']['convert'];
convertTo: GlobInstance['curr']['convertTo'];
get : GlobInstance['curr']['get'];
}
export interface ReactiveDati {
f : (date: Date, opts?: DatiOptions) => string;
setTimeFormat: GlobInstance['dati']['setTimeFormat'];
getTimeFormat: GlobInstance['dati']['getTimeFormat'];
setDateOrder : GlobInstance['dati']['setDateOrder'];
getDateOrder : GlobInstance['dati']['getDateOrder'];
}
export interface ReactiveUnitCategory {
f : (value: number, display?: UnitDisplay) => string;
fTo : (value: number, targetUnit?: string, display?: UnitDisplay) => string;
convert: UnitCategoryInstance['convert'];
}
export interface ReactiveUnit {
weight : ReactiveUnitCategory;
length : ReactiveUnitCategory;
temp : ReactiveUnitCategory;
volume : ReactiveUnitCategory;
area : ReactiveUnitCategory;
setSystem: GlobInstance['unit']['setSystem'];
getSystem: GlobInstance['unit']['getSystem'];
}
// ── Estado reactivo ───────────────────────────────────────────────────────────
/**
* Crea un estado reactivo de Svelte 5 (runes) a partir de una `GlobInstance`.
*
* Usa `$state` internamente. Debe importarse desde un archivo `.svelte`,
* `.svelte.ts` o `.svelte.js` para que las runes funcionen.
*
* Los getters leen `_locale` / `_currency` para establecer dependencias
* reactivas: cuando el locale o la moneda cambian, cualquier contexto
* reactivo que lea esas propiedades se re-evalúa automáticamente.
*
* @example
* ```svelte
* <script>
* import { createGlobState } from '@/svelte/glob.svelte';
* import { glob } from '$lib/glob';
*
* const g = createGlobState(glob);
* </script>
*
* <p>{g.t('hello')}</p>
* <p>{g.numr.f(1_234.5)}</p>
* <p>{g.curr.f(99.99, 'symbol')}</p>
*
* <select bind:value={g.locale}>
* <option value="es">Español</option>
* <option value="en">English</option>
* </select>
* ```
*/
function isGlobInstance<S extends LingNode>(
input: GlobInstance<S> | GlobConfig<S>
): input is GlobInstance<S> {
return 'onLocaleChange' in input;
}
export function createGlobState<S extends LingNode>(input: GlobInstance<S> | GlobConfig<S>) {
const glob = isGlobInstance(input) ? input : createGlob(input);
let _locale = $state<GlobLocale>(glob.getLocale());
let _currency = $state<string>(glob.curr.getCurrency());
// Sincroniza cuando glob cambia el locale desde fuera (ej. otro módulo)
const unsubLocale = glob.onLocaleChange((loc: GlobLocale) => { _locale = loc; });
// Helper reutilizable para categorías de unidades
const reactiveUnitCat = (c: UnitCategoryInstance, locale: GlobLocale): ReactiveUnitCategory => ({
f : (value, display) => c.f(value, display, locale),
fTo : (value, target, display) => c.fTo(value, target, display, locale),
convert: c.convert.bind(c),
});
return {
// ── Locale ────────────────────────────────────────────────────────────
// El setter llama a glob.setLocale(), que a su vez dispara onLocaleChange,
// que actualiza _locale y reactiva todos los getters dependientes.
get locale() { return _locale; },
set locale(v: GlobLocale) { glob.setLocale(v); },
// ── Currency ──────────────────────────────────────────────────────────
get currency() { return _currency; },
set currency(v: string) {
glob.curr.setCurrency(v);
_currency = glob.curr.getCurrency(); // normaliza a mayúsculas
},
// ── Traducciones ──────────────────────────────────────────────────────
// Leer _locale dentro del getter establece la dependencia reactiva.
// glob.t usa ling.getLocale() internamente, así que siempre lee el locale actual.
get t() { _locale; return glob.t; },
get ts() { _locale; return glob.ts; },
// ── Numr ──────────────────────────────────────────────────────────────
get numr(): ReactiveNumr {
const l = _locale;
return {
f: (n, opts) => glob.numr.f(n, opts, l),
};
},
// ── Curr ──────────────────────────────────────────────────────────────
// Depende de locale y currency — ambas lecturas crean dependencias.
get curr(): ReactiveCurr {
const l = _locale;
void _currency; // dependencia reactiva: se re-evalúa al cambiar moneda
return {
f : (amount, display) => glob.curr.f(amount, display, l),
convert : glob.curr.convert.bind(glob.curr),
convertTo: glob.curr.convertTo.bind(glob.curr),
get : glob.curr.get.bind(glob.curr),
};
},
// ── Dati ──────────────────────────────────────────────────────────────
get dati(): ReactiveDati {
const l = _locale;
return {
f : (date, opts) => glob.dati.f(date, opts, l),
setTimeFormat: glob.dati.setTimeFormat.bind(glob.dati),
getTimeFormat: glob.dati.getTimeFormat.bind(glob.dati),
setDateOrder : glob.dati.setDateOrder.bind(glob.dati),
getDateOrder : glob.dati.getDateOrder.bind(glob.dati),
};
},
// ── Unit ──────────────────────────────────────────────────────────────
get unit(): ReactiveUnit {
const l = _locale;
return {
weight : reactiveUnitCat(glob.unit.weight, l),
length : reactiveUnitCat(glob.unit.length, l),
temp : reactiveUnitCat(glob.unit.temp, l),
volume : reactiveUnitCat(glob.unit.volume, l),
area : reactiveUnitCat(glob.unit.area, l),
setSystem: glob.unit.setSystem.bind(glob.unit),
getSystem: glob.unit.getSystem.bind(glob.unit),
};
},
/** Limpia la suscripción interna con glob. Útil si desmontas el contexto raíz. */
destroy: () => unsubLocale(),
};
}

@ -0,0 +1 @@
export * from './glob.svelte.ts';

@ -0,0 +1,15 @@
<script lang="ts">
import './app.css';
import GlobDemo from './components/GlobDemo.svelte';
</script>
<main>
<GlobDemo />
</main>
<style global>
main { font-family: sans-serif; max-width: 600px; margin: 2rem auto; padding: 0 1rem; }
</style>

@ -0,0 +1,15 @@
@import "tailwindcss";
/* Smooth scroll para navegación */
html {
scroll-behavior: smooth;
}
@layer components {
}

@ -0,0 +1,147 @@
<script lang="ts">
import { ling } from '@/ling';
import { createGlobState } from '@/adapters/svelte/glob.svelte';
import { p } from '@/ling/engine';
import type { CurrDefinition } from '@/glob/curr_types';
import type { UnitSystem } from '@/glob/unit_types';
import type { DatiTimeFormat } from '@/glob/dati_types';
// ── Definiciones de monedas ───────────────────────────────────────────────
const currencies: Record<string, CurrDefinition> = {
EUR: { symbol: '€', code: 'EUR', conversionValue: 1, name: p({ es: { one: 'euro', other: 'euros' }, en: { one: 'euro', other: 'euros' } }) },
USD: { symbol: '$', code: 'USD', conversionValue: 1.08, name: p({ es: { one: 'dólar', other: 'dólares' }, en: { one: 'dollar', other: 'dollars' } }) },
GBP: { symbol: '£', code: 'GBP', conversionValue: 0.85, name: p({ es: { one: 'libra', other: 'libras' }, en: { one: 'pound', other: 'pounds' } }) },
};
// ── Estado glob ───────────────────────────────────────────────────────────
const g = createGlobState({
locale : 'es',
ling,
numr : { maxDecimals: 2 },
curr : { definitions: currencies, selectedCurrency: 'EUR' },
dati : { selectedTimeFormat: '24h' },
});
// Estado local para propiedades sin $state en el adapter
let _system = $state<UnitSystem>('metric');
let _timeFormat = $state<DatiTimeFormat>('24h');
let msgCount = $state(3);
const DATE = new Date(2026, 1, 28, 14, 30, 0);
// ── Acciones ──────────────────────────────────────────────────────────────
function toggleLocale() { g.locale = g.locale === 'es' ? 'en' : 'es'; }
function toggleSystem() { _system = _system === 'metric' ? 'imperial' : 'metric'; g.unit.setSystem(_system); }
function toggleTimeFormat() { _timeFormat = _timeFormat === '24h' ? '12h' : '24h'; g.dati.setTimeFormat(_timeFormat); }
// Derivadas que rastrean tanto _locale (via g.*) como el estado local
const weightStr = $derived.by(() => { _system; return g.unit.weight.f(75, 'name'); });
const lengthStr = $derived.by(() => { _system; return g.unit.length.f(1.8, 'abbr'); });
const tempStr = $derived.by(() => { _system; return g.unit.temp.f(37, 'name'); });
const weightConv = $derived.by(() => { _system; return g.unit.weight.fTo(75, 'lb', 'name'); });
const datiShort = $derived.by(() => { _timeFormat; return g.dati.f(DATE, 'short'); });
const datiLong = $derived.by(() => { _timeFormat; return g.dati.f(DATE, 'long'); });
const datiTime = $derived.by(() => { _timeFormat; return g.dati.f(DATE, { time: 'short' }); });
</script>
<div>
<!-- ── LOCALE ─────────────────────────────────────────────────────────── -->
<section>
<h2>Locale</h2>
<div class="row">
<span>Activo: <strong>{g.locale}</strong></span>
<button onclick={toggleLocale}>ES ↔ EN</button>
</div>
</section>
<!-- ── TRADUCCIONES ───────────────────────────────────────────────────── -->
<section>
<h2>t() — Traducciones</h2>
<ul>
<li><code>common.ok</code> → {g.t('common.ok')}</li>
<li><code>common.cancel</code> → {g.t('common.cancel')}</li>
<li><code>checkout.pay</code> → {g.t('checkout.pay')}</li>
<li>
<code>messages.unread</code> →
{g.t('messages.unread', { count: msgCount })}
<input type="range" min="0" max="10" bind:value={msgCount} />
<span>{msgCount}</span>
</li>
</ul>
</section>
<!-- ── NÚMEROS ────────────────────────────────────────────────────────── -->
<section>
<h2>numr — Números</h2>
<ul>
<li>1 234 567.89 → {g.numr.f(1_234_567.89)}</li>
<li>0.1 (3–5 decimales) → {g.numr.f(0.1, { minDecimals: 3, maxDecimals: 5 })}</li>
<li>−9 876.5 → {g.numr.f(-9_876.5)}</li>
</ul>
</section>
<!-- ── MONEDA ─────────────────────────────────────────────────────────── -->
<section>
<h2>curr — Moneda</h2>
<div class="row">
<span>Moneda activa: <strong>{g.currency}</strong></span>
{#each Object.keys(currencies) as code}
<button
onclick={() => (g.currency = code)}
class:active={g.currency === code}
>{code}</button>
{/each}
</div>
<ul>
<li>symbol → {g.curr.f(1_234.56, 'symbol')}</li>
<li>code → {g.curr.f(1_234.56, 'code')}</li>
<li>name → {g.curr.f(1_234.56, 'name')}</li>
<li>convert 100 EUR → USD = {g.curr.convert(100, 'EUR', 'USD')?.toFixed(2)}</li>
</ul>
</section>
<!-- ── FECHAS ─────────────────────────────────────────────────────────── -->
<section>
<h2>dati — Fechas</h2>
<div class="row">
<span>Formato: <strong>{_timeFormat}</strong></span>
<button onclick={toggleTimeFormat}>12h ↔ 24h</button>
</div>
<ul>
<li>short → {datiShort}</li>
<li>long → {datiLong}</li>
<li>solo hora → {datiTime}</li>
</ul>
</section>
<!-- ── UNIDADES ───────────────────────────────────────────────────────── -->
<section>
<h2>unit — Unidades</h2>
<div class="row">
<span>Sistema: <strong>{_system}</strong></span>
<button onclick={toggleSystem}>metric ↔ imperial</button>
</div>
<ul>
<li>weight 75 kg → {weightStr}</li>
<li>length 1.8 m → {lengthStr}</li>
<li>temp 37 °C → {tempStr}</li>
<li>convert 75 kg→lb → {weightConv}</li>
</ul>
</section>
</div>
<style>
section { border: 1px solid #ddd; border-radius: 6px; padding: 1rem 1.25rem; margin-bottom: 1rem; }
h2 { margin: 0 0 .75rem; font-size: 1rem; color: #555; text-transform: uppercase; letter-spacing: .05em; }
ul { margin: 0; padding-left: 1.25rem; }
li { margin: .25rem 0; font-size: .95rem; }
code { background: #f4f4f4; padding: .1rem .35rem; border-radius: 3px; font-size: .85em; }
.row { display: flex; align-items: center; gap: .5rem; margin-bottom: .5rem; flex-wrap: wrap; }
button { padding: .25rem .65rem; border: 1px solid #aaa; border-radius: 4px; cursor: pointer; background: #fff; }
button:hover { background: #f0f0f0; }
button.active { background: #333; color: #fff; border-color: #333; }
input[type=range] { width: 120px; }
</style>

@ -0,0 +1,11 @@
import { mount } from 'svelte';
import App from './App.svelte';
const app = mount(App, {
target: document.getElementById('app')!
});
export default app;

@ -6,8 +6,8 @@ import type { LingInstance, LingNode } from "../ling";
import type { CurrConfig, CurrInstance } from "./curr_types";
import type { DatiConfig, DatiInstance } from "./dati_types";
import type { NumrConfig, NumrInstance } from "./numr_types";
import type { GlobLocale } from "./comn_types";
import type { UnitConfig, UnitInstance } from "./unit_types";
import type { GlobLocale } from "./comn_types";
/** Configuración de `createGlob`. Todos los módulos son opcionales. */

@ -0,0 +1,13 @@
export * from './comn_types';
export * from './curr_types';
export * from './dati_types';
export * from './glob_types';
export * from './numr_types';
export * from './unit_types';
export * from './comn_engine';
export * from './curr_engine';
export * from './dati_engine';
export * from './glob_engine';
export * from './numr_engine';
export * from './unit_engine';

@ -1,8 +1,6 @@
// ==============================
// UNIT TYPES
// ==============================
import type { LingPluralFn, LingRecord } from "../ling";
import type { LocaleResolver } from "./comn_types";
@ -31,6 +29,12 @@ export interface UnitCategoryInstance {
convert: (value: number, from: string, to: string) => number;
}
export interface UnitConfig {
selectedSystem?: UnitSystem
categories: Record<UnitCategoryKey, UnitCategoryData>;
}
export interface UnitInstance {
weight : UnitCategoryInstance;
length : UnitCategoryInstance;

Loading…
Cancel
Save

Powered by TurnKey Linux.