@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
@@ -0,0 +1,181 @@
1
+ import { InjectionToken } from '@angular/core';
2
+
3
+ /*
4
+ * Fechas del calendario, del Date Picker y del Date Range (G2.9; decidido por defecto el
5
+ * 05/11/2026: opción (b), cadena ISO). Una fecha es `'AAAA-MM-DD'`, sin hora ni zona horaria,
6
+ * como el `value` de `<input type="date">`: va directo a JSON y se compara como texto. Por dentro
7
+ * se calcula en UTC, así que ninguna zona horaria corre un día.
8
+ */
9
+
10
+ /** Una fecha sin hora: `'2026-09-30'`. */
11
+ export type MimiDate = string;
12
+
13
+ /** Un periodo, con los dos extremos incluidos. */
14
+ export interface MimiDateRange {
15
+ start: MimiDate;
16
+ end: MimiDate;
17
+ }
18
+
19
+ const ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
20
+
21
+ /** `true` si es una fecha ISO válida (`'2026-02-30'` no lo es). */
22
+ export function mimiIsDate(value: unknown): value is MimiDate {
23
+ if (typeof value !== 'string') return false;
24
+ const match = ISO.exec(value);
25
+ if (!match) return false;
26
+ const time = Date.UTC(+match[1], +match[2] - 1, +match[3]);
27
+ return new Date(time).toISOString().slice(0, 10) === value;
28
+ }
29
+
30
+ /**
31
+ * Convierte un `Date` en una fecha de Mimi: el día del calendario de ese instante en la zona
32
+ * horaria local o en `timeZone` (por ejemplo, `'America/Lima'`).
33
+ */
34
+ export function toMimiDate(date: Date, timeZone?: string): MimiDate {
35
+ if (!timeZone) {
36
+ return iso(date.getFullYear(), date.getMonth() + 1, date.getDate());
37
+ }
38
+ const parts = new Intl.DateTimeFormat('en-US', {
39
+ timeZone,
40
+ year: 'numeric',
41
+ month: 'numeric',
42
+ day: 'numeric',
43
+ }).formatToParts(date);
44
+ const part = (type: string) => +(parts.find((p) => p.type === type)?.value ?? 0);
45
+ return iso(part('year'), part('month'), part('day'));
46
+ }
47
+
48
+ /** Convierte una fecha de Mimi en un `Date` a medianoche local (para `DatePipe` y similares). */
49
+ export function fromMimiDate(date: MimiDate): Date {
50
+ const [year, month, day] = date.split('-').map(Number);
51
+ return new Date(year, month - 1, day);
52
+ }
53
+
54
+ function pad(n: number, size = 2): string {
55
+ return String(n).padStart(size, '0');
56
+ }
57
+
58
+ function iso(year: number, month: number, day: number): MimiDate {
59
+ return `${pad(year, 4)}-${pad(month)}-${pad(day)}`;
60
+ }
61
+
62
+ /** Milisegundos UTC de la fecha (para calcular sin zona horaria). */
63
+ function utc(date: MimiDate): number {
64
+ const [year, month, day] = date.split('-').map(Number);
65
+ return Date.UTC(year, month - 1, day);
66
+ }
67
+
68
+ function fromUtc(time: number): MimiDate {
69
+ return new Date(time).toISOString().slice(0, 10);
70
+ }
71
+
72
+ const DAY = 86_400_000;
73
+
74
+ /** Suma días (negativos restan). */
75
+ export function mimiAddDays(date: MimiDate, days: number): MimiDate {
76
+ return fromUtc(utc(date) + days * DAY);
77
+ }
78
+
79
+ /** Suma meses; si el día no existe en el mes nuevo (31 de abril), queda en el último. */
80
+ export function mimiAddMonths(date: MimiDate, months: number): MimiDate {
81
+ const [year, month, day] = date.split('-').map(Number);
82
+ const target = new Date(Date.UTC(year, month - 1 + months, 1));
83
+ const last = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0));
84
+ return iso(target.getUTCFullYear(), target.getUTCMonth() + 1, Math.min(day, last.getUTCDate()));
85
+ }
86
+
87
+ /** Días entre dos fechas, contando las dos (del 24 al 30 son 7). */
88
+ export function mimiDaysBetween(start: MimiDate, end: MimiDate): number {
89
+ return Math.round((utc(end) - utc(start)) / DAY) + 1;
90
+ }
91
+
92
+ /** Día de la semana ISO: 1 = lunes … 7 = domingo. */
93
+ export function mimiWeekday(date: MimiDate): number {
94
+ return new Date(utc(date)).getUTCDay() || 7;
95
+ }
96
+
97
+ /** Primer día del mes de la fecha. */
98
+ export function mimiStartOfMonth(date: MimiDate): MimiDate {
99
+ return `${date.slice(0, 7)}-01`;
100
+ }
101
+
102
+ /** Acota una fecha entre `min` y `max` (los dos opcionales). */
103
+ export function mimiClampDate(
104
+ date: MimiDate,
105
+ min?: MimiDate | null,
106
+ max?: MimiDate | null,
107
+ ): MimiDate {
108
+ if (min && date < min) return min;
109
+ if (max && date > max) return max;
110
+ return date;
111
+ }
112
+
113
+ /** Regiones donde la semana empieza el domingo (respaldo de `Intl.Locale#getWeekInfo`). */
114
+ const SUNDAY_FIRST = new Set([
115
+ 'US',
116
+ 'CA',
117
+ 'MX',
118
+ 'BR',
119
+ 'JP',
120
+ 'KR',
121
+ 'TW',
122
+ 'HK',
123
+ 'IL',
124
+ 'PH',
125
+ 'IN',
126
+ 'ZA',
127
+ 'SA',
128
+ 'PE',
129
+ 'CO',
130
+ 'VE',
131
+ 'GT',
132
+ 'HN',
133
+ 'SV',
134
+ 'NI',
135
+ 'PA',
136
+ 'DO',
137
+ 'PR',
138
+ 'PY',
139
+ ]);
140
+
141
+ interface WeekInfoLocale {
142
+ getWeekInfo?: () => { firstDay: number };
143
+ weekInfo?: { firstDay: number };
144
+ maximize?: () => { region?: string };
145
+ region?: string;
146
+ }
147
+
148
+ /**
149
+ * Primer día de la semana del idioma (1 = lunes … 7 = domingo), con `Intl.Locale#getWeekInfo`
150
+ * (o `weekInfo`) donde existe; si no, por la región del idioma (domingo en las de la lista,
151
+ * lunes en el resto).
152
+ */
153
+ export function mimiWeekStart(locale: string): number {
154
+ try {
155
+ const intl = new Intl.Locale(locale) as unknown as WeekInfoLocale;
156
+ const info = intl.getWeekInfo?.() ?? intl.weekInfo;
157
+ if (info?.firstDay) return info.firstDay;
158
+ const region = intl.region ?? intl.maximize?.().region;
159
+ return region && SUNDAY_FIRST.has(region) ? 7 : 1;
160
+ } catch {
161
+ return 1;
162
+ }
163
+ }
164
+
165
+ /** Formatea una fecha con `Intl.DateTimeFormat` (en UTC: no cambia de día con la zona). */
166
+ export function mimiFormatDate(
167
+ date: MimiDate,
168
+ locale: string,
169
+ options: Intl.DateTimeFormatOptions,
170
+ ): string {
171
+ return new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' }).format(utc(date));
172
+ }
173
+
174
+ /**
175
+ * «Hoy» como fecha de Mimi. Por defecto, el día local del navegador; se puede reemplazar (en
176
+ * pruebas o con la zona horaria de la cuenta): `{ provide: MIMI_TODAY, useValue: () => '2026-09-30' }`.
177
+ */
178
+ export const MIMI_TODAY = new InjectionToken<() => MimiDate>('MIMI_TODAY', {
179
+ providedIn: 'root',
180
+ factory: () => () => toMimiDate(new Date()),
181
+ });
@@ -0,0 +1,3 @@
1
+ export * from './calendar';
2
+ export * from './calendar-texts';
3
+ export * from './dates';
@@ -24,9 +24,9 @@ export class MimiCard {
24
24
  readonly userClass = input('', { alias: 'class' });
25
25
  protected readonly classes = computed(() =>
26
26
  cn(
27
- 'flex flex-col bg-card text-card-foreground',
27
+ 'flex flex-col bg-[color:var(--mimi-card-bg,var(--mimi-card))] text-[color:var(--mimi-card-fg,var(--mimi-card-foreground))]',
28
28
  'rounded-[var(--mimi-card-radius,var(--mimi-radius-card))]',
29
- 'border-[length:var(--mimi-card-border-width,1px)] border-border',
29
+ 'border-[length:var(--mimi-card-border-width,1px)] border-[color:var(--mimi-card-border,var(--mimi-border))]',
30
30
  'shadow-[shadow:var(--mimi-card-shadow,var(--mimi-shadow-card))]',
31
31
  this.userClass(),
32
32
  ),
@@ -44,7 +44,7 @@ export class MimiCardHeader {
44
44
  readonly userClass = input('', { alias: 'class' });
45
45
  protected readonly classes = computed(() =>
46
46
  cn(
47
- 'flex flex-col gap-1.5 p-[var(--mimi-card-padding-header,1.5rem_1.5rem_1rem)]',
47
+ 'flex flex-col gap-1.5 p-[var(--mimi-card-padding-header,var(--mimi-card-padding,1.5rem)_var(--mimi-card-padding,1.5rem)_1rem)]',
48
48
  this.userClass(),
49
49
  ),
50
50
  );
@@ -101,7 +101,10 @@ export class MimiCardDescription {
101
101
  export class MimiCardContent {
102
102
  readonly userClass = input('', { alias: 'class' });
103
103
  protected readonly classes = computed(() =>
104
- cn('block p-[var(--mimi-card-padding-content,0_1.5rem_1.25rem)] first:pt-6', this.userClass()),
104
+ cn(
105
+ 'block p-[var(--mimi-card-padding-content,0_var(--mimi-card-padding,1.5rem)_1.25rem)] first:pt-[var(--mimi-card-padding,1.5rem)]',
106
+ this.userClass(),
107
+ ),
105
108
  );
106
109
  }
107
110
 
@@ -116,7 +119,7 @@ export class MimiCardFooter {
116
119
  readonly userClass = input('', { alias: 'class' });
117
120
  protected readonly classes = computed(() =>
118
121
  cn(
119
- 'flex items-center justify-end gap-2 p-[var(--mimi-card-padding-footer,0_1.5rem_1.5rem)] first:pt-6',
122
+ 'flex items-center justify-end gap-2 p-[var(--mimi-card-padding-footer,0_var(--mimi-card-padding,1.5rem)_var(--mimi-card-padding,1.5rem))] first:pt-[var(--mimi-card-padding,1.5rem)]',
120
123
  this.userClass(),
121
124
  ),
122
125
  );
@@ -150,10 +150,10 @@ export class MimiCheckbox implements FormCheckboxControl, MimiFieldControl {
150
150
 
151
151
  // Caja de 16px. Marcada o mixta: primary con su sombra. Foco: borde ring (sin marcar) y contorno.
152
152
  protected readonly boxClasses = cn(
153
- 'peer inline-flex size-4 shrink-0 items-center justify-center rounded-sm border border-input bg-input-background text-primary-foreground',
153
+ 'peer inline-flex size-[var(--mimi-checkbox-size,1rem)] shrink-0 items-center justify-center rounded-[var(--mimi-checkbox-radius,var(--mimi-radius-sm))] border border-[color:var(--mimi-checkbox-border,var(--mimi-input))] bg-input-background text-[color:var(--mimi-checkbox-check,var(--mimi-primary-foreground))]',
154
154
  'mimi-transition active:scale-(--mimi-press-scale-sm)',
155
- 'data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:shadow-primary',
156
- 'data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:shadow-primary',
155
+ 'data-[state=checked]:border-[color:var(--mimi-checkbox-checked-bg,var(--mimi-primary))] data-[state=checked]:bg-[color:var(--mimi-checkbox-checked-bg,var(--mimi-primary))] data-[state=checked]:shadow-primary',
156
+ 'data-[state=indeterminate]:border-[color:var(--mimi-checkbox-checked-bg,var(--mimi-primary))] data-[state=indeterminate]:bg-[color:var(--mimi-checkbox-checked-bg,var(--mimi-primary))] data-[state=indeterminate]:shadow-primary',
157
157
  'data-[state=unchecked]:focus-visible:border-ring data-[state=unchecked]:aria-invalid:border-destructive',
158
158
  'disabled:shadow-none',
159
159
  buttonFocusStyles,
@@ -0,0 +1,40 @@
1
+ import { InjectionToken, type Provider } from '@angular/core';
2
+
3
+ /*
4
+ * Textos del chip compartido (Tags Input y el Combobox múltiple), en archivo propio: así se
5
+ * pueden proveer en `app.config.ts` sin llevar los componentes al chunk inicial.
6
+ */
7
+
8
+ export interface MimiChipText {
9
+ /** Nombre del botón de quitar. */
10
+ remove: (label: string) => string;
11
+ /** Anuncio al agregar. */
12
+ added: (label: string) => string;
13
+ /** Anuncio al quitar. */
14
+ removed: (label: string) => string;
15
+ }
16
+
17
+ /** Textos predeterminados (inglés). */
18
+ export const MIMI_CHIP_TEXT_EN: MimiChipText = {
19
+ remove: (label) => `Remove ${label}`,
20
+ added: (label) => `${label} added`,
21
+ removed: (label) => `${label} removed`,
22
+ };
23
+
24
+ /** Textos en español: `provideMimiChipText(MIMI_CHIP_TEXT_ES)`. */
25
+ export const MIMI_CHIP_TEXT_ES: MimiChipText = {
26
+ remove: (label) => `Quitar ${label}`,
27
+ added: (label) => `${label} agregada`,
28
+ removed: (label) => `${label} quitada`,
29
+ };
30
+
31
+ /** Textos activos. Sin `provideMimiChipText`, los predeterminados en inglés. */
32
+ export const MIMI_CHIP_TEXT = new InjectionToken<MimiChipText>('MIMI_CHIP_TEXT', {
33
+ providedIn: 'root',
34
+ factory: () => MIMI_CHIP_TEXT_EN,
35
+ });
36
+
37
+ /** Cambia los textos del chip. Lo que no definas sigue en inglés. */
38
+ export function provideMimiChipText(text: Partial<MimiChipText>): Provider {
39
+ return { provide: MIMI_CHIP_TEXT, useValue: { ...MIMI_CHIP_TEXT_EN, ...text } };
40
+ }
@@ -0,0 +1,116 @@
1
+ import { LiveAnnouncer } from '@angular/cdk/a11y';
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ booleanAttribute,
6
+ computed,
7
+ inject,
8
+ input,
9
+ output,
10
+ } from '@angular/core';
11
+ import { cn } from '@/components/ui/utils/cn';
12
+ import { buttonFocusStyles } from '@/components/ui/utils/control-styles';
13
+ import { MIMI_CHIP_TEXT } from './chip-texts';
14
+
15
+ /*
16
+ * Chip compartido (ítem `chip` de la CLI, decidido el 03/11/2026; prefijo de tokens `chip`,
17
+ * spec 13). Lo usan Tags Input (G2.6) y el Combobox múltiple (G2.8): el chip con su botón de
18
+ * quitar y un util de anuncios (`injectMimiChipAnnouncer`).
19
+ */
20
+
21
+ const CHIP_BASE = cn(
22
+ 'inline-flex max-w-full min-w-0 shrink-0 items-center gap-1 ps-2.5 font-medium whitespace-nowrap mimi-transition',
23
+ 'h-[var(--mimi-chip-height,26px)] rounded-[var(--mimi-chip-radius,var(--mimi-badge-radius,999px))] text-[length:var(--mimi-chip-font-size,13px)]',
24
+ );
25
+ /** Colores: el global `--mimi-chip` (G2.0b, con respaldo) o, con error, el suave de `danger`. */
26
+ const CHIP_TONE = {
27
+ default:
28
+ 'bg-[color:var(--mimi-chip-bg,var(--mimi-chip,var(--mimi-secondary)))] text-[color:var(--mimi-chip-fg,var(--mimi-chip-foreground,var(--mimi-secondary-foreground)))]',
29
+ invalid: 'bg-destructive-soft-bg text-destructive-soft-foreground',
30
+ } as const;
31
+
32
+ const REMOVE_CLASSES = cn(
33
+ 'inline-flex shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-transparent p-0 text-current opacity-70 mimi-transition',
34
+ 'size-[var(--mimi-chip-remove-size,18px)] [&_svg]:size-3',
35
+ 'hover:bg-[color:color-mix(in_oklab,var(--mimi-foreground)_12%,transparent)] hover:opacity-100 active:scale-(--mimi-press-scale-sm)',
36
+ buttonFocusStyles,
37
+ );
38
+
39
+ /**
40
+ * Chip: `<mimi-chip removable (remove)="quitar(i)">angular</mimi-chip>`. El botón se llama
41
+ * «Remove angular» (`MIMI_CHIP_TEXT`); con `label` se cambia la etiqueta que usa el nombre.
42
+ */
43
+ @Component({
44
+ selector: 'mimi-chip',
45
+ template: `
46
+ <span class="min-w-0 truncate"><ng-content /></span>
47
+ @if (removable()) {
48
+ <button
49
+ type="button"
50
+ data-slot="remove"
51
+ [class]="removeClasses"
52
+ [attr.aria-label]="text.remove(label() ?? '')"
53
+ (click)="remove.emit()"
54
+ >
55
+ <svg
56
+ viewBox="0 0 24 24"
57
+ fill="none"
58
+ stroke="currentColor"
59
+ stroke-width="2"
60
+ stroke-linecap="round"
61
+ stroke-linejoin="round"
62
+ aria-hidden="true"
63
+ >
64
+ <!-- Lucide x (ISC) -->
65
+ <path d="M18 6 6 18" />
66
+ <path d="m6 6 12 12" />
67
+ </svg>
68
+ </button>
69
+ }
70
+ `,
71
+ host: {
72
+ '[class]': 'classes()',
73
+ '[attr.data-invalid]': 'invalid() ? "" : null',
74
+ },
75
+ changeDetection: ChangeDetectionStrategy.OnPush,
76
+ })
77
+ export class MimiChip {
78
+ protected readonly text = inject(MIMI_CHIP_TEXT);
79
+
80
+ /** Muestra el botón de quitar. */
81
+ readonly removable = input(false, { transform: booleanAttribute });
82
+ /** La etiqueta para el nombre del botón («Remove angular»). */
83
+ readonly label = input<string>();
84
+ /** Con error (por ejemplo, el duplicado que se intentó agregar). */
85
+ readonly invalid = input(false, { transform: booleanAttribute });
86
+ readonly userClass = input('', { alias: 'class' });
87
+
88
+ /** Se pulsó el botón de quitar. */
89
+ readonly remove = output<void>();
90
+
91
+ protected readonly removeClasses = REMOVE_CLASSES;
92
+ protected readonly classes = computed(() =>
93
+ cn(
94
+ CHIP_BASE,
95
+ this.removable() ? 'pe-1' : 'pe-2.5',
96
+ CHIP_TONE[this.invalid() ? 'invalid' : 'default'],
97
+ this.userClass(),
98
+ ),
99
+ );
100
+ }
101
+
102
+ /**
103
+ * Anuncios de agregar y quitar, con el `LiveAnnouncer` del CDK en `polite` («angular agregada»).
104
+ * Se llama en el contexto de inyección (al declarar una propiedad).
105
+ */
106
+ export function injectMimiChipAnnouncer(): {
107
+ added: (label: string) => void;
108
+ removed: (label: string) => void;
109
+ } {
110
+ const announcer = inject(LiveAnnouncer);
111
+ const text = inject(MIMI_CHIP_TEXT);
112
+ return {
113
+ added: (label) => void announcer.announce(text.added(label), 'polite'),
114
+ removed: (label) => void announcer.announce(text.removed(label), 'polite'),
115
+ };
116
+ }
@@ -0,0 +1,2 @@
1
+ export * from './chip';
2
+ export * from './chip-texts';
@@ -0,0 +1,57 @@
1
+ import { InjectionToken, type Provider } from '@angular/core';
2
+
3
+ /*
4
+ * Textos del Color Picker, en archivo propio: así se pueden proveer en `app.config.ts` sin llevar
5
+ * el componente al chunk inicial.
6
+ */
7
+
8
+ export interface MimiColorPickerText {
9
+ area: string;
10
+ areaValue: (saturation: number, brightness: number) => string;
11
+ hue: string;
12
+ format: string;
13
+ value: string;
14
+ palette: string;
15
+ recent: string;
16
+ noRecent: string;
17
+ /** Nombre del panel en el disparador. */
18
+ pickColor: string;
19
+ /** Aviso del campo cuando el OKLCH escrito se recortó a sRGB. */
20
+ clipped: string;
21
+ }
22
+
23
+ export const MIMI_COLOR_PICKER_TEXT_EN: MimiColorPickerText = {
24
+ area: 'Saturation and brightness',
25
+ areaValue: (s, v) => `Saturation ${s}%, brightness ${v}%`,
26
+ hue: 'Hue',
27
+ format: 'Format',
28
+ value: 'Color value',
29
+ palette: 'Palette',
30
+ recent: 'Recent',
31
+ noRecent: 'The colors you choose will appear here.',
32
+ pickColor: 'Choose color',
33
+ clipped: 'Adjusted to the closest sRGB color.',
34
+ };
35
+
36
+ export const MIMI_COLOR_PICKER_TEXT_ES: MimiColorPickerText = {
37
+ area: 'Saturación y brillo',
38
+ areaValue: (s, v) => `Saturación ${s}%, brillo ${v}%`,
39
+ hue: 'Tono',
40
+ format: 'Formato',
41
+ value: 'Valor del color',
42
+ palette: 'Paleta',
43
+ recent: 'Recientes',
44
+ noRecent: 'Los colores que elijas aparecerán aquí.',
45
+ pickColor: 'Elegir color',
46
+ clipped: 'Ajustado al color sRGB más cercano.',
47
+ };
48
+
49
+ export const MIMI_COLOR_PICKER_TEXT = new InjectionToken<MimiColorPickerText>(
50
+ 'MIMI_COLOR_PICKER_TEXT',
51
+ { providedIn: 'root', factory: () => MIMI_COLOR_PICKER_TEXT_EN },
52
+ );
53
+
54
+ /** Cambia los textos del Color Picker. Lo que no definas sigue en inglés. */
55
+ export function provideMimiColorPickerText(text: Partial<MimiColorPickerText>): Provider {
56
+ return { provide: MIMI_COLOR_PICKER_TEXT, useValue: { ...MIMI_COLOR_PICKER_TEXT_EN, ...text } };
57
+ }