diff --git a/src/lib/glob/lib/dati_engine.svelte.ts b/src/lib/glob/lib/dati_engine.svelte.ts index 7730bd367..0f5639712 100644 --- a/src/lib/glob/lib/dati_engine.svelte.ts +++ b/src/lib/glob/lib/dati_engine.svelte.ts @@ -50,3 +50,5 @@ export function createDatiState( }, }; } + + diff --git a/src/lib/glob/lib/dati_engine.ts b/src/lib/glob/lib/dati_engine.ts index 604d2f8a2..2687c5dae 100644 --- a/src/lib/glob/lib/dati_engine.ts +++ b/src/lib/glob/lib/dati_engine.ts @@ -1,9 +1,8 @@ -import type { DatiInstance, DatiOptions, DatiConfig } from './dati_types.ts'; +import type { DatiInstance, DatiOptions, DatiConfig, DatiTimeFormat, DatiDateOrder } from './dati_types.ts'; import type { LocaleResolver } from './comn_types.ts'; import { resolveLocale, cachedDateTimeFormat } from './comn_engine.ts'; import { DEFAULT_ISO_LOCALE } from '../../ling/lib/consts.ts'; -import type { HourCycle } from '@/lib/util/dates'; -import type { DateOrder, DateStyleType } from '$lib/util/dates/core/types'; +import type { DateStyleType } from '$lib/util/dates/core/types'; // ============================== @@ -42,16 +41,16 @@ export function createDati( defaultLocale: LocaleResolver = DEFAULT_ISO_LOCALE ): DatiInstance { - let timeFormat: HourCycle = opts.selectedTimeFormat ?? 24; - let dateOrder : DateOrder = opts.selectedDateOrder ?? 'DMY'; + let timeFormat: DatiTimeFormat = opts.selectedTimeFormat ?? '24h'; + let dateOrder : DatiDateOrder = opts.selectedDateOrder ?? 'DMY'; // ── Preferencias ────────────────────────────────────────────────────────── - function getTimeFormat(): HourCycle { return timeFormat; } - function setTimeFormat(format: HourCycle): void { timeFormat = format; } + function getTimeFormat(): DatiTimeFormat { return timeFormat; } + function setTimeFormat(format: DatiTimeFormat): void { timeFormat = format; } - function getDateOrder(): DateOrder { return dateOrder; } - function setDateOrder(order: DateOrder): void { dateOrder = order; } + function getDateOrder(): DatiDateOrder { return dateOrder; } + function setDateOrder(order: DatiDateOrder): void { dateOrder = order; } // ── Format ──────────────────────────────────────────────────────────────── @@ -61,7 +60,7 @@ export function createDati( if (dateStyle) intlOpts.dateStyle = dateStyle; if (timeStyle) { intlOpts.timeStyle = timeStyle; - intlOpts.hour12 = timeFormat === 12; + intlOpts.hour12 = timeFormat === '12h'; } return intlOpts; } diff --git a/src/lib/glob/lib/dati_types.ts b/src/lib/glob/lib/dati_types.ts index 66310d756..0eceb8f6e 100644 --- a/src/lib/glob/lib/dati_types.ts +++ b/src/lib/glob/lib/dati_types.ts @@ -1,9 +1,6 @@ import type { LocaleResolver } from "./comn_types.ts"; -import type { HourCycle } from "$lib/util/dates"; import type { DateOrder, DateStyleType } from '$lib/util/dates/core/types'; - - /** * Opciones de `f()`: * - `'short'` / `'medium'` / `'long'` / `'full'` : solo fecha en ese estilo. @@ -13,12 +10,22 @@ export type DatiOptions = | DateStyleType | { date?: DateStyleType; time?: DateStyleType }; +/** + * Formato de hora usado en el API de dati. + * - `'12h'` → reloj de 12 horas (AM/PM) + * - `'24h'` → reloj de 24 horas + */ +export type DatiTimeFormat = '12h' | '24h'; + +/** Alias de `DateOrder` para el API de dati. */ +export type DatiDateOrder = DateOrder; + /** Configuración inicial de `createDati`. */ export interface DatiConfig { /** Formato de horas. Default: `'24h'`. */ - selectedTimeFormat?: HourCycle; + selectedTimeFormat?: DatiTimeFormat; /** Orden de los componentes de fecha. Default: `'DMY'`. */ - selectedDateOrder?: DateOrder; + selectedDateOrder?: DatiDateOrder; } export interface DatiInstance { @@ -29,15 +36,15 @@ export interface DatiInstance { * Usa `Intl.DateTimeFormat.formatRange()` con fallback para navegadores antiguos. */ fRange : (start: Date, end: Date, opts?: DatiOptions, locale?: LocaleResolver) => string; - setTimeFormat: (format: HourCycle) => void; - getTimeFormat: () => HourCycle; + setTimeFormat: (format: DatiTimeFormat) => void; + getTimeFormat: () => DatiTimeFormat; /** Formato de hora actual. Reactivo en la versión svelte. */ - readonly timeFormat : HourCycle; + readonly timeFormat : DatiTimeFormat; /** Guarda la preferencia de orden de fecha para uso de la UI. No afecta a `Intl`. */ - setDateOrder : (order: DateOrder) => void; - getDateOrder : () => DateOrder; + setDateOrder : (order: DatiDateOrder) => void; + getDateOrder : () => DatiDateOrder; /** Orden de fecha actual. Reactivo en la versión svelte. */ - readonly dateOrder : DateOrder; + readonly dateOrder : DatiDateOrder; } diff --git a/src/routes/test/progress/+page.svelte b/src/routes/test/progress/+page.svelte index 9052e3b09..57b40e695 100644 --- a/src/routes/test/progress/+page.svelte +++ b/src/routes/test/progress/+page.svelte @@ -61,10 +61,10 @@

Múltiples variantes

- {#each [[20, '#6366f1'], [45, '#22c55e'], [70, '#f59e0b'], [100, '#ef4444']] as [val, color]} + {#each [[20, '#6366f1'], [45, '#22c55e'], [70, '#f59e0b'], [100, '#ef4444']] as [val, color] (val)}
{val}% - +
diff --git a/src/routes/test/toolbar/+page.svelte b/src/routes/test/toolbar/+page.svelte index 64f234e7c..1bb7a6df1 100644 --- a/src/routes/test/toolbar/+page.svelte +++ b/src/routes/test/toolbar/+page.svelte @@ -1,18 +1,15 @@ @@ -25,7 +22,7 @@ - + B diff --git a/src/uix/terra/config/terra-config.ts b/src/uix/terra/config/terra-config.ts new file mode 100644 index 000000000..a356dcd50 --- /dev/null +++ b/src/uix/terra/config/terra-config.ts @@ -0,0 +1,93 @@ +import { createTerraContext } from '$terra/utils/context'; + +import { type Active, type ActiveProps, readableActive } from '$terra/utils'; + +import type { TerraConfigPropsWithoutChildren } from './types'; + + +type TerraConfigStateProps = ActiveProps; + + + +export const TerraConfigContext = createTerraContext('TerraConfig'); + + + +export function getTerraConfig() { + + const fallback = new TerraConfigState(null, {}); + + return TerraConfigContext.getOr(fallback).opts; + +} + + + +export function useTerraConfig(opts: TerraConfigStateProps) { + + return TerraConfigContext.set(new TerraConfigState(TerraConfigContext.getOr(null), opts)); + +} + + + +export class TerraConfigState { + + readonly opts: Required; + + + + constructor(parent: TerraConfigState | null, opts: TerraConfigStateProps) { + + const resolveConfigOption = createConfigResolver(parent, opts); + + this.opts = { + + defaultPortalTo: resolveConfigOption((config) => config.defaultPortalTo), + + defaultLocale: resolveConfigOption((config) => config.defaultLocale), + + defaultDir: resolveConfigOption((config) => config.defaultDir), + + defaultDateOrder: resolveConfigOption((config) => config.defaultDateOrder), + + defaultTimeFormat: resolveConfigOption((config) => config.defaultTimeFormat), + + logger: resolveConfigOption((config) => config.logger), + + }; + + } + +} + + + +type ConfigOptionGetter = (config: TerraConfigStateProps) => Active | undefined; + +type ConfigOptionResolver = (getter: ConfigOptionGetter) => Active; + + + +function createConfigResolver( + + parent: TerraConfigState | null, + + currentOpts: TerraConfigStateProps + +): ConfigOptionResolver { + + return (getter: ConfigOptionGetter) => { + return readableActive(() => { + const value = getter(currentOpts)?.current; + + if (value !== undefined) return value; + + if (parent === null) return undefined; + + return getter(parent.opts)?.current; + }); + }; + +} + diff --git a/src/uix/terra/number-field/number-field.svelte.ts b/src/uix/terra/number-field/number-field.svelte.ts index 8b2a2e43f..b642ae3ce 100644 --- a/src/uix/terra/number-field/number-field.svelte.ts +++ b/src/uix/terra/number-field/number-field.svelte.ts @@ -3,7 +3,6 @@ import { createTerraContext } from '$terra/utils/context'; import { attachRef, kbd, - readableActive, } from '$terra/utils'; import type { Active, @@ -19,8 +18,8 @@ import type { } from '$terra/utils'; import { boolToEmptyStrOrUndef, boolToStr, createTerraAttrs } from '../utils/attrs'; import { resolveLocaleProp, resolveDirectionProp } from '../config/prop-resolvers'; -import { createNumr } from '@/glob/lib'; -import type { NumrFormat } from '@/glob/lib'; +import { createNumr, type NumrFormat } from '@/lib'; + // ─── Attrs ──────────────────────────────────────────────────────────────────── @@ -91,9 +90,9 @@ export class NumberFieldRootState { watch.pre( () => [opts.value.current, this.#locale.current, opts.formatOptions.current] as const, ([value]) => { - if (!this.#focused) { - this.#inputValue = this.#formatValue(value); - } + this.#inputValue = this.#focused + ? this.#formatEditValue(value) + : this.#formatValue(value); }, ); } @@ -114,15 +113,30 @@ export class NumberFieldRootState { readonly dir = $derived.by(() => this.#dir.current); /** - * Instancia NumrInstance reactiva. - * Se recrea solo cuando cambian formatOptions o el locale. - * La dirección se gestiona por separado (atributo HTML, no formateo). + * Formatter de display: usa el formato completo del consumidor (style, currency, percent…). + * Se recrea cuando cambian formatOptions o locale. */ readonly #numr = $derived.by(() => { const fmt: NumrFormat = { ...(this.opts.formatOptions.current ?? {}) }; return createNumr(fmt, this.#locale.current); }); + /** + * Formatter de edición: siempre decimal, sin decoración de estilo. + * Permite que el usuario edite "75" en vez de "75%" o "1.234,56 €". + * Para `percent`, el valor de edición es `value * 100`. + */ + readonly #editNumr = $derived.by(() => { + const fmt = this.opts.formatOptions.current ?? {}; + // Heredar precisión decimal del formato original; quitar style/currency/unit. + const editFmt: NumrFormat = { + minDecimals: fmt.minDecimals, + maxDecimals: fmt.maxDecimals, + grouping: fmt.grouping, + }; + return createNumr(editFmt, this.#locale.current); + }); + readonly atMin = $derived.by( () => this.#min !== undefined && this.opts.value.current !== undefined @@ -140,15 +154,36 @@ export class NumberFieldRootState { // ── Format / Parse ─────────────────────────────────────────────────────── + /** + * Para `percent`, el valor interno 0.75 se edita como 75. + * Para el resto, el valor de edición coincide con el interno. + */ + #toEditValue(value: number): number { + return this.opts.formatOptions.current?.style === 'percent' ? value * 100 : value; + } + + #fromEditValue(editValue: number): number { + return this.opts.formatOptions.current?.style === 'percent' ? editValue / 100 : editValue; + } + + /** Formatea para mostrar al usuario (display): aplica style, currency, %, etc. */ #formatValue(value: number | undefined): string { if (value === undefined || Number.isNaN(value)) return ''; return this.#numr.f(value); } + /** Formatea para edición: decimal puro, valor escalado según el estilo. */ + #formatEditValue(value: number | undefined): string { + if (value === undefined || Number.isNaN(value)) return ''; + return this.#editNumr.f(this.#toEditValue(value)); + } + + /** Parsea el string de edición y convierte al valor interno. */ #parseValue(raw: string): number | undefined { if (!raw.trim()) return undefined; - const n = this.#numr.parse(raw); - return Number.isNaN(n) ? undefined : n; + const n = this.#editNumr.parse(raw); + if (Number.isNaN(n)) return undefined; + return this.#fromEditValue(n); } #clamp(value: number): number { @@ -174,7 +209,7 @@ export class NumberFieldRootState { const clamped = this.#clamp(this.#round(value, this.#step)); this.opts.value.current = clamped; this.opts.onValueChange.current?.(clamped); - this.#inputValue = this.#focused ? this.#inputValue : this.#formatValue(clamped); + this.#inputValue = this.#focused ? this.#formatEditValue(clamped) : this.#formatValue(clamped); } // ── Public actions ─────────────────────────────────────────────────────── @@ -195,9 +230,8 @@ export class NumberFieldRootState { readonly handleFocus = (_e: TerraFocusEvent): void => { this.#focused = true; - // Al enfocar, mostrar el número en bruto (sin formato). const raw = this.opts.value.current; - this.#inputValue = raw !== undefined ? String(raw) : ''; + this.#inputValue = raw !== undefined ? this.#formatEditValue(raw) : ''; }; readonly handleBlur = (_e: TerraFocusEvent): void => {