@mimi-ng/cli 0.1.1 → 0.3.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.
Files changed (104) hide show
  1. package/README.md +28 -1
  2. package/dist/init/css.js +17 -0
  3. package/dist/init/index.js +7 -5
  4. package/dist/init/items.js +9 -0
  5. package/dist/registry.json +482 -1
  6. package/dist/templates/components/avatar/avatar.ts +1 -1
  7. package/dist/templates/components/avatar/avatar.variants.ts +4 -4
  8. package/dist/templates/components/badge/badge.ts +41 -6
  9. package/dist/templates/components/badge/badge.variants.ts +105 -11
  10. package/dist/templates/components/button/button.ts +74 -7
  11. package/dist/templates/components/button/button.variants.ts +145 -21
  12. package/dist/templates/components/calendar/calendar-texts.ts +72 -0
  13. package/dist/templates/components/calendar/calendar.ts +588 -0
  14. package/dist/templates/components/calendar/dates.ts +181 -0
  15. package/dist/templates/components/calendar/index.ts +3 -0
  16. package/dist/templates/components/card/card.ts +8 -5
  17. package/dist/templates/components/checkbox/checkbox.ts +3 -3
  18. package/dist/templates/components/chip/chip-texts.ts +40 -0
  19. package/dist/templates/components/chip/chip.ts +116 -0
  20. package/dist/templates/components/chip/index.ts +2 -0
  21. package/dist/templates/components/color-picker/color-picker-texts.ts +57 -0
  22. package/dist/templates/components/color-picker/color-picker.ts +718 -0
  23. package/dist/templates/components/color-picker/color.ts +157 -0
  24. package/dist/templates/components/color-picker/index.ts +3 -0
  25. package/dist/templates/components/combobox/combobox-texts.ts +70 -0
  26. package/dist/templates/components/combobox/combobox.ts +798 -0
  27. package/dist/templates/components/combobox/index.ts +2 -0
  28. package/dist/templates/components/confirm/confirm-texts.ts +46 -0
  29. package/dist/templates/components/confirm/confirm.ts +190 -0
  30. package/dist/templates/components/confirm/index.ts +2 -0
  31. package/dist/templates/components/context-menu/context-menu.ts +378 -0
  32. package/dist/templates/components/context-menu/index.ts +14 -0
  33. package/dist/templates/components/date-picker/date-picker.ts +240 -0
  34. package/dist/templates/components/date-picker/index.ts +3 -0
  35. package/dist/templates/components/date-range/date-range.ts +308 -0
  36. package/dist/templates/components/date-range/index.ts +1 -0
  37. package/dist/templates/components/dialog/dialog-texts.ts +41 -0
  38. package/dist/templates/components/dialog/dialog.ts +701 -0
  39. package/dist/templates/components/dialog/index.ts +18 -0
  40. package/dist/templates/components/dropdown-menu/dropdown-menu.ts +252 -0
  41. package/dist/templates/components/dropdown-menu/index.ts +14 -0
  42. package/dist/templates/components/file-upload/file-upload-texts.ts +77 -0
  43. package/dist/templates/components/file-upload/file-upload.ts +698 -0
  44. package/dist/templates/components/file-upload/index.ts +2 -0
  45. package/dist/templates/components/form-field/form-error.ts +1 -1
  46. package/dist/templates/components/form-field/form-field.ts +3 -2
  47. package/dist/templates/components/input/input.variants.ts +6 -4
  48. package/dist/templates/components/input-mask/index.ts +1 -0
  49. package/dist/templates/components/input-mask/input-mask.ts +319 -0
  50. package/dist/templates/components/input-otp/index.ts +1 -0
  51. package/dist/templates/components/input-otp/input-otp.ts +212 -0
  52. package/dist/templates/components/menu/index.ts +15 -0
  53. package/dist/templates/components/menu/menu.ts +656 -0
  54. package/dist/templates/components/number-input/index.ts +2 -0
  55. package/dist/templates/components/number-input/number-input-texts.ts +46 -0
  56. package/dist/templates/components/number-input/number-input.ts +450 -0
  57. package/dist/templates/components/option-list/index.ts +11 -0
  58. package/dist/templates/components/option-list/option-list.ts +518 -0
  59. package/dist/templates/components/pagination/index.ts +2 -0
  60. package/dist/templates/components/pagination/pagination-page-size.ts +74 -0
  61. package/dist/templates/components/pagination/pagination-texts.ts +69 -0
  62. package/dist/templates/components/pagination/pagination.ts +443 -0
  63. package/dist/templates/components/password/index.ts +2 -0
  64. package/dist/templates/components/password/password-texts.ts +68 -0
  65. package/dist/templates/components/password/password.ts +363 -0
  66. package/dist/templates/components/popover/index.ts +12 -0
  67. package/dist/templates/components/popover/popover.ts +407 -0
  68. package/dist/templates/components/progress/index.ts +1 -0
  69. package/dist/templates/components/progress/progress.ts +217 -0
  70. package/dist/templates/components/select/index.ts +12 -0
  71. package/dist/templates/components/select/select-texts.ts +62 -0
  72. package/dist/templates/components/select/select.ts +1086 -0
  73. package/dist/templates/components/separator/separator.variants.ts +10 -7
  74. package/dist/templates/components/skeleton/skeleton.ts +4 -1
  75. package/dist/templates/components/slider/index.ts +2 -0
  76. package/dist/templates/components/slider/slider-texts.ts +26 -0
  77. package/dist/templates/components/slider/slider.ts +425 -0
  78. package/dist/templates/components/switch/switch.ts +6 -6
  79. package/dist/templates/components/tabs/index.ts +2 -0
  80. package/dist/templates/components/tabs/tabs.ts +426 -0
  81. package/dist/templates/components/tags-input/index.ts +2 -0
  82. package/dist/templates/components/tags-input/tags-input-texts.ts +44 -0
  83. package/dist/templates/components/tags-input/tags-input.ts +303 -0
  84. package/dist/templates/components/textarea/textarea.variants.ts +14 -11
  85. package/dist/templates/components/toast/index.ts +3 -0
  86. package/dist/templates/components/toast/toast-config.ts +45 -0
  87. package/dist/templates/components/toast/toast-texts.ts +46 -0
  88. package/dist/templates/components/toast/toast.ts +1000 -0
  89. package/dist/templates/components/toggle-group/index.ts +1 -0
  90. package/dist/templates/components/toggle-group/toggle-group.ts +406 -0
  91. package/dist/templates/components/toolbar/index.ts +1 -0
  92. package/dist/templates/components/toolbar/toolbar.ts +377 -0
  93. package/dist/templates/components/tooltip/index.ts +7 -0
  94. package/dist/templates/components/tooltip/tooltip.ts +407 -0
  95. package/dist/templates/theme/provider.ts +75 -6
  96. package/dist/templates/theme/theme-base.css +410 -9
  97. package/dist/templates/theme/types.ts +494 -4
  98. package/dist/templates/utils/cn.ts +15 -1
  99. package/dist/templates/utils/control-styles.ts +2 -2
  100. package/dist/templates/utils/overlay.ts +153 -0
  101. package/dist/ui/index.js +184 -8
  102. package/dist/ui/schema.json +100 -0
  103. package/dist/utils/write-files.js +166 -59
  104. package/package.json +1 -1
@@ -1,21 +1,56 @@
1
- import { Directive, computed, input } from '@angular/core';
1
+ import { Directive, computed, input, isDevMode } from '@angular/core';
2
2
  import { cn } from '@/components/ui/utils/cn';
3
- import { type BadgeVariant, badgeVariants } from './badge.variants';
3
+ import {
4
+ type BadgeTone,
5
+ type BadgeVariant,
6
+ badgeVariants,
7
+ resolveBadgeStyle,
8
+ } from './badge.variants';
4
9
 
5
- /** Etiqueta estática de Mimi sobre el `<span>` nativo: `<span mimiBadge>Nuevo</span>`. */
10
+ /** El aviso de atajo + tone sale una sola vez por página. */
11
+ let warnedShortcutWithTone = false;
12
+
13
+ /**
14
+ * Etiqueta estática de Mimi sobre el `<span>` nativo: `<span mimiBadge>Nuevo</span>`.
15
+ *
16
+ * - `variant` (solid, soft, outline) y `tone` (primary, secondary, success, warning, info,
17
+ * danger). Sin `tone`, cada variante usa su tono natural: outline es neutro (secondary); las
18
+ * demás, primary. `data-variant` y `data-tone` muestran los valores resueltos.
19
+ * - Los atajos de la 0.1.0 (`default`, `secondary`, `destructive`) siguen funcionando y ganan
20
+ * sobre `tone` (en modo desarrollo, avisa una vez).
21
+ */
6
22
  @Directive({
7
23
  selector: 'span[mimiBadge]',
8
24
  exportAs: 'mimiBadge',
9
25
  host: {
10
26
  '[class]': 'classes()',
11
- '[attr.data-variant]': 'variant()',
27
+ '[attr.data-variant]': 'resolved().variant',
28
+ '[attr.data-tone]': 'resolved().tone',
12
29
  },
13
30
  })
14
31
  export class MimiBadge {
15
- readonly variant = input<BadgeVariant>('default');
32
+ /** Incluye los atajos de la 0.1.x (`BadgeVariantShortcut`, deprecados). */
33
+ readonly variant = input<BadgeVariant>('solid');
34
+ readonly tone = input<BadgeTone>();
16
35
  readonly userClass = input('', { alias: 'class' });
17
36
 
37
+ protected readonly resolved = computed(() => {
38
+ const style = resolveBadgeStyle(this.variant(), this.tone());
39
+ if (style.ignoredTone && isDevMode() && !warnedShortcutWithTone) {
40
+ warnedShortcutWithTone = true;
41
+ console.warn(
42
+ `[Mimi] Badge: variant="${this.variant()}" ya define el color; se ignora ` +
43
+ `tone="${this.tone()}". Usa variant="${style.variant}" con tone (los atajos se ` +
44
+ `quitan en la 1.0).`,
45
+ );
46
+ }
47
+ return style;
48
+ });
49
+
18
50
  protected readonly classes = computed(() =>
19
- cn(badgeVariants({ variant: this.variant() }), this.userClass()),
51
+ cn(
52
+ badgeVariants({ variant: this.resolved().variant, tone: this.resolved().tone }),
53
+ this.userClass(),
54
+ ),
20
55
  );
21
56
  }
@@ -1,4 +1,68 @@
1
- import { cva, type VariantProps } from 'class-variance-authority';
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ /**
4
+ * Cómo se ve el badge (spec 4, «Variante y tono»), sin los atajos de la 0.1.x. Sin ghost ni link:
5
+ * el diseño no los tiene.
6
+ */
7
+ export type BadgeStyleVariant = 'solid' | 'soft' | 'outline';
8
+
9
+ /** De qué color es. `danger` usa los tokens `destructive`. */
10
+ export type BadgeTone = 'primary' | 'secondary' | 'success' | 'warning' | 'info' | 'danger';
11
+
12
+ /**
13
+ * Atajos de la 0.1.0, cuando el color era una variante. Siguen funcionando, sin error ni aviso:
14
+ * `default` = solid + primary, `secondary` = solid + secondary, `destructive` = solid + danger.
15
+ * @deprecated Se quitan en la 1.0 (spec 12). Usa `variant` y `tone`.
16
+ */
17
+ export type BadgeVariantShortcut = 'default' | 'secondary' | 'destructive';
18
+
19
+ /**
20
+ * Lo que acepta `variant`: las variantes y, hasta la 1.0, los atajos de la 0.1.x (deprecados), para
21
+ * que el código que tipaba `BadgeVariant` con `'default'` o `'destructive'` siga compilando.
22
+ */
23
+ export type BadgeVariant = BadgeStyleVariant | BadgeVariantShortcut;
24
+
25
+ /** Punto decorativo de outline (un ::before vacío: no lo leen los lectores de pantalla). */
26
+ const DOT = "gap-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:content-['']";
27
+
28
+ /**
29
+ * Colores de cada tono en cada variante. Clases completas y literales para Tailwind. Diseño:
30
+ * docs/design/«Mimi F2 Tokens y Tonos» (Tonos en Badge). Outline lleva un punto del color del
31
+ * tono; sin tono (secondary) se ve como en la 0.1.0, sin punto. El tono secondary es
32
+ * provisional, sin diseño (spec 13).
33
+ */
34
+ const TONE_CLASSES = {
35
+ primary: {
36
+ solid: 'border-transparent bg-primary text-primary-foreground',
37
+ soft: 'border-transparent bg-secondary text-secondary-foreground',
38
+ outline: `border-border bg-transparent text-foreground ${DOT} before:bg-primary`,
39
+ },
40
+ secondary: {
41
+ solid: 'border-transparent bg-secondary text-secondary-foreground',
42
+ soft: 'border-transparent bg-muted text-foreground',
43
+ outline: 'border-border bg-transparent text-foreground',
44
+ },
45
+ success: {
46
+ solid: 'border-transparent bg-success text-success-foreground',
47
+ soft: 'border-transparent bg-success-soft text-success-soft-foreground',
48
+ outline: `border-border bg-transparent text-foreground ${DOT} before:bg-success`,
49
+ },
50
+ warning: {
51
+ solid: 'border-transparent bg-warning text-warning-foreground',
52
+ soft: 'border-transparent bg-warning-soft text-warning-soft-foreground',
53
+ outline: `border-border bg-transparent text-foreground ${DOT} before:bg-warning`,
54
+ },
55
+ info: {
56
+ solid: 'border-transparent bg-info text-info-foreground',
57
+ soft: 'border-transparent bg-info-soft text-info-soft-foreground',
58
+ outline: `border-border bg-transparent text-foreground ${DOT} before:bg-info`,
59
+ },
60
+ danger: {
61
+ solid: 'border-transparent bg-destructive text-destructive-foreground',
62
+ soft: 'border-transparent bg-destructive-soft-bg text-destructive-soft-foreground',
63
+ outline: `border-border bg-transparent text-foreground ${DOT} before:bg-destructive`,
64
+ },
65
+ } as const satisfies Record<BadgeTone, Record<BadgeStyleVariant, string>>;
2
66
 
3
67
  /**
4
68
  * Clases de mimiBadge (docs/design/Mimi Componentes.dc.html, sección 5). Literales para que
@@ -7,21 +71,51 @@ import { cva, type VariantProps } from 'class-variance-authority';
7
71
  */
8
72
  export const badgeVariants = cva(
9
73
  [
10
- 'inline-flex h-[22px] shrink-0 items-center gap-1 rounded-badge border px-2.5',
11
- 'font-sans text-xs leading-none font-semibold whitespace-nowrap',
74
+ 'inline-flex h-[var(--mimi-badge-height,22px)] shrink-0 items-center gap-1 rounded-badge border px-[var(--mimi-badge-px,0.625rem)]',
75
+ 'font-sans text-[length:var(--mimi-badge-font-size,0.75rem)] leading-none font-[weight:var(--mimi-badge-font-weight,600)] whitespace-nowrap',
12
76
  '[&_svg]:pointer-events-none [&_svg]:size-3 [&_svg]:shrink-0',
13
77
  ],
14
78
  {
15
79
  variants: {
16
- variant: {
17
- default: 'border-transparent bg-primary text-primary-foreground',
18
- secondary: 'border-transparent bg-secondary text-secondary-foreground',
19
- outline: 'border-border bg-transparent text-foreground',
20
- destructive: 'border-transparent bg-destructive text-destructive-foreground',
21
- },
80
+ variant: { solid: '', soft: '', outline: '' },
81
+ tone: { primary: '', secondary: '', success: '', warning: '', info: '', danger: '' },
22
82
  },
23
- defaultVariants: { variant: 'default' },
83
+ compoundVariants: Object.entries(TONE_CLASSES).flatMap(([tone, byVariant]) =>
84
+ Object.entries(byVariant).map(([variant, cls]) => ({
85
+ tone: tone as BadgeTone,
86
+ variant: variant as BadgeStyleVariant,
87
+ class: cls,
88
+ })),
89
+ ),
90
+ defaultVariants: { variant: 'solid', tone: 'primary' },
24
91
  },
25
92
  );
26
93
 
27
- export type BadgeVariant = NonNullable<VariantProps<typeof badgeVariants>['variant']>;
94
+ /** Sin `tone`, cada variante usa su tono natural: outline es neutro, como en la 0.1.0. */
95
+ const NATURAL_TONE: Record<BadgeStyleVariant, BadgeTone> = {
96
+ solid: 'primary',
97
+ soft: 'primary',
98
+ outline: 'secondary',
99
+ };
100
+
101
+ const SHORTCUTS: Record<BadgeVariantShortcut, { variant: BadgeStyleVariant; tone: BadgeTone }> = {
102
+ default: { variant: 'solid', tone: 'primary' },
103
+ secondary: { variant: 'solid', tone: 'secondary' },
104
+ destructive: { variant: 'solid', tone: 'danger' },
105
+ };
106
+
107
+ /**
108
+ * Resuelve variante y tono. Un atajo de la 0.1.0 gana siempre; `ignoredTone` indica que además
109
+ * se pasó un `tone` que no se aplicó.
110
+ */
111
+ export function resolveBadgeStyle(
112
+ variant: BadgeVariant,
113
+ tone: BadgeTone | undefined,
114
+ ): { variant: BadgeStyleVariant; tone: BadgeTone; ignoredTone: boolean } {
115
+ if (variant in SHORTCUTS) {
116
+ const shortcut = SHORTCUTS[variant as BadgeVariantShortcut];
117
+ return { ...shortcut, ignoredTone: tone !== undefined && tone !== shortcut.tone };
118
+ }
119
+ const v = variant as BadgeStyleVariant;
120
+ return { variant: v, tone: tone ?? NATURAL_TONE[v], ignoredTone: false };
121
+ }
@@ -3,17 +3,35 @@ import {
3
3
  Component,
4
4
  DestroyRef,
5
5
  ElementRef,
6
+ Renderer2,
6
7
  booleanAttribute,
7
8
  computed,
9
+ effect,
8
10
  inject,
9
11
  input,
12
+ isDevMode,
10
13
  } from '@angular/core';
11
14
  import { cn } from '@/components/ui/utils/cn';
12
- import { type ButtonSize, type ButtonVariant, buttonVariants } from './button.variants';
15
+ import {
16
+ type ButtonSize,
17
+ type ButtonTone,
18
+ type ButtonVariant,
19
+ buttonVariants,
20
+ resolveButtonStyle,
21
+ } from './button.variants';
22
+
23
+ /** El aviso de atajo + tone sale una sola vez por página. */
24
+ let warnedShortcutWithTone = false;
13
25
 
14
26
  /**
15
27
  * Botón de Mimi sobre el elemento nativo: `<button mimiBtn>` o `<a mimiBtn>`.
16
28
  *
29
+ * - `variant` (cómo se ve: solid, soft, outline, ghost, link) y `tone` (de qué color: primary,
30
+ * secondary, success, warning, info, danger). Sin `tone`, cada variante usa su tono natural:
31
+ * outline y ghost son neutros (secondary); las demás, primary. `data-variant` y `data-tone`
32
+ * muestran los valores resueltos.
33
+ * - Los atajos de la 0.1.0 (`default`, `secondary`, `destructive`) siguen funcionando y ganan
34
+ * sobre `tone` (en modo desarrollo, avisa una vez).
17
35
  * - `disabled` o `loading` deshabilitan: `disabled` nativo en <button>; en <a>,
18
36
  * `aria-disabled="true"`, `tabindex="-1"` y el clic se cancela.
19
37
  * - `loading` muestra un spinner centrado encima del contenido, que queda transparente en su
@@ -48,19 +66,21 @@ import { type ButtonSize, type ButtonVariant, buttonVariants } from './button.va
48
66
  `,
49
67
  host: {
50
68
  '[class]': 'classes()',
51
- '[attr.data-variant]': 'variant()',
69
+ '[attr.data-variant]': 'resolved().variant',
70
+ '[attr.data-tone]': 'resolved().tone',
52
71
  '[attr.data-size]': 'size()',
53
72
  '[attr.data-loading]': 'loading() ? "" : null',
54
73
  '[attr.data-disabled]': 'isDisabled() ? "" : null',
55
74
  '[attr.disabled]': 'isNativeButton && isDisabled() ? "" : null',
56
- '[attr.aria-disabled]': '!isNativeButton && isDisabled() ? "true" : null',
57
- '[attr.tabindex]': '!isNativeButton && isDisabled() ? "-1" : null',
58
- '[attr.aria-busy]': 'loading() ? "true" : null',
75
+ // aria-disabled, tabindex y aria-busy no van aquí: un binding de host que vale null borra el
76
+ // atributo que haya puesto el usuario. Los pone `ownAttributes` solo cuando hacen falta.
59
77
  },
60
78
  changeDetection: ChangeDetectionStrategy.OnPush,
61
79
  })
62
80
  export class MimiButton {
63
- readonly variant = input<ButtonVariant>('default');
81
+ /** Incluye los atajos de la 0.1.x (`ButtonVariantShortcut`, deprecados). */
82
+ readonly variant = input<ButtonVariant>('solid');
83
+ readonly tone = input<ButtonTone>();
64
84
  readonly size = input<ButtonSize>('default');
65
85
  readonly disabled = input(false, { transform: booleanAttribute });
66
86
  readonly loading = input(false, { transform: booleanAttribute });
@@ -71,15 +91,62 @@ export class MimiButton {
71
91
 
72
92
  protected readonly isDisabled = computed(() => this.disabled() || this.loading());
73
93
 
94
+ protected readonly resolved = computed(() => {
95
+ const style = resolveButtonStyle(this.variant(), this.tone());
96
+ if (style.ignoredTone && isDevMode() && !warnedShortcutWithTone) {
97
+ warnedShortcutWithTone = true;
98
+ console.warn(
99
+ `[Mimi] Button: variant="${this.variant()}" ya define el color; se ignora ` +
100
+ `tone="${this.tone()}". Usa variant="${style.variant}" con tone (los atajos se ` +
101
+ `quitan en la 1.0).`,
102
+ );
103
+ }
104
+ return style;
105
+ });
106
+
74
107
  protected readonly classes = computed(() =>
75
108
  cn(
76
- buttonVariants({ variant: this.variant(), size: this.size() }),
109
+ buttonVariants({
110
+ variant: this.resolved().variant,
111
+ tone: this.resolved().tone,
112
+ size: this.size(),
113
+ }),
77
114
  // El diseño deshabilitado no tiene sombra; cargando la conserva.
78
115
  this.disabled() && !this.loading() && 'shadow-none',
79
116
  this.userClass(),
80
117
  ),
81
118
  );
82
119
 
120
+ /**
121
+ * `aria-disabled="true"` y `tabindex="-1"` en un <a> deshabilitado, y `aria-busy="true"` mientras
122
+ * carga. Solo se escriben cuando hacen falta; al dejar de hacer falta vuelve el valor que tenía el
123
+ * elemento (el del usuario, fijo o enlazado) o se quitan. Así `<button mimiBtn aria-disabled="true">`
124
+ * (un botón deshabilitado que sigue enfocable) conserva su atributo.
125
+ */
126
+ private readonly ownAttributes = (() => {
127
+ const renderer = inject(Renderer2);
128
+ /** Lo que había antes de que Mimi escribiera cada atributo (null: no había). */
129
+ const previous = new Map<string, string | null>();
130
+ const apply = (name: string, value: string | null) => {
131
+ if (value !== null) {
132
+ if (!previous.has(name)) previous.set(name, this.element.getAttribute(name));
133
+ renderer.setAttribute(this.element, name, value);
134
+ return;
135
+ }
136
+ if (!previous.has(name)) return;
137
+ const before = previous.get(name) ?? null;
138
+ previous.delete(name);
139
+ if (before === null) renderer.removeAttribute(this.element, name);
140
+ else renderer.setAttribute(this.element, name, before);
141
+ };
142
+ return effect(() => {
143
+ const disabledLink = !this.isNativeButton && this.isDisabled();
144
+ apply('aria-disabled', disabledLink ? 'true' : null);
145
+ apply('tabindex', disabledLink ? '-1' : null);
146
+ apply('aria-busy', this.loading() ? 'true' : null);
147
+ });
148
+ })();
149
+
83
150
  /**
84
151
  * En <a> deshabilitado cancela el clic (en <button> el navegador ya lo bloquea). Se escucha
85
152
  * en la fase de captura: en el propio elemento corre antes que los (click) del usuario, que
@@ -1,14 +1,96 @@
1
1
  import { cva, type VariantProps } from 'class-variance-authority';
2
- import {
3
- buttonFocusStyles,
4
- controlDisabledStyles,
5
- controlPressStyles,
6
- } from '@/components/ui/utils/control-styles';
2
+ import { buttonFocusStyles, controlDisabledStyles } from '@/components/ui/utils/control-styles';
3
+
4
+ /** Cómo se ve el botón (spec 4, «Variante y tono»), sin los atajos de la 0.1.x. */
5
+ export type ButtonStyleVariant = 'solid' | 'soft' | 'outline' | 'ghost' | 'link';
6
+
7
+ /** De qué color es. `danger` usa los tokens `destructive`. */
8
+ export type ButtonTone = 'primary' | 'secondary' | 'success' | 'warning' | 'info' | 'danger';
9
+
10
+ /**
11
+ * Atajos de la 0.1.0, cuando el color era una variante. Siguen funcionando, sin error ni aviso:
12
+ * `default` = solid + primary, `secondary` = solid + secondary, `destructive` = solid + danger.
13
+ * @deprecated Se quitan en la 1.0 (spec 12). Usa `variant` y `tone`.
14
+ */
15
+ export type ButtonVariantShortcut = 'default' | 'secondary' | 'destructive';
16
+
17
+ /**
18
+ * Lo que acepta `variant`: las variantes y, hasta la 1.0, los atajos de la 0.1.x (deprecados), para
19
+ * que el código que tipaba `ButtonVariant` con `'default'` o `'destructive'` siga compilando.
20
+ */
21
+ export type ButtonVariant = ButtonStyleVariant | ButtonVariantShortcut;
22
+
23
+ /**
24
+ * Colores de cada tono en cada variante: una fila por tono, una entrada por variante. Todas las
25
+ * clases van completas y literales para que Tailwind las detecte. Diseño: docs/design/
26
+ * «Mimi F2 Tokens y Tonos» (solid, soft y outline con tono). Las celdas de secondary, ghost y
27
+ * link con tono son provisionales, sin diseño (spec 13).
28
+ */
29
+ const TONE_CLASSES = {
30
+ primary: {
31
+ solid:
32
+ 'bg-primary text-primary-foreground shadow-primary hover:not-disabled:bg-primary-hover hover:not-disabled:shadow-primary-hover active:ring-3 active:ring-ring-soft',
33
+ soft: 'bg-secondary text-secondary-foreground hover:not-disabled:bg-primary-soft-hover',
34
+ outline:
35
+ 'border-primary-border bg-transparent text-secondary-foreground hover:not-disabled:bg-primary-subtle',
36
+ ghost:
37
+ 'bg-transparent text-foreground hover:not-disabled:bg-accent hover:not-disabled:text-accent-foreground',
38
+ link: 'bg-transparent text-primary',
39
+ },
40
+ secondary: {
41
+ solid:
42
+ 'bg-secondary text-secondary-foreground shadow-neutral hover:not-disabled:bg-secondary-hover hover:not-disabled:shadow-neutral-hover active:ring-3 active:ring-ring-soft',
43
+ soft: 'bg-muted text-foreground hover:not-disabled:bg-accent hover:not-disabled:text-accent-foreground',
44
+ outline:
45
+ 'border-border bg-card text-foreground shadow-neutral hover:not-disabled:bg-accent hover:not-disabled:text-accent-foreground hover:not-disabled:shadow-neutral-hover active:ring-3 active:ring-ring-soft',
46
+ ghost:
47
+ 'bg-transparent text-foreground hover:not-disabled:bg-accent hover:not-disabled:text-accent-foreground',
48
+ link: 'bg-transparent text-foreground',
49
+ },
50
+ success: {
51
+ solid:
52
+ 'bg-success text-success-foreground shadow-success hover:not-disabled:bg-success-hover hover:not-disabled:shadow-success-hover active:ring-3 active:ring-success-ring',
53
+ soft: 'bg-success-soft text-success-soft-foreground hover:not-disabled:bg-success-soft-hover',
54
+ outline:
55
+ 'border-success-border bg-transparent text-success-soft-foreground hover:not-disabled:bg-success-subtle',
56
+ ghost: 'bg-transparent text-success-soft-foreground hover:not-disabled:bg-success-soft',
57
+ link: 'bg-transparent text-success-soft-foreground',
58
+ },
59
+ warning: {
60
+ solid:
61
+ 'bg-warning text-warning-foreground shadow-warning hover:not-disabled:bg-warning-hover hover:not-disabled:shadow-warning-hover active:ring-3 active:ring-warning-ring',
62
+ soft: 'bg-warning-soft text-warning-soft-foreground hover:not-disabled:bg-warning-soft-hover',
63
+ outline:
64
+ 'border-warning-border bg-transparent text-warning-soft-foreground hover:not-disabled:bg-warning-subtle',
65
+ ghost: 'bg-transparent text-warning-soft-foreground hover:not-disabled:bg-warning-soft',
66
+ link: 'bg-transparent text-warning-soft-foreground',
67
+ },
68
+ info: {
69
+ solid:
70
+ 'bg-info text-info-foreground shadow-info hover:not-disabled:bg-info-hover hover:not-disabled:shadow-info-hover active:ring-3 active:ring-info-ring',
71
+ soft: 'bg-info-soft text-info-soft-foreground hover:not-disabled:bg-info-soft-hover',
72
+ outline:
73
+ 'border-info-border bg-transparent text-info-soft-foreground hover:not-disabled:bg-info-subtle',
74
+ ghost: 'bg-transparent text-info-soft-foreground hover:not-disabled:bg-info-soft',
75
+ link: 'bg-transparent text-info-soft-foreground',
76
+ },
77
+ danger: {
78
+ solid:
79
+ 'bg-destructive text-destructive-foreground shadow-destructive hover:not-disabled:bg-destructive-hover hover:not-disabled:shadow-destructive-hover active:ring-3 active:ring-destructive-ring',
80
+ soft: 'bg-destructive-soft-bg text-destructive-soft-foreground hover:not-disabled:bg-destructive-soft-hover',
81
+ outline:
82
+ 'border-destructive-border bg-transparent text-destructive-soft-foreground hover:not-disabled:bg-destructive-subtle',
83
+ ghost:
84
+ 'bg-transparent text-destructive-soft-foreground hover:not-disabled:bg-destructive-soft-bg',
85
+ link: 'bg-transparent text-destructive-soft-foreground',
86
+ },
87
+ } as const satisfies Record<ButtonTone, Record<ButtonStyleVariant, string>>;
7
88
 
8
89
  /**
9
90
  * Clases de mimiBtn (docs/design/Mimi Componentes.dc.html, sección 1). Todas literales para
10
91
  * que Tailwind las detecte. Las medidas siguen la cascada de la spec 6.2:
11
- * --mimi-btn-* → --mimi-control-* / --mimi-radius → valor del diseño.
92
+ * --mimi-btn-* → --mimi-control-* / --mimi-radius → valor del diseño. La variante da la forma
93
+ * y el tono, los colores (TONE_CLASSES).
12
94
  */
13
95
  export const buttonVariants = cva(
14
96
  [
@@ -20,41 +102,83 @@ export const buttonVariants = cva(
20
102
  buttonFocusStyles,
21
103
  'focus-visible:outline-[length:var(--mimi-btn-focus-ring-width,2px)]',
22
104
  controlDisabledStyles,
23
- controlPressStyles,
105
+ // Escala al presionar: --mimi-btn-press-scale → --mimi-press-scale (spec 6.6).
106
+ 'mimi-transition active:scale-[var(--mimi-btn-press-scale,var(--mimi-press-scale))]',
24
107
  // Cargando: deshabilitado, pero con opacidad 85% y cursor de progreso (gana a disabled:).
25
108
  'aria-busy:disabled:cursor-progress aria-busy:disabled:opacity-85 aria-busy:aria-disabled:opacity-85',
26
109
  ],
27
110
  {
28
111
  variants: {
29
112
  variant: {
30
- default:
31
- 'bg-primary text-primary-foreground shadow-primary hover:not-disabled:bg-primary-hover hover:not-disabled:shadow-primary-hover active:ring-3 active:ring-ring-soft',
32
- secondary:
33
- 'bg-secondary text-secondary-foreground shadow-neutral hover:not-disabled:bg-secondary-hover hover:not-disabled:shadow-neutral-hover active:ring-3 active:ring-ring-soft',
34
- destructive:
35
- 'bg-destructive text-destructive-foreground shadow-destructive hover:not-disabled:bg-destructive-hover hover:not-disabled:shadow-destructive-hover active:ring-3 active:ring-destructive-soft',
36
- outline:
37
- 'border-border bg-card text-foreground shadow-neutral hover:not-disabled:bg-accent hover:not-disabled:text-accent-foreground hover:not-disabled:shadow-neutral-hover active:ring-3 active:ring-ring-soft',
38
- ghost:
39
- 'bg-transparent text-foreground hover:not-disabled:bg-accent hover:not-disabled:text-accent-foreground',
40
- link: 'bg-transparent text-primary underline-offset-4 hover:not-disabled:underline',
113
+ solid: '',
114
+ soft: '',
115
+ outline: '',
116
+ ghost: '',
117
+ link: 'underline-offset-4 hover:not-disabled:underline',
118
+ },
119
+ tone: {
120
+ primary: '',
121
+ secondary: '',
122
+ success: '',
123
+ warning: '',
124
+ info: '',
125
+ danger: '',
41
126
  },
42
127
  size: {
43
128
  default:
44
- 'h-[var(--mimi-btn-height,var(--mimi-control-height,2.5rem))] gap-2 px-[var(--mimi-btn-padding-x,1rem)] text-[length:var(--mimi-btn-font-size,0.875rem)]',
129
+ 'h-[var(--mimi-btn-height,var(--mimi-control-height,2.5rem))] gap-[var(--mimi-btn-gap,0.5rem)] px-[var(--mimi-btn-px,var(--mimi-btn-padding-x,1rem))] text-[length:var(--mimi-btn-font-size,0.875rem)]',
45
130
  sm: 'h-[var(--mimi-btn-height-sm,var(--mimi-control-height-sm,2rem))] gap-1.5 px-3 text-[13px]',
46
131
  lg: 'h-[var(--mimi-btn-height-lg,var(--mimi-control-height-lg,3rem))] gap-2 px-6 text-[15px]',
47
132
  icon: 'size-[var(--mimi-btn-height,var(--mimi-control-height,2.5rem))] p-0',
48
133
  },
49
134
  },
50
135
  compoundVariants: [
136
+ // Los colores: una combinación por cada tono y variante de TONE_CLASSES.
137
+ ...Object.entries(TONE_CLASSES).flatMap(([tone, byVariant]) =>
138
+ Object.entries(byVariant).map(([variant, cls]) => ({
139
+ tone: tone as ButtonTone,
140
+ variant: variant as ButtonStyleVariant,
141
+ class: cls,
142
+ })),
143
+ ),
51
144
  // El enlace del diseño solo tiene 4px de padding horizontal.
52
145
  { variant: 'link', size: ['default', 'sm', 'lg'], class: 'px-1' },
53
146
  ],
54
- defaultVariants: { variant: 'default', size: 'default' },
147
+ defaultVariants: { variant: 'solid', tone: 'primary', size: 'default' },
55
148
  },
56
149
  );
57
150
 
58
151
  type ButtonVariantProps = VariantProps<typeof buttonVariants>;
59
- export type ButtonVariant = NonNullable<ButtonVariantProps['variant']>;
60
152
  export type ButtonSize = NonNullable<ButtonVariantProps['size']>;
153
+
154
+ /** Sin `tone`, cada variante usa su tono natural: outline y ghost son neutros, como en la 0.1.0. */
155
+ const NATURAL_TONE: Record<ButtonStyleVariant, ButtonTone> = {
156
+ solid: 'primary',
157
+ soft: 'primary',
158
+ outline: 'secondary',
159
+ ghost: 'secondary',
160
+ link: 'primary',
161
+ };
162
+
163
+ const SHORTCUTS: Record<ButtonVariantShortcut, { variant: ButtonStyleVariant; tone: ButtonTone }> =
164
+ {
165
+ default: { variant: 'solid', tone: 'primary' },
166
+ secondary: { variant: 'solid', tone: 'secondary' },
167
+ destructive: { variant: 'solid', tone: 'danger' },
168
+ };
169
+
170
+ /**
171
+ * Resuelve variante y tono. Un atajo de la 0.1.0 gana siempre; `ignoredTone` indica que además
172
+ * se pasó un `tone` que no se aplicó.
173
+ */
174
+ export function resolveButtonStyle(
175
+ variant: ButtonVariant,
176
+ tone: ButtonTone | undefined,
177
+ ): { variant: ButtonStyleVariant; tone: ButtonTone; ignoredTone: boolean } {
178
+ if (variant in SHORTCUTS) {
179
+ const shortcut = SHORTCUTS[variant as ButtonVariantShortcut];
180
+ return { ...shortcut, ignoredTone: tone !== undefined && tone !== shortcut.tone };
181
+ }
182
+ const v = variant as ButtonStyleVariant;
183
+ return { variant: v, tone: tone ?? NATURAL_TONE[v], ignoredTone: false };
184
+ }
@@ -0,0 +1,72 @@
1
+ import { InjectionToken, type Provider } from '@angular/core';
2
+
3
+ /*
4
+ * Textos del calendario, del Date Picker y del Date Range, en archivo propio: así se pueden
5
+ * proveer en `app.config.ts` sin llevar los componentes al chunk inicial. Los nombres de meses y
6
+ * días no van aquí: salen de `Intl.DateTimeFormat` con el idioma.
7
+ */
8
+
9
+ export interface MimiDatePickerText {
10
+ /** Nombre del panel del Date Picker. */
11
+ pickDate: string;
12
+ /** Nombre del panel del Date Range. */
13
+ pickRange: string;
14
+ /** Botones de mes (o año, o página de años) anterior y siguiente. */
15
+ previous: string;
16
+ next: string;
17
+ /** Botón del título, que cambia la vista (días → meses → años). */
18
+ changeView: string;
19
+ /** Leyenda del punto de hoy. */
20
+ today: string;
21
+ /** Botón que lleva al mes de hoy. */
22
+ goToday: string;
23
+ /** Se agrega al nombre de un día que no se puede elegir. */
24
+ unavailable: string;
25
+ /** Ayuda del rango con el inicio elegido. */
26
+ pickEnd: string;
27
+ /** Ayuda del rango sin elegir o con el periodo completo. */
28
+ pickStart: string;
29
+ /** Cuenta de días del rango: «7 días». */
30
+ days: (count: number) => string;
31
+ }
32
+
33
+ /** Textos predeterminados (inglés). */
34
+ export const MIMI_DATE_PICKER_TEXT_EN: MimiDatePickerText = {
35
+ pickDate: 'Choose date',
36
+ pickRange: 'Choose period',
37
+ previous: 'Previous',
38
+ next: 'Next',
39
+ changeView: 'Change view',
40
+ today: 'Today',
41
+ goToday: 'Go to today',
42
+ unavailable: 'unavailable',
43
+ pickEnd: 'Choose the end date.',
44
+ pickStart: 'Click a day to start a new period.',
45
+ days: (count) => `${count} ${count === 1 ? 'day' : 'days'}`,
46
+ };
47
+
48
+ /** Textos en español: `provideMimiDatePickerText(MIMI_DATE_PICKER_TEXT_ES)`. */
49
+ export const MIMI_DATE_PICKER_TEXT_ES: MimiDatePickerText = {
50
+ pickDate: 'Elegir fecha',
51
+ pickRange: 'Elegir periodo',
52
+ previous: 'Anterior',
53
+ next: 'Siguiente',
54
+ changeView: 'Cambiar vista',
55
+ today: 'Hoy',
56
+ goToday: 'Ir a hoy',
57
+ unavailable: 'no disponible',
58
+ pickEnd: 'Elige el día final.',
59
+ pickStart: 'Haz clic en un día para empezar un periodo nuevo.',
60
+ days: (count) => `${count} ${count === 1 ? 'día' : 'días'}`,
61
+ };
62
+
63
+ /** Textos activos. Sin `provideMimiDatePickerText`, los predeterminados en inglés. */
64
+ export const MIMI_DATE_PICKER_TEXT = new InjectionToken<MimiDatePickerText>(
65
+ 'MIMI_DATE_PICKER_TEXT',
66
+ { providedIn: 'root', factory: () => MIMI_DATE_PICKER_TEXT_EN },
67
+ );
68
+
69
+ /** Cambia los textos del calendario, el Date Picker y el Date Range. Lo que no definas sigue en inglés. */
70
+ export function provideMimiDatePickerText(text: Partial<MimiDatePickerText>): Provider {
71
+ return { provide: MIMI_DATE_PICKER_TEXT, useValue: { ...MIMI_DATE_PICKER_TEXT_EN, ...text } };
72
+ }