@mimi-ng/cli 0.1.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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/THIRD_PARTY_NOTICES.md +84 -0
  4. package/dist/collection.json +22 -0
  5. package/dist/init/css.js +32 -0
  6. package/dist/init/index.js +297 -0
  7. package/dist/init/paths.js +43 -0
  8. package/dist/init/schema.json +24 -0
  9. package/dist/ng-add/index.js +8 -0
  10. package/dist/registry.js +30 -0
  11. package/dist/registry.json +194 -0
  12. package/dist/templates/components/avatar/avatar.ts +129 -0
  13. package/dist/templates/components/avatar/avatar.variants.ts +25 -0
  14. package/dist/templates/components/avatar/index.ts +2 -0
  15. package/dist/templates/components/badge/badge.ts +21 -0
  16. package/dist/templates/components/badge/badge.variants.ts +27 -0
  17. package/dist/templates/components/badge/index.ts +2 -0
  18. package/dist/templates/components/button/button.ts +99 -0
  19. package/dist/templates/components/button/button.variants.ts +60 -0
  20. package/dist/templates/components/button/index.ts +2 -0
  21. package/dist/templates/components/card/card.ts +133 -0
  22. package/dist/templates/components/card/index.ts +1 -0
  23. package/dist/templates/components/checkbox/checkbox.ts +178 -0
  24. package/dist/templates/components/checkbox/index.ts +1 -0
  25. package/dist/templates/components/form-field/error-messages.ts +68 -0
  26. package/dist/templates/components/form-field/form-error.ts +82 -0
  27. package/dist/templates/components/form-field/form-field.ts +114 -0
  28. package/dist/templates/components/form-field/index.ts +3 -0
  29. package/dist/templates/components/input/index.ts +2 -0
  30. package/dist/templates/components/input/input.ts +52 -0
  31. package/dist/templates/components/input/input.variants.ts +38 -0
  32. package/dist/templates/components/separator/index.ts +2 -0
  33. package/dist/templates/components/separator/separator.ts +38 -0
  34. package/dist/templates/components/separator/separator.variants.ts +20 -0
  35. package/dist/templates/components/skeleton/index.ts +1 -0
  36. package/dist/templates/components/skeleton/skeleton.ts +27 -0
  37. package/dist/templates/components/switch/index.ts +1 -0
  38. package/dist/templates/components/switch/switch.ts +138 -0
  39. package/dist/templates/components/textarea/index.ts +2 -0
  40. package/dist/templates/components/textarea/textarea.ts +46 -0
  41. package/dist/templates/components/textarea/textarea.variants.ts +27 -0
  42. package/dist/templates/theme/index.ts +2 -0
  43. package/dist/templates/theme/provider.ts +150 -0
  44. package/dist/templates/theme/theme-base.css +241 -0
  45. package/dist/templates/theme/types.ts +124 -0
  46. package/dist/templates/utils/cn.ts +36 -0
  47. package/dist/templates/utils/control-styles.ts +27 -0
  48. package/dist/templates/utils/field-state.ts +164 -0
  49. package/dist/ui/index.js +172 -0
  50. package/dist/ui/schema.json +75 -0
  51. package/dist/utils/project.js +28 -0
  52. package/dist/utils/write-files.js +167 -0
  53. package/package.json +55 -0
@@ -0,0 +1,194 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "suggestedDependencies": {
4
+ "@lucide/angular": "^1.47.0"
5
+ },
6
+ "items": {
7
+ "utils/cn": {
8
+ "type": "util",
9
+ "files": [
10
+ "utils/cn.ts"
11
+ ],
12
+ "dependencies": {
13
+ "clsx": "^2.1.1",
14
+ "tailwind-merge": "^3.7.0"
15
+ }
16
+ },
17
+ "utils/control-styles": {
18
+ "type": "util",
19
+ "files": [
20
+ "utils/control-styles.ts"
21
+ ]
22
+ },
23
+ "utils/field-state": {
24
+ "type": "util",
25
+ "files": [
26
+ "utils/field-state.ts"
27
+ ],
28
+ "peerDependencies": {
29
+ "@angular/forms": "^22.0.0"
30
+ }
31
+ },
32
+ "theme": {
33
+ "type": "theme",
34
+ "files": [
35
+ "theme/theme-base.css",
36
+ "theme/types.ts",
37
+ "theme/provider.ts",
38
+ "theme/index.ts"
39
+ ]
40
+ },
41
+ "avatar": {
42
+ "type": "component",
43
+ "files": [
44
+ "components/avatar/avatar.ts",
45
+ "components/avatar/avatar.variants.ts",
46
+ "components/avatar/index.ts"
47
+ ],
48
+ "dependencies": {
49
+ "class-variance-authority": "^0.7.1"
50
+ },
51
+ "registryDependencies": [
52
+ "utils/cn"
53
+ ]
54
+ },
55
+ "badge": {
56
+ "type": "component",
57
+ "files": [
58
+ "components/badge/badge.ts",
59
+ "components/badge/badge.variants.ts",
60
+ "components/badge/index.ts"
61
+ ],
62
+ "dependencies": {
63
+ "class-variance-authority": "^0.7.1"
64
+ },
65
+ "registryDependencies": [
66
+ "utils/cn"
67
+ ]
68
+ },
69
+ "button": {
70
+ "type": "component",
71
+ "files": [
72
+ "components/button/button.ts",
73
+ "components/button/button.variants.ts",
74
+ "components/button/index.ts"
75
+ ],
76
+ "dependencies": {
77
+ "class-variance-authority": "^0.7.1"
78
+ },
79
+ "registryDependencies": [
80
+ "utils/cn",
81
+ "utils/control-styles"
82
+ ]
83
+ },
84
+ "card": {
85
+ "type": "component",
86
+ "files": [
87
+ "components/card/card.ts",
88
+ "components/card/index.ts"
89
+ ],
90
+ "registryDependencies": [
91
+ "utils/cn"
92
+ ]
93
+ },
94
+ "checkbox": {
95
+ "type": "component",
96
+ "files": [
97
+ "components/checkbox/checkbox.ts",
98
+ "components/checkbox/index.ts"
99
+ ],
100
+ "peerDependencies": {
101
+ "@angular/forms": "^22.0.0"
102
+ },
103
+ "registryDependencies": [
104
+ "utils/cn",
105
+ "utils/control-styles",
106
+ "utils/field-state"
107
+ ]
108
+ },
109
+ "form-field": {
110
+ "type": "component",
111
+ "files": [
112
+ "components/form-field/form-field.ts",
113
+ "components/form-field/form-error.ts",
114
+ "components/form-field/error-messages.ts",
115
+ "components/form-field/index.ts"
116
+ ],
117
+ "registryDependencies": [
118
+ "utils/cn",
119
+ "utils/field-state"
120
+ ]
121
+ },
122
+ "input": {
123
+ "type": "component",
124
+ "files": [
125
+ "components/input/input.ts",
126
+ "components/input/input.variants.ts",
127
+ "components/input/index.ts"
128
+ ],
129
+ "dependencies": {
130
+ "class-variance-authority": "^0.7.1"
131
+ },
132
+ "registryDependencies": [
133
+ "utils/cn",
134
+ "utils/control-styles",
135
+ "utils/field-state"
136
+ ]
137
+ },
138
+ "separator": {
139
+ "type": "component",
140
+ "files": [
141
+ "components/separator/separator.ts",
142
+ "components/separator/separator.variants.ts",
143
+ "components/separator/index.ts"
144
+ ],
145
+ "dependencies": {
146
+ "class-variance-authority": "^0.7.1"
147
+ },
148
+ "registryDependencies": [
149
+ "utils/cn"
150
+ ]
151
+ },
152
+ "skeleton": {
153
+ "type": "component",
154
+ "files": [
155
+ "components/skeleton/skeleton.ts",
156
+ "components/skeleton/index.ts"
157
+ ],
158
+ "registryDependencies": [
159
+ "utils/cn"
160
+ ]
161
+ },
162
+ "switch": {
163
+ "type": "component",
164
+ "files": [
165
+ "components/switch/switch.ts",
166
+ "components/switch/index.ts"
167
+ ],
168
+ "peerDependencies": {
169
+ "@angular/forms": "^22.0.0"
170
+ },
171
+ "registryDependencies": [
172
+ "utils/cn",
173
+ "utils/control-styles",
174
+ "utils/field-state"
175
+ ]
176
+ },
177
+ "textarea": {
178
+ "type": "component",
179
+ "files": [
180
+ "components/textarea/textarea.ts",
181
+ "components/textarea/textarea.variants.ts",
182
+ "components/textarea/index.ts"
183
+ ],
184
+ "dependencies": {
185
+ "class-variance-authority": "^0.7.1"
186
+ },
187
+ "registryDependencies": [
188
+ "utils/cn",
189
+ "utils/control-styles",
190
+ "utils/field-state"
191
+ ]
192
+ }
193
+ }
194
+ }
@@ -0,0 +1,129 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ Directive,
5
+ ElementRef,
6
+ afterRenderEffect,
7
+ computed,
8
+ effect,
9
+ inject,
10
+ input,
11
+ signal,
12
+ untracked,
13
+ } from '@angular/core';
14
+ import { cn } from '@/components/ui/utils/cn';
15
+ import { type AvatarSize, type AvatarStatus, avatarVariants } from './avatar.variants';
16
+
17
+ /**
18
+ * Contenedor del avatar. Guarda el estado de la imagen para que la imagen y el fallback sepan
19
+ * cuál se ve:
20
+ *
21
+ * ```html
22
+ * <mimi-avatar>
23
+ * <img mimiAvatarImage src="/ana.png" alt="Ana Torres" />
24
+ * <mimi-avatar-fallback label="Ana Torres">AT</mimi-avatar-fallback>
25
+ * </mimi-avatar>
26
+ * ```
27
+ */
28
+ @Component({
29
+ selector: 'mimi-avatar',
30
+ template: '<ng-content />',
31
+ host: {
32
+ '[class]': 'classes()',
33
+ '[attr.data-size]': 'size()',
34
+ '[attr.data-state]': 'status()',
35
+ },
36
+ changeDetection: ChangeDetectionStrategy.OnPush,
37
+ })
38
+ export class MimiAvatar {
39
+ readonly size = input<AvatarSize>('default');
40
+ readonly userClass = input('', { alias: 'class' });
41
+
42
+ /** Estado de la imagen. Lo escribe `img[mimiAvatarImage]`; sin imagen queda en `idle`. */
43
+ readonly status = signal<AvatarStatus>('idle');
44
+
45
+ protected readonly classes = computed(() =>
46
+ cn(avatarVariants({ size: this.size() }), this.userClass()),
47
+ );
48
+ }
49
+
50
+ /**
51
+ * Foto del avatar sobre el `<img>` nativo, así `alt`, `srcset`, `loading`… funcionan tal cual.
52
+ * Queda invisible hasta que carga (sin salir del flujo, para que `loading="lazy"` funcione);
53
+ * si falla, sigue invisible y se ve el fallback. `ngSrc` (NgOptimizedImage) no está soportado.
54
+ */
55
+ @Directive({
56
+ selector: 'img[mimiAvatarImage]',
57
+ host: {
58
+ // Al declarar `src` como entrada, Angular ya no lo escribe en el elemento: se hace aquí.
59
+ '[attr.src]': 'src() || null',
60
+ '[class]': 'classes()',
61
+ '(load)': 'avatar.status.set("loaded")',
62
+ '(error)': 'avatar.status.set("error")',
63
+ },
64
+ })
65
+ export class MimiAvatarImage {
66
+ readonly src = input<string | null | undefined>();
67
+ readonly userClass = input('', { alias: 'class' });
68
+
69
+ protected readonly avatar = inject(MimiAvatar);
70
+ private readonly img = inject<ElementRef<HTMLImageElement>>(ElementRef).nativeElement;
71
+
72
+ protected readonly classes = computed(() =>
73
+ cn(
74
+ 'absolute inset-0 aspect-square size-full object-cover',
75
+ this.avatar.status() !== 'loaded' && 'invisible',
76
+ this.userClass(),
77
+ ),
78
+ );
79
+
80
+ constructor() {
81
+ // Cada `src` nuevo vuelve a cargar; sin `src`, se ve el fallback.
82
+ effect(() => {
83
+ const src = this.src();
84
+ untracked(() => this.avatar.status.set(src ? 'loading' : 'error'));
85
+ });
86
+ // Si la imagen ya estaba en caché, puede estar completa antes de escuchar `load`.
87
+ afterRenderEffect(() => {
88
+ if (!this.src()) return;
89
+ if (this.img.complete && this.img.naturalWidth > 0) {
90
+ untracked(() => this.avatar.status.set('loaded'));
91
+ }
92
+ });
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Iniciales o ícono que se ven mientras la imagen carga, si falla o si no hay imagen. Con
98
+ * `label`, los lectores de pantalla anuncian el nombre (`role="img"` + `aria-label`) en lugar
99
+ * de leer las letras sueltas.
100
+ */
101
+ @Component({
102
+ selector: 'mimi-avatar-fallback',
103
+ template: '<ng-content />',
104
+ host: {
105
+ '[class]': 'classes()',
106
+ '[attr.role]': 'label() ? "img" : null',
107
+ '[attr.aria-label]': 'label() || null',
108
+ },
109
+ changeDetection: ChangeDetectionStrategy.OnPush,
110
+ })
111
+ export class MimiAvatarFallback {
112
+ /** Nombre completo para lectores de pantalla. */
113
+ readonly label = input<string>();
114
+ readonly userClass = input('', { alias: 'class' });
115
+
116
+ private readonly avatar = inject(MimiAvatar);
117
+
118
+ protected readonly classes = computed(() =>
119
+ cn(
120
+ 'flex size-full items-center justify-center rounded-full bg-secondary font-semibold text-secondary-foreground',
121
+ this.userClass(),
122
+ // Al final: con la imagen cargada se oculta aunque el usuario pase `flex`.
123
+ this.avatar.status() === 'loaded' && 'hidden',
124
+ ),
125
+ );
126
+ }
127
+
128
+ /** Todas las piezas de Avatar, para importarlas juntas. */
129
+ export const MimiAvatarImports = [MimiAvatar, MimiAvatarImage, MimiAvatarFallback] as const;
@@ -0,0 +1,25 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+
3
+ /**
4
+ * Clases de mimi-avatar (docs/design/Mimi Componentes.dc.html, sección 6). Literales para que
5
+ * Tailwind las detecte. Círculo con fondo --mimi-muted mientras no hay foto; sm 32px / 12px,
6
+ * default 40px / 14px, lg 56px / 18px (el tamaño de letra lo heredan las iniciales).
7
+ */
8
+ export const avatarVariants = cva(
9
+ 'relative inline-flex shrink-0 overflow-hidden rounded-full bg-muted select-none',
10
+ {
11
+ variants: {
12
+ size: {
13
+ sm: 'size-8 text-xs',
14
+ default: 'size-10 text-sm',
15
+ lg: 'size-14 text-lg',
16
+ },
17
+ },
18
+ defaultVariants: { size: 'default' },
19
+ },
20
+ );
21
+
22
+ export type AvatarSize = NonNullable<VariantProps<typeof avatarVariants>['size']>;
23
+
24
+ /** Estado de la imagen: sin imagen, cargando, cargada o con error. */
25
+ export type AvatarStatus = 'idle' | 'loading' | 'loaded' | 'error';
@@ -0,0 +1,2 @@
1
+ export * from './avatar';
2
+ export * from './avatar.variants';
@@ -0,0 +1,21 @@
1
+ import { Directive, computed, input } from '@angular/core';
2
+ import { cn } from '@/components/ui/utils/cn';
3
+ import { type BadgeVariant, badgeVariants } from './badge.variants';
4
+
5
+ /** Etiqueta estática de Mimi sobre el `<span>` nativo: `<span mimiBadge>Nuevo</span>`. */
6
+ @Directive({
7
+ selector: 'span[mimiBadge]',
8
+ exportAs: 'mimiBadge',
9
+ host: {
10
+ '[class]': 'classes()',
11
+ '[attr.data-variant]': 'variant()',
12
+ },
13
+ })
14
+ export class MimiBadge {
15
+ readonly variant = input<BadgeVariant>('default');
16
+ readonly userClass = input('', { alias: 'class' });
17
+
18
+ protected readonly classes = computed(() =>
19
+ cn(badgeVariants({ variant: this.variant() }), this.userClass()),
20
+ );
21
+ }
@@ -0,0 +1,27 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+
3
+ /**
4
+ * Clases de mimiBadge (docs/design/Mimi Componentes.dc.html, sección 5). Literales para que
5
+ * Tailwind las detecte. 22px de alto, 10px de padding, 12px y peso 600, radio
6
+ * --mimi-badge-radius. Los íconos van a 12px (no están en el diseño; decidido en la tarea 2.4).
7
+ */
8
+ export const badgeVariants = cva(
9
+ [
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',
12
+ '[&_svg]:pointer-events-none [&_svg]:size-3 [&_svg]:shrink-0',
13
+ ],
14
+ {
15
+ 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
+ },
22
+ },
23
+ defaultVariants: { variant: 'default' },
24
+ },
25
+ );
26
+
27
+ export type BadgeVariant = NonNullable<VariantProps<typeof badgeVariants>['variant']>;
@@ -0,0 +1,2 @@
1
+ export * from './badge';
2
+ export * from './badge.variants';
@@ -0,0 +1,99 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ DestroyRef,
5
+ ElementRef,
6
+ booleanAttribute,
7
+ computed,
8
+ inject,
9
+ input,
10
+ } from '@angular/core';
11
+ import { cn } from '@/components/ui/utils/cn';
12
+ import { type ButtonSize, type ButtonVariant, buttonVariants } from './button.variants';
13
+
14
+ /**
15
+ * Botón de Mimi sobre el elemento nativo: `<button mimiBtn>` o `<a mimiBtn>`.
16
+ *
17
+ * - `disabled` o `loading` deshabilitan: `disabled` nativo en <button>; en <a>,
18
+ * `aria-disabled="true"`, `tabindex="-1"` y el clic se cancela.
19
+ * - `loading` muestra un spinner centrado encima del contenido, que queda transparente en su
20
+ * lugar: el botón no cambia de ancho y conserva su nombre accesible. Lleva `aria-busy="true"`.
21
+ */
22
+ @Component({
23
+ selector: 'button[mimiBtn], a[mimiBtn]',
24
+ exportAs: 'mimiBtn',
25
+ template: `
26
+ @if (loading()) {
27
+ <svg
28
+ data-slot="spinner"
29
+ class="absolute inset-0 m-auto animate-[mimi-spin_0.8s_linear_infinite]"
30
+ xmlns="http://www.w3.org/2000/svg"
31
+ width="16"
32
+ height="16"
33
+ viewBox="0 0 24 24"
34
+ fill="none"
35
+ stroke="currentColor"
36
+ stroke-width="2"
37
+ stroke-linecap="round"
38
+ stroke-linejoin="round"
39
+ aria-hidden="true"
40
+ >
41
+ <!-- Lucide loader-circle (ISC) -->
42
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
43
+ </svg>
44
+ }
45
+ <span data-slot="content" class="contents" [class.text-transparent]="loading()">
46
+ <ng-content />
47
+ </span>
48
+ `,
49
+ host: {
50
+ '[class]': 'classes()',
51
+ '[attr.data-variant]': 'variant()',
52
+ '[attr.data-size]': 'size()',
53
+ '[attr.data-loading]': 'loading() ? "" : null',
54
+ '[attr.data-disabled]': 'isDisabled() ? "" : null',
55
+ '[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',
59
+ },
60
+ changeDetection: ChangeDetectionStrategy.OnPush,
61
+ })
62
+ export class MimiButton {
63
+ readonly variant = input<ButtonVariant>('default');
64
+ readonly size = input<ButtonSize>('default');
65
+ readonly disabled = input(false, { transform: booleanAttribute });
66
+ readonly loading = input(false, { transform: booleanAttribute });
67
+ readonly userClass = input('', { alias: 'class' });
68
+
69
+ private readonly element = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
70
+ protected readonly isNativeButton = this.element.tagName === 'BUTTON';
71
+
72
+ protected readonly isDisabled = computed(() => this.disabled() || this.loading());
73
+
74
+ protected readonly classes = computed(() =>
75
+ cn(
76
+ buttonVariants({ variant: this.variant(), size: this.size() }),
77
+ // El diseño deshabilitado no tiene sombra; cargando la conserva.
78
+ this.disabled() && !this.loading() && 'shadow-none',
79
+ this.userClass(),
80
+ ),
81
+ );
82
+
83
+ /**
84
+ * En <a> deshabilitado cancela el clic (en <button> el navegador ya lo bloquea). Se escucha
85
+ * en la fase de captura: en el propio elemento corre antes que los (click) del usuario, que
86
+ * Angular registra primero, y así stopImmediatePropagation también los detiene.
87
+ */
88
+ private readonly blockDisabledClicks = (() => {
89
+ const onClick = (event: Event) => {
90
+ if (!this.isDisabled()) return;
91
+ event.preventDefault();
92
+ event.stopImmediatePropagation();
93
+ };
94
+ this.element.addEventListener('click', onClick, { capture: true });
95
+ inject(DestroyRef).onDestroy(() =>
96
+ this.element.removeEventListener('click', onClick, { capture: true }),
97
+ );
98
+ })();
99
+ }
@@ -0,0 +1,60 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+ import {
3
+ buttonFocusStyles,
4
+ controlDisabledStyles,
5
+ controlPressStyles,
6
+ } from '@/components/ui/utils/control-styles';
7
+
8
+ /**
9
+ * Clases de mimiBtn (docs/design/Mimi Componentes.dc.html, sección 1). Todas literales para
10
+ * 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.
12
+ */
13
+ export const buttonVariants = cva(
14
+ [
15
+ 'relative inline-flex shrink-0 cursor-pointer items-center justify-center whitespace-nowrap select-none',
16
+ 'rounded-[var(--mimi-btn-radius,var(--mimi-radius,0.75rem))]',
17
+ 'border-[length:var(--mimi-btn-border-width,1px)] border-transparent',
18
+ 'font-sans leading-none font-[weight:var(--mimi-btn-font-weight,500)] tracking-[var(--mimi-btn-letter-spacing,normal)]',
19
+ '[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
20
+ buttonFocusStyles,
21
+ 'focus-visible:outline-[length:var(--mimi-btn-focus-ring-width,2px)]',
22
+ controlDisabledStyles,
23
+ controlPressStyles,
24
+ // Cargando: deshabilitado, pero con opacidad 85% y cursor de progreso (gana a disabled:).
25
+ 'aria-busy:disabled:cursor-progress aria-busy:disabled:opacity-85 aria-busy:aria-disabled:opacity-85',
26
+ ],
27
+ {
28
+ variants: {
29
+ 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',
41
+ },
42
+ size: {
43
+ 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)]',
45
+ sm: 'h-[var(--mimi-btn-height-sm,var(--mimi-control-height-sm,2rem))] gap-1.5 px-3 text-[13px]',
46
+ lg: 'h-[var(--mimi-btn-height-lg,var(--mimi-control-height-lg,3rem))] gap-2 px-6 text-[15px]',
47
+ icon: 'size-[var(--mimi-btn-height,var(--mimi-control-height,2.5rem))] p-0',
48
+ },
49
+ },
50
+ compoundVariants: [
51
+ // El enlace del diseño solo tiene 4px de padding horizontal.
52
+ { variant: 'link', size: ['default', 'sm', 'lg'], class: 'px-1' },
53
+ ],
54
+ defaultVariants: { variant: 'default', size: 'default' },
55
+ },
56
+ );
57
+
58
+ type ButtonVariantProps = VariantProps<typeof buttonVariants>;
59
+ export type ButtonVariant = NonNullable<ButtonVariantProps['variant']>;
60
+ export type ButtonSize = NonNullable<ButtonVariantProps['size']>;
@@ -0,0 +1,2 @@
1
+ export * from './button';
2
+ export * from './button.variants';