@mimi-ng/cli 0.1.1 → 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.
Files changed (61) hide show
  1. package/README.md +14 -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 +222 -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/card/card.ts +8 -5
  13. package/dist/templates/components/checkbox/checkbox.ts +3 -3
  14. package/dist/templates/components/confirm/confirm-texts.ts +46 -0
  15. package/dist/templates/components/confirm/confirm.ts +190 -0
  16. package/dist/templates/components/confirm/index.ts +2 -0
  17. package/dist/templates/components/context-menu/context-menu.ts +378 -0
  18. package/dist/templates/components/context-menu/index.ts +14 -0
  19. package/dist/templates/components/dialog/dialog-texts.ts +41 -0
  20. package/dist/templates/components/dialog/dialog.ts +701 -0
  21. package/dist/templates/components/dialog/index.ts +18 -0
  22. package/dist/templates/components/dropdown-menu/dropdown-menu.ts +252 -0
  23. package/dist/templates/components/dropdown-menu/index.ts +14 -0
  24. package/dist/templates/components/form-field/form-error.ts +1 -1
  25. package/dist/templates/components/form-field/form-field.ts +3 -2
  26. package/dist/templates/components/input/input.variants.ts +6 -4
  27. package/dist/templates/components/menu/index.ts +15 -0
  28. package/dist/templates/components/menu/menu.ts +656 -0
  29. package/dist/templates/components/pagination/index.ts +2 -0
  30. package/dist/templates/components/pagination/pagination-page-size.ts +74 -0
  31. package/dist/templates/components/pagination/pagination-texts.ts +69 -0
  32. package/dist/templates/components/pagination/pagination.ts +443 -0
  33. package/dist/templates/components/popover/index.ts +12 -0
  34. package/dist/templates/components/popover/popover.ts +399 -0
  35. package/dist/templates/components/select/index.ts +12 -0
  36. package/dist/templates/components/select/select-texts.ts +62 -0
  37. package/dist/templates/components/select/select.ts +1085 -0
  38. package/dist/templates/components/separator/separator.variants.ts +10 -7
  39. package/dist/templates/components/skeleton/skeleton.ts +4 -1
  40. package/dist/templates/components/switch/switch.ts +6 -6
  41. package/dist/templates/components/tabs/index.ts +2 -0
  42. package/dist/templates/components/tabs/tabs.ts +426 -0
  43. package/dist/templates/components/textarea/textarea.variants.ts +14 -11
  44. package/dist/templates/components/toast/index.ts +3 -0
  45. package/dist/templates/components/toast/toast-config.ts +45 -0
  46. package/dist/templates/components/toast/toast-texts.ts +46 -0
  47. package/dist/templates/components/toast/toast.ts +1000 -0
  48. package/dist/templates/components/toolbar/index.ts +1 -0
  49. package/dist/templates/components/toolbar/toolbar.ts +377 -0
  50. package/dist/templates/components/tooltip/index.ts +7 -0
  51. package/dist/templates/components/tooltip/tooltip.ts +407 -0
  52. package/dist/templates/theme/provider.ts +62 -6
  53. package/dist/templates/theme/theme-base.css +307 -9
  54. package/dist/templates/theme/types.ts +278 -4
  55. package/dist/templates/utils/cn.ts +15 -1
  56. package/dist/templates/utils/control-styles.ts +2 -2
  57. package/dist/templates/utils/overlay.ts +140 -0
  58. package/dist/ui/index.js +80 -8
  59. package/dist/ui/schema.json +44 -0
  60. package/dist/utils/write-files.js +156 -59
  61. package/package.json +1 -1
package/README.md CHANGED
@@ -46,7 +46,9 @@ ng g mimi # menú para elegir (terminal interactiva)
46
46
  ng g @mimi-ng/cli:ui button # forma larga (si Mimi no está en schematicCollections)
47
47
  ```
48
48
 
49
- Cada componente se copia a `src/app/components/ui/<nombre>`, con lo que necesite, y se instalan las dependencias que falten. Después lo importas como cualquier otro:
49
+ Cada componente se copia a `src/app/components/ui/<nombre>`, con lo que necesite, y se instalan las dependencias que falten. Algunos usan por dentro `@angular/cdk` y `@angular/aria`, paquetes oficiales del equipo de Angular: `ng g mimi` los instala cuando hacen falta (aria en la versión exacta de tu cdk) y agrega los estilos del CDK a tu CSS global. Más en [Fundamentos](https://ng.mimiworks.dev/docs/foundations).
50
+
51
+ Después lo importas como cualquier otro:
50
52
 
51
53
  ```ts
52
54
  import { MimiButton } from '@/components/ui/button';
@@ -66,12 +68,23 @@ export class App {}
66
68
  - **Button**: `ng g mimi button` · [documentación](https://ng.mimiworks.dev/docs/components/button)
67
69
  - **Card**: `ng g mimi card` · [documentación](https://ng.mimiworks.dev/docs/components/card)
68
70
  - **Checkbox**: `ng g mimi checkbox` · [documentación](https://ng.mimiworks.dev/docs/components/switch)
71
+ - **Confirm**: `ng g mimi confirm` · [documentación](https://ng.mimiworks.dev/docs/components/confirm)
72
+ - **Context Menu**: `ng g mimi context-menu` · [documentación](https://ng.mimiworks.dev/docs/components/context-menu)
73
+ - **Dialog**: `ng g mimi dialog` · [documentación](https://ng.mimiworks.dev/docs/components/dialog)
74
+ - **Dropdown Menu**: `ng g mimi dropdown-menu` · [documentación](https://ng.mimiworks.dev/docs/components/dropdown-menu)
69
75
  - **FormField**: `ng g mimi form-field` · [documentación](https://ng.mimiworks.dev/docs/components/form-field)
70
76
  - **Input**: `ng g mimi input` · [documentación](https://ng.mimiworks.dev/docs/components/input)
77
+ - **Pagination**: `ng g mimi pagination` · [documentación](https://ng.mimiworks.dev/docs/components/pagination)
78
+ - **Popover**: `ng g mimi popover` · [documentación](https://ng.mimiworks.dev/docs/components/popover)
79
+ - **Select**: `ng g mimi select` · [documentación](https://ng.mimiworks.dev/docs/components/select)
71
80
  - **Separator**: `ng g mimi separator` · [documentación](https://ng.mimiworks.dev/docs/components/separator)
72
81
  - **Skeleton**: `ng g mimi skeleton` · [documentación](https://ng.mimiworks.dev/docs/components/skeleton)
73
82
  - **Switch**: `ng g mimi switch` · [documentación](https://ng.mimiworks.dev/docs/components/switch)
83
+ - **Tabs**: `ng g mimi tabs` · [documentación](https://ng.mimiworks.dev/docs/components/tabs)
74
84
  - **Textarea**: `ng g mimi textarea` · [documentación](https://ng.mimiworks.dev/docs/components/input)
85
+ - **Toast**: `ng g mimi toast` · [documentación](https://ng.mimiworks.dev/docs/components/toast)
86
+ - **Toolbar**: `ng g mimi toolbar` · [documentación](https://ng.mimiworks.dev/docs/components/toolbar)
87
+ - **Tooltip**: `ng g mimi tooltip` · [documentación](https://ng.mimiworks.dev/docs/components/tooltip)
75
88
 
76
89
  ## Tus cambios
77
90
 
package/dist/init/css.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.findTailwindImport = findTailwindImport;
4
4
  exports.insertAfterTailwind = insertAfterTailwind;
5
+ exports.insertAfterTheme = insertAfterTheme;
5
6
  exports.hasDirective = hasDirective;
6
7
  /** `@import "tailwindcss";` o `@import 'tailwindcss' source(none);`, en su propia línea. */
7
8
  const TAILWIND_IMPORT = /^[ \t]*@import\s+(["'])tailwindcss\1([^;\n]*);[^\n]*$/m;
@@ -25,6 +26,22 @@ function insertAfterTailwind(css, lines) {
25
26
  const insert = lines.map((line) => eol + line).join('');
26
27
  return css.slice(0, found.end) + insert + css.slice(found.end);
27
28
  }
29
+ /** `@import` del tema de Mimi (`…/theme/theme-base.css`), en su propia línea. */
30
+ const THEME_IMPORT = /^[ \t]*@import\s+(["'])[^"'\n]*theme-base\.css\1[^;\n]*;[^\n]*$/m;
31
+ /**
32
+ * Inserta líneas después del `@import` del tema o, si no está, después del de Tailwind.
33
+ * Devuelve null si no hay ninguno de los dos (no se sabe dónde ponerlas).
34
+ */
35
+ function insertAfterTheme(css, lines) {
36
+ const theme = THEME_IMPORT.exec(css);
37
+ if (!theme)
38
+ return findTailwindImport(css) ? insertAfterTailwind(css, lines) : null;
39
+ if (lines.length === 0)
40
+ return css;
41
+ const end = theme.index + theme[0].length;
42
+ const eol = eolOf(css);
43
+ return css.slice(0, end) + lines.map((line) => eol + line).join('') + css.slice(end);
44
+ }
28
45
  /** ¿Ya hay un `@import` o `@source` con esa ruta (con comillas simples o dobles)? */
29
46
  function hasDirective(css, directive, path) {
30
47
  const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -9,11 +9,10 @@ const registry_1 = require("../registry");
9
9
  const project_1 = require("../utils/project");
10
10
  const write_files_1 = require("../utils/write-files");
11
11
  const css_1 = require("./css");
12
+ const items_1 = require("./items");
12
13
  const paths_1 = require("./paths");
13
14
  /** Alias que usan todos los componentes. Lo configurable es la carpeta a la que apunta. */
14
15
  exports.ALIAS = '@/components/ui/*';
15
- /** Ítems del registro que copia init: el tema y las utilidades base. */
16
- const INIT_ITEMS = ['theme', 'utils/cn', 'utils/control-styles'];
17
16
  /** Paquetes que instala init (versiones del registro). */
18
17
  const INIT_PACKAGES = ['clsx', 'tailwind-merge', 'class-variance-authority'];
19
18
  const ICONS_PACKAGE = '@lucide/angular';
@@ -35,7 +34,7 @@ function init(options) {
35
34
  const report = (0, write_files_1.emptyReport)();
36
35
  return (0, schematics_1.chain)([
37
36
  (0, write_files_1.writeTemplates)({
38
- files: INIT_ITEMS.flatMap((item) => (0, registry_1.resolveItems)(item)).flatMap((item) => registry_1.registry.items[item].files),
37
+ items: items_1.INIT_ITEMS.flatMap((item) => (0, registry_1.resolveItems)(item)),
39
38
  componentsDir: plan.componentsDir,
40
39
  overwrite: options.overwrite ?? false,
41
40
  version: registry_1.registry.version,
@@ -219,7 +218,7 @@ function writeMimiConfig(plan) {
219
218
  if (plan.mimiConfig)
220
219
  return;
221
220
  const config = {
222
- style: 'vivid',
221
+ style: 'mimi',
223
222
  tailwind: { css: plan.cssPath },
224
223
  aliases: {
225
224
  components: plan.componentsDir,
@@ -270,10 +269,13 @@ function nextSteps(plan, report, icons) {
270
269
  for (const file of report.overwritten) {
271
270
  context.logger.info(`Reemplazado: ${plan.componentsDir}/${file}`);
272
271
  }
273
- const itemOf = (file) => INIT_ITEMS.find((item) => registry_1.registry.items[item].files.includes(file)) ?? file;
272
+ const itemOf = (file) => items_1.INIT_ITEMS.find((item) => registry_1.registry.items[item].files.includes(file)) ?? file;
274
273
  for (const message of (0, write_files_1.updatedMessages)(report, itemOf, registry_1.registry.version)) {
275
274
  context.logger.info(message);
276
275
  }
276
+ for (const message of (0, write_files_1.blockedMessages)(report, plan.componentsDir, registry_1.registry.version)) {
277
+ context.logger.warn(message);
278
+ }
277
279
  if (!plan.changed && !(0, write_files_1.hasFileChanges)(report)) {
278
280
  context.logger.info('Mimi ya estaba configurado; no hubo cambios.');
279
281
  return;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.INIT_ITEMS = void 0;
4
+ /**
5
+ * Ítems del registro que copia init: el tema y las utilidades base. `ng g mimi` también los
6
+ * revisa (y los actualiza si el usuario no los modificó), para que el tema no quede en una
7
+ * versión anterior a la de los componentes.
8
+ */
9
+ exports.INIT_ITEMS = ['theme', 'utils/cn', 'utils/control-styles'];
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.1",
2
+ "version": "0.2.0",
3
3
  "suggestedDependencies": {
4
4
  "@lucide/angular": "^1.47.0"
5
5
  },
@@ -29,6 +29,29 @@
29
29
  "@angular/forms": "^22.0.0"
30
30
  }
31
31
  },
32
+ "utils/overlay": {
33
+ "type": "util",
34
+ "files": [
35
+ "utils/overlay.ts"
36
+ ],
37
+ "peerDependencies": {
38
+ "@angular/cdk": "^22.2.0"
39
+ }
40
+ },
41
+ "menu": {
42
+ "type": "util",
43
+ "files": [
44
+ "components/menu/menu.ts",
45
+ "components/menu/index.ts"
46
+ ],
47
+ "peerDependencies": {
48
+ "@angular/aria": "^22.2.0"
49
+ },
50
+ "registryDependencies": [
51
+ "utils/cn",
52
+ "utils/overlay"
53
+ ]
54
+ },
32
55
  "theme": {
33
56
  "type": "theme",
34
57
  "files": [
@@ -106,6 +129,148 @@
106
129
  "utils/field-state"
107
130
  ]
108
131
  },
132
+ "confirm": {
133
+ "type": "component",
134
+ "files": [
135
+ "components/confirm/confirm.ts",
136
+ "components/confirm/confirm-texts.ts",
137
+ "components/confirm/index.ts"
138
+ ],
139
+ "registryDependencies": [
140
+ "button",
141
+ "dialog",
142
+ "utils/cn"
143
+ ]
144
+ },
145
+ "context-menu": {
146
+ "type": "component",
147
+ "files": [
148
+ "components/context-menu/context-menu.ts",
149
+ "components/context-menu/index.ts"
150
+ ],
151
+ "peerDependencies": {
152
+ "@angular/cdk": "^22.2.0"
153
+ },
154
+ "cssImports": [
155
+ "@angular/cdk/overlay-prebuilt.css"
156
+ ],
157
+ "registryDependencies": [
158
+ "menu",
159
+ "utils/overlay"
160
+ ]
161
+ },
162
+ "dialog": {
163
+ "type": "component",
164
+ "files": [
165
+ "components/dialog/dialog.ts",
166
+ "components/dialog/dialog-texts.ts",
167
+ "components/dialog/index.ts"
168
+ ],
169
+ "peerDependencies": {
170
+ "@angular/cdk": "^22.2.0"
171
+ },
172
+ "cssImports": [
173
+ "@angular/cdk/overlay-prebuilt.css"
174
+ ],
175
+ "registryDependencies": [
176
+ "utils/cn",
177
+ "utils/control-styles",
178
+ "utils/overlay"
179
+ ]
180
+ },
181
+ "tabs": {
182
+ "type": "component",
183
+ "files": [
184
+ "components/tabs/tabs.ts",
185
+ "components/tabs/index.ts"
186
+ ],
187
+ "dependencies": {
188
+ "class-variance-authority": "^0.7.1"
189
+ },
190
+ "peerDependencies": {
191
+ "@angular/aria": "^22.2.0"
192
+ },
193
+ "registryDependencies": [
194
+ "utils/cn",
195
+ "utils/control-styles"
196
+ ]
197
+ },
198
+ "toast": {
199
+ "type": "component",
200
+ "files": [
201
+ "components/toast/toast.ts",
202
+ "components/toast/toast-config.ts",
203
+ "components/toast/toast-texts.ts",
204
+ "components/toast/index.ts"
205
+ ],
206
+ "dependencies": {
207
+ "class-variance-authority": "^0.7.1"
208
+ },
209
+ "peerDependencies": {
210
+ "@angular/cdk": "^22.2.0"
211
+ },
212
+ "registryDependencies": [
213
+ "button",
214
+ "utils/cn",
215
+ "utils/control-styles",
216
+ "utils/overlay"
217
+ ]
218
+ },
219
+ "pagination": {
220
+ "type": "component",
221
+ "files": [
222
+ "components/pagination/pagination.ts",
223
+ "components/pagination/pagination-page-size.ts",
224
+ "components/pagination/pagination-texts.ts",
225
+ "components/pagination/index.ts"
226
+ ],
227
+ "dependencies": {
228
+ "class-variance-authority": "^0.7.1"
229
+ },
230
+ "peerDependencies": {
231
+ "@angular/cdk": "^22.2.0"
232
+ },
233
+ "registryDependencies": [
234
+ "button",
235
+ "input",
236
+ "select",
237
+ "utils/cn"
238
+ ]
239
+ },
240
+ "toolbar": {
241
+ "type": "component",
242
+ "files": [
243
+ "components/toolbar/toolbar.ts",
244
+ "components/toolbar/index.ts"
245
+ ],
246
+ "dependencies": {
247
+ "class-variance-authority": "^0.7.1"
248
+ },
249
+ "peerDependencies": {
250
+ "@angular/cdk": "^22.2.0"
251
+ },
252
+ "registryDependencies": [
253
+ "utils/cn",
254
+ "utils/control-styles"
255
+ ]
256
+ },
257
+ "dropdown-menu": {
258
+ "type": "component",
259
+ "files": [
260
+ "components/dropdown-menu/dropdown-menu.ts",
261
+ "components/dropdown-menu/index.ts"
262
+ ],
263
+ "peerDependencies": {
264
+ "@angular/cdk": "^22.2.0"
265
+ },
266
+ "cssImports": [
267
+ "@angular/cdk/overlay-prebuilt.css"
268
+ ],
269
+ "registryDependencies": [
270
+ "menu",
271
+ "utils/overlay"
272
+ ]
273
+ },
109
274
  "form-field": {
110
275
  "type": "component",
111
276
  "files": [
@@ -135,6 +300,45 @@
135
300
  "utils/field-state"
136
301
  ]
137
302
  },
303
+ "popover": {
304
+ "type": "component",
305
+ "files": [
306
+ "components/popover/popover.ts",
307
+ "components/popover/index.ts"
308
+ ],
309
+ "peerDependencies": {
310
+ "@angular/cdk": "^22.2.0"
311
+ },
312
+ "cssImports": [
313
+ "@angular/cdk/overlay-prebuilt.css"
314
+ ],
315
+ "registryDependencies": [
316
+ "utils/cn",
317
+ "utils/overlay"
318
+ ]
319
+ },
320
+ "select": {
321
+ "type": "component",
322
+ "files": [
323
+ "components/select/select.ts",
324
+ "components/select/select-texts.ts",
325
+ "components/select/index.ts"
326
+ ],
327
+ "peerDependencies": {
328
+ "@angular/aria": "^22.2.0",
329
+ "@angular/cdk": "^22.2.0",
330
+ "@angular/forms": "^22.0.0"
331
+ },
332
+ "cssImports": [
333
+ "@angular/cdk/overlay-prebuilt.css"
334
+ ],
335
+ "registryDependencies": [
336
+ "utils/cn",
337
+ "utils/control-styles",
338
+ "utils/field-state",
339
+ "utils/overlay"
340
+ ]
341
+ },
138
342
  "separator": {
139
343
  "type": "component",
140
344
  "files": [
@@ -174,6 +378,23 @@
174
378
  "utils/field-state"
175
379
  ]
176
380
  },
381
+ "tooltip": {
382
+ "type": "component",
383
+ "files": [
384
+ "components/tooltip/tooltip.ts",
385
+ "components/tooltip/index.ts"
386
+ ],
387
+ "peerDependencies": {
388
+ "@angular/cdk": "^22.2.0"
389
+ },
390
+ "cssImports": [
391
+ "@angular/cdk/overlay-prebuilt.css"
392
+ ],
393
+ "registryDependencies": [
394
+ "utils/cn",
395
+ "utils/overlay"
396
+ ]
397
+ },
177
398
  "textarea": {
178
399
  "type": "component",
179
400
  "files": [
@@ -117,7 +117,7 @@ export class MimiAvatarFallback {
117
117
 
118
118
  protected readonly classes = computed(() =>
119
119
  cn(
120
- 'flex size-full items-center justify-center rounded-full bg-secondary font-semibold text-secondary-foreground',
120
+ 'flex size-full items-center justify-center rounded-[var(--mimi-avatar-radius,calc(infinity*1px))] bg-[color:var(--mimi-avatar-bg,var(--mimi-secondary))] font-semibold text-[color:var(--mimi-avatar-fg,var(--mimi-secondary-foreground))]',
121
121
  this.userClass(),
122
122
  // Al final: con la imagen cargada se oculta aunque el usuario pase `flex`.
123
123
  this.avatar.status() === 'loaded' && 'hidden',
@@ -6,13 +6,13 @@ import { cva, type VariantProps } from 'class-variance-authority';
6
6
  * default 40px / 14px, lg 56px / 18px (el tamaño de letra lo heredan las iniciales).
7
7
  */
8
8
  export const avatarVariants = cva(
9
- 'relative inline-flex shrink-0 overflow-hidden rounded-full bg-muted select-none',
9
+ 'relative inline-flex shrink-0 overflow-hidden rounded-[var(--mimi-avatar-radius,calc(infinity*1px))] bg-muted select-none',
10
10
  {
11
11
  variants: {
12
12
  size: {
13
- sm: 'size-8 text-xs',
14
- default: 'size-10 text-sm',
15
- lg: 'size-14 text-lg',
13
+ sm: 'size-[var(--mimi-avatar-size-sm,2rem)] text-xs',
14
+ default: 'size-[var(--mimi-avatar-size,2.5rem)] text-sm',
15
+ lg: 'size-[var(--mimi-avatar-size-lg,3.5rem)] text-lg',
16
16
  },
17
17
  },
18
18
  defaultVariants: { size: 'default' },
@@ -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
+ }