@satellatickets/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,12 +19,12 @@ theme.color.action.primary;
19
19
 
20
20
  ## Qué contiene
21
21
 
22
- | Qué | Ejemplos |
23
- | ----------------------- | ------------------------------------------------------------------------------- |
24
- | Contratos de props | `ButtonProps`, `BadgeProps`, `BoxProps`, `StackProps`, `TextProps`, `IconProps` |
25
- | Constantes de variantes | `buttonVariants`, `badgeVariants`, `textVariants`, `iconSizes` |
26
- | Tema | `UIContext`, `useTheme`, `useColorScheme`, `useBrand`, `resolveTheme` |
27
- | Hooks headless | `useButton` |
22
+ | Qué | Ejemplos |
23
+ | ----------------------- | --------------------------------------------------------------------------------- |
24
+ | Contratos de props | `ButtonProps`, `BadgeProps`, `BoxProps`, `StackProps`, `TextProps`, `IconProps` |
25
+ | Constantes de variantes | `buttonVariants`, `badgeVariants`, `textVariants`, `iconSizes` |
26
+ | Tema | `UIContext`, `useTheme`, `useColorScheme`, `useBrand`, `resolveTheme` |
27
+ | Hooks headless | `useButton`, `useLink`, `useControllableState`, `useFormFieldControl`, `useToast` |
28
28
 
29
29
  Las variantes se exportan como arrays `as const`, además de como tipos, para poder recorrerlas: en historias, en selectores o en tests.
30
30
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,62 @@
1
1
  import { ReactNode } from "react";
2
2
  import { BrandName, Theme, ThemeName, ThemeOverrides } from "@satellatickets/tokens";
3
+ //#region src/form-field/context.d.ts
4
+ /** Lo que `FormField` publica para el control que envuelve (ADR-037). */
5
+ export interface FormFieldValue {
6
+ /** `id` del control. `FormField` lo usa en el `htmlFor` de su etiqueta (web). */
7
+ controlId: string;
8
+ /** `id` del texto de ayuda, si lo hay (web). */
9
+ helpId: string | undefined;
10
+ /** `id` del mensaje de error, si lo hay (web). */
11
+ errorId: string | undefined;
12
+ label: string;
13
+ help: string | undefined;
14
+ error: string | undefined;
15
+ disabled: boolean;
16
+ required: boolean;
17
+ }
18
+ export interface FormFieldValueOptions {
19
+ label: string;
20
+ help?: string | undefined;
21
+ error?: string | undefined;
22
+ disabled?: boolean | undefined;
23
+ required?: boolean | undefined;
24
+ }
25
+ /** Construye el valor del contexto a partir de un id base (el de `useId()`). */
26
+ export declare function createFormFieldValue(baseId: string, { label, help, error, disabled, required }: FormFieldValueOptions): FormFieldValue;
27
+ /** `null` fuera de un `FormField`. */
28
+ export declare const FormFieldContext: import("react").Context<FormFieldValue | null>;
29
+ /** Las props propias de un control que se combinan con las de su `FormField`. */
30
+ export interface FormFieldControlOptions {
31
+ id?: string | undefined;
32
+ invalid?: boolean | undefined;
33
+ disabled?: boolean | undefined;
34
+ required?: boolean | undefined;
35
+ accessibilityLabel?: string | undefined;
36
+ }
37
+ /** Lo que un control necesita para enlazarse con su `FormField`, en web y en nativo. */
38
+ export interface FormFieldControl {
39
+ /** `id` del control (web): el propio o el que espera la etiqueta del campo. */
40
+ id: string | undefined;
41
+ /** Ids de la ayuda y el error para `aria-describedby` (web). */
42
+ describedBy: string | undefined;
43
+ /** Nombre accesible (nativo): el propio o la etiqueta del campo. */
44
+ accessibilityLabel: string | undefined;
45
+ /** El error y la ayuda, uno tras otro, para `accessibilityHint` (nativo). */
46
+ accessibilityHint: string | undefined;
47
+ invalid: boolean;
48
+ disabled: boolean;
49
+ required: boolean;
50
+ }
51
+ /**
52
+ * Combina las props de un control con las de su `FormField`. Las del control
53
+ * mandan en `id` y nombre accesible; `invalid`, `disabled` y `required` se suman:
54
+ * basta con que lo diga uno de los dos.
55
+ */
56
+ export declare function resolveFormFieldControl(field: FormFieldValue | null, { id, invalid, disabled, required, accessibilityLabel }?: FormFieldControlOptions): FormFieldControl;
57
+ /** Enlaza un control con el `FormField` que lo contiene, si lo hay. */
58
+ export declare function useFormFieldControl(options?: FormFieldControlOptions): FormFieldControl;
59
+ //#endregion
3
60
  //#region src/hooks/use-button.d.ts
4
61
  export interface UseButtonOptions {
5
62
  disabled?: boolean | undefined;
@@ -21,6 +78,127 @@ export interface UseButtonResult {
21
78
  */
22
79
  export declare function useButton({ disabled, loading, onPress }: UseButtonOptions): UseButtonResult;
23
80
  //#endregion
81
+ //#region src/hooks/use-controllable-state.d.ts
82
+ export interface UseControllableStateOptions<T> {
83
+ /** Valor controlado por la app. Si existe, manda sobre el estado interno. */
84
+ value?: T | undefined;
85
+ /** Valor inicial cuando el componente guarda su propio estado. */
86
+ defaultValue: T;
87
+ /** Se llama con el valor nuevo cuando cambia, en los dos modos. */
88
+ onChange?: ((value: T) => void) | undefined;
89
+ }
90
+ /**
91
+ * Estado controlado o no, a elección de la app (ADR-037). Con `value`, el componente
92
+ * solo avisa del cambio y espera a que la app le pase el valor nuevo; sin él, guarda
93
+ * el estado a partir de `defaultValue`. No avisa si el valor no cambia.
94
+ */
95
+ export declare function useControllableState<T>({ value, defaultValue, onChange }: UseControllableStateOptions<T>): [T, (next: T) => void];
96
+ //#endregion
97
+ //#region src/types/text.d.ts
98
+ export declare const textVariants: readonly ["hero", "display", "title", "heading", "subheading", "body", "bodySmall", "label", "caption", "code"];
99
+ export type TextVariant = (typeof textVariants)[number];
100
+ export type TextColorToken = keyof Theme['color']['text'];
101
+ export declare const textColors: TextColorToken[];
102
+ export declare const textAligns: readonly ["left", "center", "right"];
103
+ export type TextAlign = (typeof textAligns)[number];
104
+ /** Tokens que componen una variante tipográfica; ambas vistas los leen del tema. */
105
+ export interface TextVariantStyle {
106
+ family: keyof Theme['font']['family'];
107
+ /** Tamaño; `font.lineHeight` comparte las mismas claves (lo comprueba el typecheck de las vistas). */
108
+ size: keyof Theme['font']['size'];
109
+ weight: keyof Theme['font']['weight'];
110
+ uppercase?: boolean;
111
+ /** Espaciado entre letras en px. */
112
+ letterSpacing?: number;
113
+ /** Nivel de encabezado semántico (h1–h4) si la variante es un título. */
114
+ headingLevel?: 1 | 2 | 3 | 4;
115
+ }
116
+ /** Escala tipográfica de Satella: Unbounded para titulares, Hanken Grotesk para texto, IBM Plex Mono para etiquetas. */
117
+ export declare const textVariantStyles: Readonly<Record<TextVariant, TextVariantStyle>>;
118
+ export interface TextProps {
119
+ variant?: TextVariant | undefined;
120
+ color?: TextColorToken | undefined;
121
+ align?: TextAlign | undefined;
122
+ /** Una sola línea con puntos suspensivos. */
123
+ truncate?: boolean | undefined;
124
+ testID?: string | undefined;
125
+ children?: ReactNode;
126
+ }
127
+ //#endregion
128
+ //#region src/types/link.d.ts
129
+ /**
130
+ * Cuándo se subraya un `Link` (ADR-009). `always` es lo correcto dentro de un párrafo:
131
+ * el color solo no basta para distinguir un enlace. `hover` lo subraya al pasar el
132
+ * puntero o enfocarlo en web, y mientras se pulsa en nativo.
133
+ */
134
+ export declare const linkUnderlines: readonly ["always", "hover"];
135
+ export type LinkUnderline = (typeof linkUnderlines)[number];
136
+ /** Lo que recibe `onPress` de un `Link`: permite cancelar la navegación por defecto. */
137
+ export interface LinkPressEvent {
138
+ /** Cancela la navegación por defecto, por ejemplo para hacerla con el router de la app. */
139
+ preventDefault: () => void;
140
+ readonly defaultPrevented: boolean;
141
+ }
142
+ export interface LinkProps {
143
+ /** Destino. En web es el `href` del `<a>`; en nativo se abre con `Linking`. */
144
+ href: string;
145
+ /**
146
+ * Tipografía, con las mismas variantes que `Text`. Sin ella hereda la del texto
147
+ * que lo contiene; fuera de un `Text`, usa `body`.
148
+ */
149
+ variant?: TextVariant | undefined;
150
+ /** Color del texto (por defecto `link`). */
151
+ color?: TextColorToken | undefined;
152
+ underline?: LinkUnderline | undefined;
153
+ /**
154
+ * Se llama antes de navegar. Con `event.preventDefault()` la librería no navega y
155
+ * la app decide qué hacer. En web no se llama si el clic lleva un modificador
156
+ * (Ctrl, Cmd, Mayús, Alt) o no es del botón principal: ahí manda el navegador.
157
+ */
158
+ onPress?: ((event: LinkPressEvent) => void) | undefined;
159
+ /** Nombre accesible cuando el texto visible no basta para describir el destino. */
160
+ accessibilityLabel?: string | undefined;
161
+ /** `data-testid` en web, `testID` en nativo. */
162
+ testID?: string | undefined;
163
+ children: ReactNode;
164
+ }
165
+ //#endregion
166
+ //#region src/hooks/use-link.d.ts
167
+ export interface UseLinkOptions {
168
+ onPress?: ((event: LinkPressEvent) => void) | undefined;
169
+ }
170
+ export interface UseLinkResult {
171
+ /**
172
+ * Llama a `onPress` y devuelve `true` si la navegación por defecto debe seguir,
173
+ * es decir, si nadie llamó a `preventDefault()`.
174
+ */
175
+ press: () => boolean;
176
+ }
177
+ /**
178
+ * Comportamiento de `Link` común a web y nativo: `onPress` se llama antes de navegar
179
+ * y puede cancelar la navegación por defecto. Cada vista aporta esa navegación
180
+ * (el `<a>` en web, `Linking` en nativo).
181
+ */
182
+ export declare function useLink({ onPress }: UseLinkOptions): UseLinkResult;
183
+ //#endregion
184
+ //#region src/tabs/navigation.d.ts
185
+ /** Lo mínimo que hace falta saber de una pestaña para moverse entre ellas. */
186
+ export interface NavigableTab {
187
+ value: string;
188
+ disabled?: boolean | undefined;
189
+ }
190
+ /** Hacia dónde mueve el foco una tecla: flechas, Inicio y Fin. */
191
+ export declare const tabDirections: readonly ["next", "previous", "first", "last"];
192
+ export type TabDirection = (typeof tabDirections)[number];
193
+ /** La primera pestaña habilitada: la elegida por defecto. `undefined` si no hay ninguna. */
194
+ export declare function firstEnabledTab(items: readonly NavigableTab[]): string | undefined;
195
+ /**
196
+ * La pestaña a la que lleva una tecla desde la actual, saltando las deshabilitadas.
197
+ * `next` y `previous` dan la vuelta al llegar al final. Si no hay otra a la que ir,
198
+ * devuelve la actual.
199
+ */
200
+ export declare function getTabInDirection(items: readonly NavigableTab[], current: string, direction: TabDirection): string;
201
+ //#endregion
24
202
  //#region src/types/ui-provider.d.ts
25
203
  /** Modo de tema que acepta `UIProvider` (ADR-010). */
26
204
  export declare const themeModes: readonly ["light", "dark", "system"];
@@ -59,6 +237,121 @@ export declare function mergeTheme(base: Theme, overrides: ThemeOverrides): Them
59
237
  export declare function resolveTheme(scheme: ThemeName, brand?: BrandName): Theme;
60
238
  export declare function resolveColorScheme(mode: ThemeMode, systemScheme: ColorScheme): ColorScheme;
61
239
  //#endregion
240
+ //#region src/types/feedback.d.ts
241
+ /**
242
+ * Los cuatro significados de `color.feedback` en los tokens: éxito, aviso, error e
243
+ * información. Los comparten los componentes que comunican un estado.
244
+ */
245
+ export declare const feedbackTones: readonly ["success", "warning", "danger", "info"];
246
+ export type FeedbackTone = (typeof feedbackTones)[number];
247
+ export declare function isFeedbackTone(value: string): value is FeedbackTone;
248
+ //#endregion
249
+ //#region src/types/toast.d.ts
250
+ /** Acción opcional de un toast, por ejemplo "Deshacer". */
251
+ export interface ToastAction {
252
+ label: string;
253
+ onPress: () => void;
254
+ }
255
+ /** Lo que recibe `show()` de `useToast` (ADR-039). */
256
+ export interface ToastOptions {
257
+ /** Qué comunica (por defecto `info`). Fija el icono y la urgencia con que se anuncia. */
258
+ tone?: FeedbackTone | undefined;
259
+ /** El mensaje, en una línea. */
260
+ title: string;
261
+ /** Detalle opcional. */
262
+ description?: string | undefined;
263
+ /**
264
+ * Milisegundos hasta que se cierra solo (por defecto 5000). Con `0` no se cierra
265
+ * solo: necesita `closeLabel`, una `action` o una llamada a `dismiss`.
266
+ */
267
+ duration?: number | undefined;
268
+ /** Botón de acción. Al pulsarlo se llama a `onPress` y el toast se cierra. */
269
+ action?: ToastAction | undefined;
270
+ /**
271
+ * Si existe, el toast muestra un botón de cierre con este nombre accesible
272
+ * ("Cerrar aviso"). La librería no trae textos propios.
273
+ */
274
+ closeLabel?: string | undefined;
275
+ }
276
+ /** Un toast ya en la cola: sus opciones resueltas y su identificador. */
277
+ export interface ToastItem {
278
+ id: string;
279
+ tone: FeedbackTone;
280
+ title: string;
281
+ description: string | undefined;
282
+ duration: number;
283
+ action: ToastAction | undefined;
284
+ closeLabel: string | undefined;
285
+ }
286
+ /** Lo que devuelve `useToast`. */
287
+ export interface ToastApi {
288
+ /** Muestra un toast y devuelve su `id`. */
289
+ show: (options: ToastOptions) => string;
290
+ /** Cierra el toast con ese `id`; sin `id`, los cierra todos. */
291
+ dismiss: (id?: string) => void;
292
+ }
293
+ //#endregion
294
+ //#region src/toast/store.d.ts
295
+ /** Milisegundos que un toast permanece visible si no se indica otra cosa. */
296
+ export declare const TOAST_DEFAULT_DURATION = 5000;
297
+ /** Toasts visibles a la vez. Al llegar uno más, se retira el más antiguo. */
298
+ export declare const TOAST_MAX_VISIBLE = 3;
299
+ interface ToastStore extends ToastApi {
300
+ /** Para `useSyncExternalStore`: avisa de cada cambio y devuelve cómo dejar de escuchar. */
301
+ subscribe: (listener: () => void) => () => void;
302
+ /** Los toasts visibles, del más antiguo al más reciente. Cambia de identidad con cada cambio. */
303
+ getSnapshot: () => readonly ToastItem[];
304
+ /** Detiene el cierre automático, por ejemplo mientras el puntero está sobre los toasts. */
305
+ pause: () => void;
306
+ /** Reanuda el cierre automático con el tiempo que le quedaba a cada toast. */
307
+ resume: () => void;
308
+ }
309
+ /**
310
+ * La cola de toasts de un `UIProvider` (ADR-039): alta, baja, cierre automático, pausa
311
+ * y límite de visibles. No depende de React ni de la plataforma.
312
+ */
313
+ export declare function createToastStore(): ToastStore;
314
+ //#endregion
315
+ //#region src/toast/context.d.ts
316
+ /** La cola de toasts del `UIProvider` más cercano; `null` fuera de uno. */
317
+ export declare const ToastContext: import("react").Context<ToastStore | null>;
318
+ /**
319
+ * Muestra y cierra toasts (ADR-039). Necesita un `UIProvider` por encima: es él quien
320
+ * guarda la cola y los pinta.
321
+ */
322
+ export declare function useToast(): ToastApi;
323
+ //#endregion
324
+ //#region src/types/alert.d.ts
325
+ interface AlertBaseProps {
326
+ /** Qué comunica (por defecto `info`). Fija el color y el icono. */
327
+ tone?: FeedbackTone | undefined;
328
+ /** Resumen en una línea. */
329
+ title?: string | undefined;
330
+ /** Explicación o siguiente paso. */
331
+ children?: ReactNode;
332
+ /** `data-testid` en web, `testID` en nativo. */
333
+ testID?: string | undefined;
334
+ }
335
+ interface AlertStaticProps extends AlertBaseProps {
336
+ onClose?: undefined;
337
+ closeLabel?: undefined;
338
+ }
339
+ interface AlertClosableProps extends AlertBaseProps {
340
+ /** Si existe, el aviso muestra un botón para cerrarlo y la llama al pulsarlo. */
341
+ onClose: () => void;
342
+ /**
343
+ * Nombre accesible del botón de cierre ("Cerrar aviso"). Es obligatorio con
344
+ * `onClose`: la librería no trae textos propios.
345
+ */
346
+ closeLabel: string;
347
+ }
348
+ /**
349
+ * Contrato de `Alert`: un mensaje dentro de la página que no desaparece solo. `danger`
350
+ * y `warning` se anuncian de inmediato a los lectores de pantalla; `success` e `info`,
351
+ * cuando terminan lo que estén leyendo.
352
+ */
353
+ export type AlertProps = AlertStaticProps | AlertClosableProps;
354
+ //#endregion
62
355
  //#region src/types/badge.d.ts
63
356
  /**
64
357
  * Variantes de `Badge` (ADR-009): una por cada color de `color.feedback`. Las historias
@@ -146,46 +439,123 @@ export interface ButtonProps<IconName extends string = string> {
146
439
  children: ReactNode;
147
440
  }
148
441
  //#endregion
149
- //#region src/types/text.d.ts
150
- export declare const textVariants: readonly ["hero", "display", "title", "heading", "subheading", "body", "bodySmall", "label", "caption", "code"];
151
- export type TextVariant = (typeof textVariants)[number];
152
- export type TextColorToken = keyof Theme['color']['text'];
153
- export declare const textColors: TextColorToken[];
154
- export declare const textAligns: readonly ["left", "center", "right"];
155
- export type TextAlign = (typeof textAligns)[number];
156
- /** Tokens que componen una variante tipográfica; ambas vistas los leen del tema. */
157
- export interface TextVariantStyle {
158
- family: keyof Theme['font']['family'];
159
- /** Tamaño; `font.lineHeight` comparte las mismas claves (lo comprueba el typecheck de las vistas). */
160
- size: keyof Theme['font']['size'];
161
- weight: keyof Theme['font']['weight'];
162
- uppercase?: boolean;
163
- /** Espaciado entre letras en px. */
164
- letterSpacing?: number;
165
- /** Nivel de encabezado semántico (h1–h4) si la variante es un título. */
166
- headingLevel?: 1 | 2 | 3 | 4;
442
+ //#region src/types/card.d.ts
443
+ /**
444
+ * Variantes de `Card` (ADR-009): `outlined` se apoya en el borde; `elevated` añade
445
+ * sombra para separarse de lo que tiene debajo.
446
+ */
447
+ export declare const cardVariants: readonly ["outlined", "elevated"];
448
+ export type CardVariant = (typeof cardVariants)[number];
449
+ /**
450
+ * Contrato de `Card`: una superficie que agrupa contenido relacionado. Con `onPress`
451
+ * toda la tarjeta es pulsable; en ese caso no debe contener otros controles.
452
+ */
453
+ export interface CardProps {
454
+ variant?: CardVariant | undefined;
455
+ /** Relleno interior (por defecto 4). Con `0`, el contenido llega hasta el borde, por ejemplo una imagen. */
456
+ padding?: SpaceToken | undefined;
457
+ /** Hace pulsable toda la tarjeta, con ratón, teclado o toque. */
458
+ onPress?: (() => void) | undefined;
459
+ /**
460
+ * Nombre accesible de una tarjeta pulsable. Sin él, es su contenido, que suele ser lo
461
+ * correcto. Si se indica, tiene que incluir el texto visible: quien navega por voz
462
+ * dice lo que ve.
463
+ */
464
+ accessibilityLabel?: string | undefined;
465
+ /** `data-testid` en web, `testID` en nativo. */
466
+ testID?: string | undefined;
467
+ children?: ReactNode;
167
468
  }
168
- /** Escala tipográfica de Satella: Unbounded para titulares, Hanken Grotesk para texto, IBM Plex Mono para etiquetas. */
169
- export declare const textVariantStyles: Readonly<Record<TextVariant, TextVariantStyle>>;
170
- export interface TextProps {
171
- variant?: TextVariant | undefined;
172
- color?: TextColorToken | undefined;
173
- align?: TextAlign | undefined;
174
- /** Una sola línea con puntos suspensivos. */
175
- truncate?: boolean | undefined;
469
+ //#endregion
470
+ //#region src/types/checkbox.d.ts
471
+ /** Contrato de `Checkbox`: una opción que se marca o no, con su etiqueta como `children`. */
472
+ export interface CheckboxProps {
473
+ /** Estado controlado por la app. Sin él, guarda su propio estado (ADR-037). */
474
+ checked?: boolean | undefined;
475
+ /** Estado inicial cuando guarda su propio estado. */
476
+ defaultChecked?: boolean | undefined;
477
+ /**
478
+ * Ni marcado ni sin marcar: representa a un grupo donde solo algunas opciones lo
479
+ * están. Es solo visual; al pulsarlo pasa a marcado o sin marcar según `checked`.
480
+ */
481
+ indeterminate?: boolean | undefined;
482
+ /** Se llama con el estado nuevo al pulsarlo. */
483
+ onCheckedChange?: ((checked: boolean) => void) | undefined;
484
+ /** No se puede pulsar ni enfocar. */
485
+ disabled?: boolean | undefined;
486
+ /** El estado no es válido, por ejemplo unas condiciones sin aceptar. */
487
+ invalid?: boolean | undefined;
488
+ required?: boolean | undefined;
489
+ /** Nombre accesible cuando no hay etiqueta visible. */
490
+ accessibilityLabel?: string | undefined;
491
+ /** `data-testid` en web, `testID` en nativo. */
176
492
  testID?: string | undefined;
493
+ /** Etiqueta. Pulsarla también marca la casilla. */
177
494
  children?: ReactNode;
178
495
  }
179
496
  //#endregion
497
+ //#region src/types/control.d.ts
498
+ /**
499
+ * Alturas de los controles interactivos de una línea (`Input`, `Select`). Coinciden
500
+ * con `size.control` de los tokens y con los tamaños de `Button`, para que un campo y
501
+ * un botón del mismo tamaño queden alineados en una fila.
502
+ */
503
+ export declare const controlSizes: readonly ["sm", "md", "lg"];
504
+ export type ControlSize = (typeof controlSizes)[number];
505
+ //#endregion
506
+ //#region src/types/divider.d.ts
507
+ /** Orientación de `Divider` (ADR-009). */
508
+ export declare const dividerOrientations: readonly ["horizontal", "vertical"];
509
+ export type DividerOrientation = (typeof dividerOrientations)[number];
510
+ /** Contrato de `Divider`: una línea fina que separa dos bloques de contenido. */
511
+ export interface DividerProps {
512
+ /**
513
+ * `horizontal` (por defecto) separa bloques apilados. `vertical` separa elementos de
514
+ * una fila y ocupa su altura.
515
+ */
516
+ orientation?: DividerOrientation | undefined;
517
+ /** `data-testid` en web, `testID` en nativo. */
518
+ testID?: string | undefined;
519
+ }
520
+ //#endregion
521
+ //#region src/types/form-field.d.ts
522
+ /**
523
+ * Contrato de `FormField`: la etiqueta, la ayuda y el error de un control, enlazados
524
+ * con él para los lectores de pantalla (ADR-037). Envuelve un `Input`, un `TextArea`
525
+ * o un `Select`.
526
+ */
527
+ export interface FormFieldProps {
528
+ /** Etiqueta visible. Es texto porque en nativo hace de nombre accesible del control. */
529
+ label: string;
530
+ /** Ayuda bajo el control: formato esperado, para qué se usa el dato. */
531
+ help?: string | undefined;
532
+ /** Mensaje de error. Si existe, el control se marca como inválido. */
533
+ error?: string | undefined;
534
+ /** Marca la etiqueta como obligatoria y lo comunica al control. */
535
+ required?: boolean | undefined;
536
+ /** Deshabilita el control. */
537
+ disabled?: boolean | undefined;
538
+ /** `data-testid` en web, `testID` en nativo. */
539
+ testID?: string | undefined;
540
+ /** El control. */
541
+ children: ReactNode;
542
+ }
543
+ //#endregion
180
544
  //#region src/types/icon.d.ts
181
- export declare const iconSizes: readonly ["sm", "md", "lg"];
545
+ export declare const iconSizes: readonly ["sm", "md", "lg", "xl"];
182
546
  export type IconSize = (typeof iconSizes)[number];
183
547
  /** Tamaño en px/puntos de cada icono. */
184
548
  export declare const iconSizePx: {
185
549
  sm: number;
186
550
  md: number;
187
551
  lg: number;
552
+ xl: number;
188
553
  };
554
+ /**
555
+ * Color de un icono: un color de texto o el de un estado de feedback (`success`,
556
+ * `warning`, `danger`, `info`), que usa el token `color.feedback.<estado>.icon`.
557
+ */
558
+ export type IconColor = TextColorToken | FeedbackTone;
189
559
  /**
190
560
  * Contrato de `Icon`. El conjunto de nombres lo aporta `@satellatickets/icons`,
191
561
  * que está fuera de `core` (ADR-002), por eso el nombre es un parámetro de tipo.
@@ -193,24 +563,214 @@ export declare const iconSizePx: {
193
563
  export interface IconProps<Name extends string = string> {
194
564
  name: Name;
195
565
  size?: IconSize | undefined;
196
- color?: TextColorToken | undefined;
566
+ color?: IconColor | undefined;
197
567
  /** Texto alternativo. Sin él, el icono es decorativo y se oculta a los lectores de pantalla. */
198
568
  label?: string | undefined;
199
569
  testID?: string | undefined;
200
570
  }
201
571
  //#endregion
572
+ //#region src/types/icon-button.d.ts
573
+ /**
574
+ * Tamaño del icono para cada tamaño de `IconButton`. Es un paso mayor que en
575
+ * `Button`, porque aquí el icono es todo el contenido.
576
+ */
577
+ export declare const iconButtonIconSize: {
578
+ readonly sm: "sm";
579
+ readonly md: "md";
580
+ readonly lg: "lg";
581
+ };
582
+ /**
583
+ * Contrato de `IconButton`: un `Button` cuadrado cuyo único contenido es un icono.
584
+ * Comparte variantes y tamaños con `Button` (`buttonVariants`). El conjunto de iconos
585
+ * lo aporta `@satellatickets/icons`, que está fuera de `core` (ADR-002), por eso el
586
+ * nombre es un parámetro de tipo.
587
+ */
588
+ export interface IconButtonProps<IconName extends string = string> {
589
+ icon: IconName;
590
+ /**
591
+ * Nombre accesible. Es obligatorio porque no hay texto visible: describe la
592
+ * acción ("Cerrar"), no el dibujo ("Aspa").
593
+ */
594
+ label: string;
595
+ /** Jerarquía visual de la acción (por defecto `ghost`). */
596
+ variant?: ButtonVariant | undefined;
597
+ size?: ButtonSize | undefined;
598
+ /** No se puede pulsar ni enfocar. */
599
+ disabled?: boolean | undefined;
600
+ /** Acción en curso: sustituye el icono por un spinner y no dispara `onPress`, pero conserva el foco. */
601
+ loading?: boolean | undefined;
602
+ /** Se pulsa con ratón, teclado o toque. Nombre neutral: la vista web lo mapea a `click`. */
603
+ onPress?: (() => void) | undefined;
604
+ /** `data-testid` en web, `testID` en nativo. */
605
+ testID?: string | undefined;
606
+ }
607
+ //#endregion
608
+ //#region src/types/input.d.ts
609
+ /**
610
+ * Qué se escribe en un `Input` (ADR-009). Cada vista lo traduce a su plataforma:
611
+ * el atributo `type` y el teclado en web, y el teclado, las mayúsculas automáticas
612
+ * y el autocompletado en nativo.
613
+ */
614
+ export declare const inputTypes: readonly ["text", "email", "password", "search", "tel", "url", "number"];
615
+ export type InputType = (typeof inputTypes)[number];
616
+ /** Tamaño de los iconos de un `Input` para cada tamaño de campo. */
617
+ export declare const inputIconSize: {
618
+ readonly sm: "sm";
619
+ readonly md: "md";
620
+ readonly lg: "md";
621
+ };
622
+ /**
623
+ * Contrato de `Input`: un campo de texto de una línea. El conjunto de iconos lo aporta
624
+ * `@satellatickets/icons`, que está fuera de `core` (ADR-002), por eso el nombre es
625
+ * un parámetro de tipo.
626
+ */
627
+ export interface InputProps<IconName extends string = string> {
628
+ /** Texto controlado por la app. Sin él, el campo guarda su propio estado (ADR-037). */
629
+ value?: string | undefined;
630
+ /** Texto inicial cuando el campo guarda su propio estado. */
631
+ defaultValue?: string | undefined;
632
+ /** Se llama con el texto nuevo en cada cambio. */
633
+ onChangeText?: ((text: string) => void) | undefined;
634
+ placeholder?: string | undefined;
635
+ /** Qué se escribe (por defecto `text`). */
636
+ type?: InputType | undefined;
637
+ size?: ControlSize | undefined;
638
+ /** No se puede editar ni enfocar. */
639
+ disabled?: boolean | undefined;
640
+ /** Se puede enfocar y copiar, pero no editar. */
641
+ readOnly?: boolean | undefined;
642
+ /** El valor no es válido. Dentro de un `FormField` con `error` se activa solo. */
643
+ invalid?: boolean | undefined;
644
+ required?: boolean | undefined;
645
+ /** Icono decorativo delante del texto. */
646
+ iconStart?: IconName | undefined;
647
+ /** Icono decorativo detrás del texto. */
648
+ iconEnd?: IconName | undefined;
649
+ maxLength?: number | undefined;
650
+ onFocus?: (() => void) | undefined;
651
+ onBlur?: (() => void) | undefined;
652
+ /** Se pulsa Intro, o la tecla de envío del teclado en pantalla. */
653
+ onSubmit?: (() => void) | undefined;
654
+ /** Nombre accesible cuando no hay un `FormField` que lo etiquete. */
655
+ accessibilityLabel?: string | undefined;
656
+ /** `data-testid` en web, `testID` en nativo. */
657
+ testID?: string | undefined;
658
+ }
659
+ //#endregion
660
+ //#region src/types/modal.d.ts
661
+ /**
662
+ * Contrato de `Modal` y de `Sheet` (ADR-040): un diálogo que interrumpe lo que hay
663
+ * debajo. `Modal` se centra; `Sheet` se ancla al borde inferior.
664
+ */
665
+ export interface ModalProps {
666
+ /** Si está abierto. Lo decide la app. */
667
+ open: boolean;
668
+ /**
669
+ * El diálogo pide cerrarse: Escape, pulsar fuera, el botón atrás de Android o su
670
+ * botón de cierre. La app responde poniendo `open` a `false`.
671
+ */
672
+ onClose: () => void;
673
+ /** Título visible. Es también su nombre accesible. */
674
+ title: string;
675
+ /** Texto bajo el título. Los lectores de pantalla lo leen al abrirse. */
676
+ description?: string | undefined;
677
+ /**
678
+ * Si existe, muestra un botón de cierre con este nombre accesible ("Cerrar"). La
679
+ * librería no trae textos propios.
680
+ */
681
+ closeLabel?: string | undefined;
682
+ /**
683
+ * Con `false`, ni Escape, ni pulsar fuera, ni el botón atrás lo cierran: es para una
684
+ * pregunta que exige respuesta. Por defecto `true`.
685
+ */
686
+ dismissible?: boolean | undefined;
687
+ /** Acciones al pie, normalmente botones. */
688
+ footer?: ReactNode;
689
+ /** `data-testid` en web, `testID` en nativo. */
690
+ testID?: string | undefined;
691
+ children?: ReactNode;
692
+ }
693
+ /** `Sheet` tiene el mismo contrato que `Modal`: solo cambia dónde se coloca. */
694
+ export type SheetProps = ModalProps;
695
+ /** Las dos presentaciones de un diálogo. */
696
+ export declare const modalPresentations: readonly ["dialog", "sheet"];
697
+ export type ModalPresentation = (typeof modalPresentations)[number];
698
+ //#endregion
699
+ //#region src/types/select.d.ts
700
+ /** Una opción de `Select`. */
701
+ export interface SelectOption {
702
+ /** Lo que recibe `onValueChange`. No puede ser una cadena vacía: eso es "sin elegir". */
703
+ value: string;
704
+ /** Lo que se muestra. */
705
+ label: string;
706
+ disabled?: boolean | undefined;
707
+ }
708
+ /** Contrato de `Select`: elegir una opción de una lista (ADR-038). */
709
+ export interface SelectProps {
710
+ options: readonly SelectOption[];
711
+ /** Opción elegida, controlada por la app. Sin ella, guarda su propio estado (ADR-037). */
712
+ value?: string | undefined;
713
+ /** Opción inicial cuando guarda su propio estado. */
714
+ defaultValue?: string | undefined;
715
+ /** Se llama con el `value` de la opción elegida. */
716
+ onValueChange?: ((value: string) => void) | undefined;
717
+ /** Texto que se muestra mientras no hay ninguna opción elegida. */
718
+ placeholder?: string | undefined;
719
+ size?: ControlSize | undefined;
720
+ /** No se puede abrir ni enfocar. */
721
+ disabled?: boolean | undefined;
722
+ /** El valor no es válido. Dentro de un `FormField` con `error` se activa solo. */
723
+ invalid?: boolean | undefined;
724
+ required?: boolean | undefined;
725
+ /** Nombre accesible cuando no hay un `FormField` que lo etiquete. */
726
+ accessibilityLabel?: string | undefined;
727
+ /** `data-testid` en web, `testID` en nativo. */
728
+ testID?: string | undefined;
729
+ }
730
+ //#endregion
731
+ //#region src/types/skeleton.d.ts
732
+ /**
733
+ * Formas de `Skeleton` (ADR-009): `text` ocupa el hueco de una línea de `Text`,
734
+ * `rectangle` el de una imagen o un control, y `circle` el de un avatar.
735
+ */
736
+ export declare const skeletonShapes: readonly ["text", "rectangle", "circle"];
737
+ export type SkeletonShape = (typeof skeletonShapes)[number];
738
+ /**
739
+ * Contrato de `Skeleton`: el hueco de un contenido que todavía se está cargando. Es
740
+ * decorativo; que la zona está cargando lo comunica quien lo contiene.
741
+ */
742
+ export interface SkeletonProps {
743
+ /** Forma (por defecto `text`). */
744
+ shape?: SkeletonShape | undefined;
745
+ /**
746
+ * Ancho, en puntos o como porcentaje del contenedor (`'60%'`). Por defecto ocupa
747
+ * todo el ancho; en `circle`, mide lo mismo que de alto.
748
+ */
749
+ width?: number | `${number}%` | undefined;
750
+ /** Alto en puntos. En `text` no se usa: lo fija `variant`. */
751
+ height?: number | undefined;
752
+ /** Con `text`: la variante de `Text` cuyo hueco ocupa (por defecto `body`). */
753
+ variant?: TextVariant | undefined;
754
+ /** Con `text`: número de líneas (por defecto 1). Si hay varias, la última es más corta. */
755
+ lines?: number | undefined;
756
+ /** `data-testid` en web, `testID` en nativo. */
757
+ testID?: string | undefined;
758
+ }
759
+ //#endregion
202
760
  //#region src/types/spinner.d.ts
203
761
  /**
204
762
  * `Spinner` comparte la escala de `Icon`: así puede ocupar el sitio de un icono
205
763
  * (por ejemplo, dentro de un botón que carga) sin mover el resto del contenido.
764
+ * `xl` es para la carga de una página o de una sección entera.
206
765
  */
207
- export declare const spinnerSizes: readonly ["sm", "md", "lg"];
766
+ export declare const spinnerSizes: readonly ["sm", "md", "lg", "xl"];
208
767
  export type SpinnerSize = IconSize;
209
768
  /** Tamaño en px/puntos de cada spinner. */
210
769
  export declare const spinnerSizePx: {
211
770
  sm: number;
212
771
  md: number;
213
772
  lg: number;
773
+ xl: number;
214
774
  };
215
775
  export interface SpinnerProps {
216
776
  size?: SpinnerSize | undefined;
@@ -239,4 +799,89 @@ export interface StackProps extends BoxProps {
239
799
  wrap?: boolean | undefined;
240
800
  }
241
801
  //#endregion
242
- export type { UIContextValue };
802
+ //#region src/types/switch.d.ts
803
+ /**
804
+ * Contrato de `Switch`: un ajuste que se activa o desactiva con efecto inmediato.
805
+ * Para una opción que se confirma al enviar un formulario, usa `Checkbox`.
806
+ */
807
+ export interface SwitchProps {
808
+ /** Estado controlado por la app. Sin él, guarda su propio estado (ADR-037). */
809
+ checked?: boolean | undefined;
810
+ /** Estado inicial cuando guarda su propio estado. */
811
+ defaultChecked?: boolean | undefined;
812
+ /** Se llama con el estado nuevo al pulsarlo. */
813
+ onCheckedChange?: ((checked: boolean) => void) | undefined;
814
+ /** No se puede pulsar ni enfocar. */
815
+ disabled?: boolean | undefined;
816
+ /** Nombre accesible cuando no hay etiqueta visible. */
817
+ accessibilityLabel?: string | undefined;
818
+ /** `data-testid` en web, `testID` en nativo. */
819
+ testID?: string | undefined;
820
+ /** Etiqueta. Pulsarla también cambia el interruptor. */
821
+ children?: ReactNode;
822
+ }
823
+ //#endregion
824
+ //#region src/types/tabs.d.ts
825
+ /** Una pestaña de `Tabs`. */
826
+ export interface TabItem<IconName extends string = string> {
827
+ /** Lo que recibe `onValueChange`. */
828
+ value: string;
829
+ /** Texto de la pestaña. */
830
+ label: string;
831
+ /** Icono decorativo delante del texto. */
832
+ icon?: IconName | undefined;
833
+ disabled?: boolean | undefined;
834
+ /**
835
+ * Contenido de su panel. Sin él, `Tabs` solo pinta las pestañas y la app decide qué
836
+ * mostrar con `value`, por ejemplo para filtrar una lista.
837
+ */
838
+ content?: ReactNode;
839
+ }
840
+ /**
841
+ * Contrato de `Tabs`: varias vistas del mismo nivel entre las que se cambia sin salir de
842
+ * la página. El conjunto de iconos lo aporta `@satellatickets/icons`, que está fuera de
843
+ * `core` (ADR-002), por eso el nombre es un parámetro de tipo.
844
+ */
845
+ export interface TabsProps<IconName extends string = string> {
846
+ items: readonly TabItem<IconName>[];
847
+ /** Pestaña elegida, controlada por la app. Sin ella, guarda su propio estado (ADR-037). */
848
+ value?: string | undefined;
849
+ /** Pestaña inicial cuando guarda su propio estado. Por defecto, la primera habilitada. */
850
+ defaultValue?: string | undefined;
851
+ /** Se llama con el `value` de la pestaña elegida. */
852
+ onValueChange?: ((value: string) => void) | undefined;
853
+ /** Nombre accesible del grupo de pestañas ("Mis entradas"). */
854
+ accessibilityLabel?: string | undefined;
855
+ /** `data-testid` en web, `testID` en nativo. */
856
+ testID?: string | undefined;
857
+ }
858
+ //#endregion
859
+ //#region src/types/text-area.d.ts
860
+ /** Contrato de `TextArea`: un campo de texto de varias líneas. */
861
+ export interface TextAreaProps {
862
+ /** Texto controlado por la app. Sin él, el campo guarda su propio estado (ADR-037). */
863
+ value?: string | undefined;
864
+ /** Texto inicial cuando el campo guarda su propio estado. */
865
+ defaultValue?: string | undefined;
866
+ /** Se llama con el texto nuevo en cada cambio. */
867
+ onChangeText?: ((text: string) => void) | undefined;
868
+ placeholder?: string | undefined;
869
+ /** Líneas visibles sin desplazarse (por defecto 3). Fija la altura mínima. */
870
+ rows?: number | undefined;
871
+ /** No se puede editar ni enfocar. */
872
+ disabled?: boolean | undefined;
873
+ /** Se puede enfocar y copiar, pero no editar. */
874
+ readOnly?: boolean | undefined;
875
+ /** El valor no es válido. Dentro de un `FormField` con `error` se activa solo. */
876
+ invalid?: boolean | undefined;
877
+ required?: boolean | undefined;
878
+ maxLength?: number | undefined;
879
+ onFocus?: (() => void) | undefined;
880
+ onBlur?: (() => void) | undefined;
881
+ /** Nombre accesible cuando no hay un `FormField` que lo etiquete. */
882
+ accessibilityLabel?: string | undefined;
883
+ /** `data-testid` en web, `testID` en nativo. */
884
+ testID?: string | undefined;
885
+ }
886
+ //#endregion
887
+ export type { ToastStore, UIContextValue };
package/dist/index.js CHANGED
@@ -1,5 +1,59 @@
1
- import { createContext, useCallback, useContext } from "react";
1
+ import { createContext, useCallback, useContext, useState } from "react";
2
2
  import { brands, themes } from "@satellatickets/tokens";
3
+ //#region src/form-field/context.ts
4
+ /** Un texto vacío cuenta como ausente: `error={errores.email}` suele llegar así. */
5
+ function present(text) {
6
+ return text === void 0 || text === "" ? void 0 : text;
7
+ }
8
+ /** Construye el valor del contexto a partir de un id base (el de `useId()`). */
9
+ function createFormFieldValue(baseId, { label, help, error, disabled = false, required = false }) {
10
+ const helpText = present(help);
11
+ const errorText = present(error);
12
+ return {
13
+ controlId: `${baseId}-control`,
14
+ helpId: helpText === void 0 ? void 0 : `${baseId}-help`,
15
+ errorId: errorText === void 0 ? void 0 : `${baseId}-error`,
16
+ label,
17
+ help: helpText,
18
+ error: errorText,
19
+ disabled,
20
+ required
21
+ };
22
+ }
23
+ /** `null` fuera de un `FormField`. */
24
+ const FormFieldContext = createContext(null);
25
+ /**
26
+ * Combina las props de un control con las de su `FormField`. Las del control
27
+ * mandan en `id` y nombre accesible; `invalid`, `disabled` y `required` se suman:
28
+ * basta con que lo diga uno de los dos.
29
+ */
30
+ function resolveFormFieldControl(field, { id, invalid = false, disabled = false, required = false, accessibilityLabel } = {}) {
31
+ if (field === null) return {
32
+ id,
33
+ describedBy: void 0,
34
+ accessibilityLabel,
35
+ accessibilityHint: void 0,
36
+ invalid,
37
+ disabled,
38
+ required
39
+ };
40
+ const describedBy = [field.helpId, field.errorId].filter((part) => part !== void 0).join(" ");
41
+ const hint = [field.error, field.help].filter((part) => part !== void 0).join(" ");
42
+ return {
43
+ id: id ?? field.controlId,
44
+ describedBy: describedBy === "" ? void 0 : describedBy,
45
+ accessibilityLabel: accessibilityLabel ?? field.label,
46
+ accessibilityHint: hint === "" ? void 0 : hint,
47
+ invalid: invalid || field.error !== void 0,
48
+ disabled: disabled || field.disabled,
49
+ required: required || field.required
50
+ };
51
+ }
52
+ /** Enlaza un control con el `FormField` que lo contiene, si lo hay. */
53
+ function useFormFieldControl(options) {
54
+ return resolveFormFieldControl(useContext(FormFieldContext), options);
55
+ }
56
+ //#endregion
3
57
  //#region src/hooks/use-button.ts
4
58
  /**
5
59
  * Comportamiento de `Button` común a web y nativo: un botón deshabilitado o que
@@ -18,6 +72,78 @@ function useButton({ disabled = false, loading = false, onPress }) {
18
72
  };
19
73
  }
20
74
  //#endregion
75
+ //#region src/hooks/use-controllable-state.ts
76
+ /**
77
+ * Estado controlado o no, a elección de la app (ADR-037). Con `value`, el componente
78
+ * solo avisa del cambio y espera a que la app le pase el valor nuevo; sin él, guarda
79
+ * el estado a partir de `defaultValue`. No avisa si el valor no cambia.
80
+ */
81
+ function useControllableState({ value, defaultValue, onChange }) {
82
+ const [internal, setInternal] = useState(defaultValue);
83
+ const controlled = value !== void 0;
84
+ const current = controlled ? value : internal;
85
+ return [current, useCallback((next) => {
86
+ if (Object.is(next, current)) return;
87
+ if (!controlled) setInternal(next);
88
+ onChange?.(next);
89
+ }, [
90
+ controlled,
91
+ current,
92
+ onChange
93
+ ])];
94
+ }
95
+ //#endregion
96
+ //#region src/hooks/use-link.ts
97
+ /**
98
+ * Comportamiento de `Link` común a web y nativo: `onPress` se llama antes de navegar
99
+ * y puede cancelar la navegación por defecto. Cada vista aporta esa navegación
100
+ * (el `<a>` en web, `Linking` en nativo).
101
+ */
102
+ function useLink({ onPress }) {
103
+ return { press: useCallback(() => {
104
+ let prevented = false;
105
+ onPress?.({
106
+ preventDefault: () => {
107
+ prevented = true;
108
+ },
109
+ get defaultPrevented() {
110
+ return prevented;
111
+ }
112
+ });
113
+ return !prevented;
114
+ }, [onPress]) };
115
+ }
116
+ //#endregion
117
+ //#region src/tabs/navigation.ts
118
+ /** Hacia dónde mueve el foco una tecla: flechas, Inicio y Fin. */
119
+ const tabDirections = [
120
+ "next",
121
+ "previous",
122
+ "first",
123
+ "last"
124
+ ];
125
+ /** La primera pestaña habilitada: la elegida por defecto. `undefined` si no hay ninguna. */
126
+ function firstEnabledTab(items) {
127
+ return items.find((item) => item.disabled !== true)?.value;
128
+ }
129
+ /**
130
+ * La pestaña a la que lleva una tecla desde la actual, saltando las deshabilitadas.
131
+ * `next` y `previous` dan la vuelta al llegar al final. Si no hay otra a la que ir,
132
+ * devuelve la actual.
133
+ */
134
+ function getTabInDirection(items, current, direction) {
135
+ const enabled = items.filter((item) => item.disabled !== true).map((item) => item.value);
136
+ if (enabled.length === 0) return current;
137
+ const [first] = enabled;
138
+ const last = enabled[enabled.length - 1];
139
+ if (first === void 0 || last === void 0) return current;
140
+ if (direction === "first") return first;
141
+ if (direction === "last") return last;
142
+ const index = enabled.indexOf(current);
143
+ if (index === -1) return direction === "next" ? first : last;
144
+ return enabled[(index + (direction === "next" ? 1 : -1) + enabled.length) % enabled.length] ?? current;
145
+ }
146
+ //#endregion
21
147
  //#region src/theme/resolve-theme.ts
22
148
  function isPlainObject(value) {
23
149
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -78,6 +204,110 @@ function useBrand() {
78
204
  return useContext(UIContext).brand;
79
205
  }
80
206
  //#endregion
207
+ //#region src/toast/context.ts
208
+ /** La cola de toasts del `UIProvider` más cercano; `null` fuera de uno. */
209
+ const ToastContext = createContext(null);
210
+ /**
211
+ * Muestra y cierra toasts (ADR-039). Necesita un `UIProvider` por encima: es él quien
212
+ * guarda la cola y los pinta.
213
+ */
214
+ function useToast() {
215
+ const store = useContext(ToastContext);
216
+ if (store === null) throw new Error("useToast necesita un <UIProvider> por encima: es él quien pinta los toasts.");
217
+ return store;
218
+ }
219
+ //#endregion
220
+ //#region src/toast/store.ts
221
+ /** Milisegundos que un toast permanece visible si no se indica otra cosa. */
222
+ const TOAST_DEFAULT_DURATION = 5e3;
223
+ /** Toasts visibles a la vez. Al llegar uno más, se retira el más antiguo. */
224
+ const TOAST_MAX_VISIBLE = 3;
225
+ /**
226
+ * La cola de toasts de un `UIProvider` (ADR-039): alta, baja, cierre automático, pausa
227
+ * y límite de visibles. No depende de React ni de la plataforma.
228
+ */
229
+ function createToastStore() {
230
+ let items = [];
231
+ let paused = false;
232
+ let nextId = 0;
233
+ const timers = /* @__PURE__ */ new Map();
234
+ const listeners = /* @__PURE__ */ new Set();
235
+ function emit(next) {
236
+ items = next;
237
+ for (const listener of listeners) listener();
238
+ }
239
+ function stop(id) {
240
+ const timer = timers.get(id);
241
+ if (timer?.handle !== void 0) clearTimeout(timer.handle);
242
+ timers.delete(id);
243
+ }
244
+ function start(id, remaining) {
245
+ const timer = {
246
+ handle: void 0,
247
+ startedAt: Date.now(),
248
+ remaining
249
+ };
250
+ if (!paused) timer.handle = setTimeout(() => dismiss(id), remaining);
251
+ timers.set(id, timer);
252
+ }
253
+ function dismiss(id) {
254
+ if (id === void 0) {
255
+ if (items.length === 0) return;
256
+ for (const item of items) stop(item.id);
257
+ emit([]);
258
+ return;
259
+ }
260
+ if (!items.some((item) => item.id === id)) return;
261
+ stop(id);
262
+ emit(items.filter((item) => item.id !== id));
263
+ }
264
+ function show(options) {
265
+ nextId += 1;
266
+ const item = {
267
+ id: `toast-${nextId}`,
268
+ tone: options.tone ?? "info",
269
+ title: options.title,
270
+ description: options.description,
271
+ duration: options.duration ?? 5e3,
272
+ action: options.action,
273
+ closeLabel: options.closeLabel
274
+ };
275
+ const next = [...items, item];
276
+ for (const old of next.splice(0, Math.max(0, next.length - 3))) stop(old.id);
277
+ if (item.duration > 0) start(item.id, item.duration);
278
+ emit(next);
279
+ return item.id;
280
+ }
281
+ function pause() {
282
+ if (paused) return;
283
+ paused = true;
284
+ const now = Date.now();
285
+ for (const timer of timers.values()) {
286
+ if (timer.handle !== void 0) clearTimeout(timer.handle);
287
+ timer.handle = void 0;
288
+ timer.remaining = Math.max(0, timer.remaining - (now - timer.startedAt));
289
+ }
290
+ }
291
+ function resume() {
292
+ if (!paused) return;
293
+ paused = false;
294
+ for (const [id, timer] of timers) start(id, timer.remaining);
295
+ }
296
+ return {
297
+ subscribe(listener) {
298
+ listeners.add(listener);
299
+ return () => {
300
+ listeners.delete(listener);
301
+ };
302
+ },
303
+ getSnapshot: () => items,
304
+ show,
305
+ dismiss,
306
+ pause,
307
+ resume
308
+ };
309
+ }
310
+ //#endregion
81
311
  //#region src/types/badge.ts
82
312
  /**
83
313
  * Variantes de `Badge` (ADR-009): una por cada color de `color.feedback`. Las historias
@@ -120,23 +350,120 @@ const buttonIconSize = {
120
350
  lg: "md"
121
351
  };
122
352
  //#endregion
353
+ //#region src/types/card.ts
354
+ /**
355
+ * Variantes de `Card` (ADR-009): `outlined` se apoya en el borde; `elevated` añade
356
+ * sombra para separarse de lo que tiene debajo.
357
+ */
358
+ const cardVariants = ["outlined", "elevated"];
359
+ //#endregion
360
+ //#region src/types/control.ts
361
+ /**
362
+ * Alturas de los controles interactivos de una línea (`Input`, `Select`). Coinciden
363
+ * con `size.control` de los tokens y con los tamaños de `Button`, para que un campo y
364
+ * un botón del mismo tamaño queden alineados en una fila.
365
+ */
366
+ const controlSizes = [
367
+ "sm",
368
+ "md",
369
+ "lg"
370
+ ];
371
+ //#endregion
372
+ //#region src/types/divider.ts
373
+ /** Orientación de `Divider` (ADR-009). */
374
+ const dividerOrientations = ["horizontal", "vertical"];
375
+ //#endregion
376
+ //#region src/types/feedback.ts
377
+ /**
378
+ * Los cuatro significados de `color.feedback` en los tokens: éxito, aviso, error e
379
+ * información. Los comparten los componentes que comunican un estado.
380
+ */
381
+ const feedbackTones = [
382
+ "success",
383
+ "warning",
384
+ "danger",
385
+ "info"
386
+ ];
387
+ function isFeedbackTone(value) {
388
+ return feedbackTones.includes(value);
389
+ }
390
+ //#endregion
123
391
  //#region src/types/icon.ts
124
392
  const iconSizes = [
125
393
  "sm",
126
394
  "md",
127
- "lg"
395
+ "lg",
396
+ "xl"
128
397
  ];
129
398
  /** Tamaño en px/puntos de cada icono. */
130
399
  const iconSizePx = {
131
400
  sm: 16,
132
401
  md: 20,
133
- lg: 24
402
+ lg: 24,
403
+ xl: 32
404
+ };
405
+ //#endregion
406
+ //#region src/types/icon-button.ts
407
+ /**
408
+ * Tamaño del icono para cada tamaño de `IconButton`. Es un paso mayor que en
409
+ * `Button`, porque aquí el icono es todo el contenido.
410
+ */
411
+ const iconButtonIconSize = {
412
+ sm: "sm",
413
+ md: "md",
414
+ lg: "lg"
134
415
  };
135
416
  //#endregion
417
+ //#region src/types/input.ts
418
+ /**
419
+ * Qué se escribe en un `Input` (ADR-009). Cada vista lo traduce a su plataforma:
420
+ * el atributo `type` y el teclado en web, y el teclado, las mayúsculas automáticas
421
+ * y el autocompletado en nativo.
422
+ */
423
+ const inputTypes = [
424
+ "text",
425
+ "email",
426
+ "password",
427
+ "search",
428
+ "tel",
429
+ "url",
430
+ "number"
431
+ ];
432
+ /** Tamaño de los iconos de un `Input` para cada tamaño de campo. */
433
+ const inputIconSize = {
434
+ sm: "sm",
435
+ md: "md",
436
+ lg: "md"
437
+ };
438
+ //#endregion
439
+ //#region src/types/link.ts
440
+ /**
441
+ * Cuándo se subraya un `Link` (ADR-009). `always` es lo correcto dentro de un párrafo:
442
+ * el color solo no basta para distinguir un enlace. `hover` lo subraya al pasar el
443
+ * puntero o enfocarlo en web, y mientras se pulsa en nativo.
444
+ */
445
+ const linkUnderlines = ["always", "hover"];
446
+ //#endregion
447
+ //#region src/types/modal.ts
448
+ /** Las dos presentaciones de un diálogo. */
449
+ const modalPresentations = ["dialog", "sheet"];
450
+ //#endregion
451
+ //#region src/types/skeleton.ts
452
+ /**
453
+ * Formas de `Skeleton` (ADR-009): `text` ocupa el hueco de una línea de `Text`,
454
+ * `rectangle` el de una imagen o un control, y `circle` el de un avatar.
455
+ */
456
+ const skeletonShapes = [
457
+ "text",
458
+ "rectangle",
459
+ "circle"
460
+ ];
461
+ //#endregion
136
462
  //#region src/types/spinner.ts
137
463
  /**
138
464
  * `Spinner` comparte la escala de `Icon`: así puede ocupar el sitio de un icono
139
465
  * (por ejemplo, dentro de un botón que carga) sin mover el resto del contenido.
466
+ * `xl` es para la carga de una página o de una sección entera.
140
467
  */
141
468
  const spinnerSizes = iconSizes;
142
469
  /** Tamaño en px/puntos de cada spinner. */
@@ -246,4 +573,4 @@ const themeModes = [
246
573
  "system"
247
574
  ];
248
575
  //#endregion
249
- export { UIContext, backgroundTokens, badgeVariants, borderColorTokens, buttonIconSize, buttonVariants, createUIContextValue, iconSizePx, iconSizes, mergeTheme, radiusTokens, resolveColorScheme, resolveTheme, shadowTokens, spaceTokens, spinnerSizePx, spinnerSizes, stackAligns, stackDirections, stackJustifies, textAligns, textColors, textVariantStyles, textVariants, themeModes, useBrand, useButton, useColorScheme, useTheme, useUIContext };
576
+ export { FormFieldContext, TOAST_DEFAULT_DURATION, TOAST_MAX_VISIBLE, ToastContext, UIContext, backgroundTokens, badgeVariants, borderColorTokens, buttonIconSize, buttonVariants, cardVariants, controlSizes, createFormFieldValue, createToastStore, createUIContextValue, dividerOrientations, feedbackTones, firstEnabledTab, getTabInDirection, iconButtonIconSize, iconSizePx, iconSizes, inputIconSize, inputTypes, isFeedbackTone, linkUnderlines, mergeTheme, modalPresentations, radiusTokens, resolveColorScheme, resolveFormFieldControl, resolveTheme, shadowTokens, skeletonShapes, spaceTokens, spinnerSizePx, spinnerSizes, stackAligns, stackDirections, stackJustifies, tabDirections, textAligns, textColors, textVariantStyles, textVariants, themeModes, useBrand, useButton, useColorScheme, useControllableState, useFormFieldControl, useLink, useTheme, useToast, useUIContext };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@satellatickets/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Lógica headless de satella-ui: contratos de props, constantes de variantes, contexto de tema y hooks compartidos por React y React Native",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,8 +40,8 @@
40
40
  "tsdown": "^0.23.0",
41
41
  "typescript": "^5.9.3",
42
42
  "vitest": "^5.0.3",
43
- "@satellatickets/eslint-config": "0.0.0",
44
- "@satellatickets/tsconfig": "0.0.0"
43
+ "@satellatickets/tsconfig": "0.0.0",
44
+ "@satellatickets/eslint-config": "0.0.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsdown",