@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.
- package/README.md +14 -1
- package/dist/init/css.js +17 -0
- package/dist/init/index.js +7 -5
- package/dist/init/items.js +9 -0
- package/dist/registry.json +222 -1
- package/dist/templates/components/avatar/avatar.ts +1 -1
- package/dist/templates/components/avatar/avatar.variants.ts +4 -4
- package/dist/templates/components/badge/badge.ts +41 -6
- package/dist/templates/components/badge/badge.variants.ts +105 -11
- package/dist/templates/components/button/button.ts +74 -7
- package/dist/templates/components/button/button.variants.ts +145 -21
- package/dist/templates/components/card/card.ts +8 -5
- package/dist/templates/components/checkbox/checkbox.ts +3 -3
- package/dist/templates/components/confirm/confirm-texts.ts +46 -0
- package/dist/templates/components/confirm/confirm.ts +190 -0
- package/dist/templates/components/confirm/index.ts +2 -0
- package/dist/templates/components/context-menu/context-menu.ts +378 -0
- package/dist/templates/components/context-menu/index.ts +14 -0
- package/dist/templates/components/dialog/dialog-texts.ts +41 -0
- package/dist/templates/components/dialog/dialog.ts +701 -0
- package/dist/templates/components/dialog/index.ts +18 -0
- package/dist/templates/components/dropdown-menu/dropdown-menu.ts +252 -0
- package/dist/templates/components/dropdown-menu/index.ts +14 -0
- package/dist/templates/components/form-field/form-error.ts +1 -1
- package/dist/templates/components/form-field/form-field.ts +3 -2
- package/dist/templates/components/input/input.variants.ts +6 -4
- package/dist/templates/components/menu/index.ts +15 -0
- package/dist/templates/components/menu/menu.ts +656 -0
- package/dist/templates/components/pagination/index.ts +2 -0
- package/dist/templates/components/pagination/pagination-page-size.ts +74 -0
- package/dist/templates/components/pagination/pagination-texts.ts +69 -0
- package/dist/templates/components/pagination/pagination.ts +443 -0
- package/dist/templates/components/popover/index.ts +12 -0
- package/dist/templates/components/popover/popover.ts +399 -0
- package/dist/templates/components/select/index.ts +12 -0
- package/dist/templates/components/select/select-texts.ts +62 -0
- package/dist/templates/components/select/select.ts +1085 -0
- package/dist/templates/components/separator/separator.variants.ts +10 -7
- package/dist/templates/components/skeleton/skeleton.ts +4 -1
- package/dist/templates/components/switch/switch.ts +6 -6
- package/dist/templates/components/tabs/index.ts +2 -0
- package/dist/templates/components/tabs/tabs.ts +426 -0
- package/dist/templates/components/textarea/textarea.variants.ts +14 -11
- package/dist/templates/components/toast/index.ts +3 -0
- package/dist/templates/components/toast/toast-config.ts +45 -0
- package/dist/templates/components/toast/toast-texts.ts +46 -0
- package/dist/templates/components/toast/toast.ts +1000 -0
- package/dist/templates/components/toolbar/index.ts +1 -0
- package/dist/templates/components/toolbar/toolbar.ts +377 -0
- package/dist/templates/components/tooltip/index.ts +7 -0
- package/dist/templates/components/tooltip/tooltip.ts +407 -0
- package/dist/templates/theme/provider.ts +62 -6
- package/dist/templates/theme/theme-base.css +307 -9
- package/dist/templates/theme/types.ts +278 -4
- package/dist/templates/utils/cn.ts +15 -1
- package/dist/templates/utils/control-styles.ts +2 -2
- package/dist/templates/utils/overlay.ts +140 -0
- package/dist/ui/index.js +80 -8
- package/dist/ui/schema.json +44 -0
- package/dist/utils/write-files.js +156 -59
- package/package.json +1 -1
|
@@ -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 {
|
|
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]': '
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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-
|
|
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: '
|
|
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
|
+
}
|
|
@@ -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.
|
|
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(
|
|
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,
|
|
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-
|
|
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,46 @@
|
|
|
1
|
+
import { InjectionToken, type Provider } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Textos de Confirm, en archivo propio y sin @angular/cdk: así se pueden proveer en
|
|
5
|
+
* `app.config.ts` sin llevar el Dialog al chunk inicial.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Textos de los botones de Confirm. */
|
|
9
|
+
export interface MimiConfirmText {
|
|
10
|
+
/** Botón que confirma. */
|
|
11
|
+
confirm: string;
|
|
12
|
+
/** Botón que cancela. */
|
|
13
|
+
cancel: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Textos predeterminados (inglés). */
|
|
17
|
+
export const MIMI_CONFIRM_TEXT_EN: MimiConfirmText = {
|
|
18
|
+
confirm: 'Confirm',
|
|
19
|
+
cancel: 'Cancel',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Textos en español: `provideMimiConfirmText(MIMI_CONFIRM_TEXT_ES)`. */
|
|
23
|
+
export const MIMI_CONFIRM_TEXT_ES: MimiConfirmText = {
|
|
24
|
+
confirm: 'Confirmar',
|
|
25
|
+
cancel: 'Cancelar',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Textos activos. Sin `provideMimiConfirmText`, los predeterminados en inglés. */
|
|
29
|
+
export const MIMI_CONFIRM_TEXT = new InjectionToken<MimiConfirmText>('MIMI_CONFIRM_TEXT', {
|
|
30
|
+
providedIn: 'root',
|
|
31
|
+
factory: () => MIMI_CONFIRM_TEXT_EN,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Cambia los textos de Confirm. Lo que no definas sigue en inglés:
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* provideMimiConfirmText(MIMI_CONFIRM_TEXT_ES)
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* Va en `app.config.ts` o en los `providers` de un componente o ruta. `confirmText` y
|
|
42
|
+
* `cancelText` de cada llamada ganan sobre estos.
|
|
43
|
+
*/
|
|
44
|
+
export function provideMimiConfirmText(text: Partial<MimiConfirmText>): Provider {
|
|
45
|
+
return { provide: MIMI_CONFIRM_TEXT, useValue: { ...MIMI_CONFIRM_TEXT_EN, ...text } };
|
|
46
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
DestroyRef,
|
|
5
|
+
Directive,
|
|
6
|
+
Injector,
|
|
7
|
+
TemplateRef,
|
|
8
|
+
ViewContainerRef,
|
|
9
|
+
assertInInjectionContext,
|
|
10
|
+
effect,
|
|
11
|
+
inject,
|
|
12
|
+
input,
|
|
13
|
+
untracked,
|
|
14
|
+
} from '@angular/core';
|
|
15
|
+
import { MimiButton } from '@/components/ui/button';
|
|
16
|
+
import {
|
|
17
|
+
MIMI_DIALOG_DATA,
|
|
18
|
+
MimiDialogDescription,
|
|
19
|
+
MimiDialogRef,
|
|
20
|
+
MimiDialogService,
|
|
21
|
+
MimiDialogTitle,
|
|
22
|
+
} from '@/components/ui/dialog';
|
|
23
|
+
import { cn } from '@/components/ui/utils/cn';
|
|
24
|
+
import { MIMI_CONFIRM_TEXT } from './confirm-texts';
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* Confirm (docs/design/Mimi F2 Overlays.dc.html, «2 · Confirm»; prefijo de tokens `confirm`,
|
|
28
|
+
* spec 13): un alertdialog de 380px, sin X, con un círculo de 40px para el ícono, el título, la
|
|
29
|
+
* descripción y dos botones de 36px a la derecha.
|
|
30
|
+
*
|
|
31
|
+
* Por dentro abre un Dialog (MimiDialogService): el foco, el fondo, Escape, el bloqueo del scroll
|
|
32
|
+
* y la animación son suyos. Confirm solo pone el contenido y traduce el cierre a true o false.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** Opciones de `confirm()`. */
|
|
36
|
+
export interface MimiConfirmOptions {
|
|
37
|
+
/** La pregunta. Es el nombre accesible del diálogo. */
|
|
38
|
+
title: string;
|
|
39
|
+
/** Qué pasa si se confirma. Recomendada: un alertdialog la anuncia junto al título. */
|
|
40
|
+
description?: string;
|
|
41
|
+
/** Texto del botón que confirma. Por defecto, el de `MIMI_CONFIRM_TEXT`. */
|
|
42
|
+
confirmText?: string;
|
|
43
|
+
/** Texto del botón que cancela. Por defecto, el de `MIMI_CONFIRM_TEXT`. */
|
|
44
|
+
cancelText?: string;
|
|
45
|
+
/**
|
|
46
|
+
* `danger` para una acción destructiva: el botón de confirmar va en rojo y el foco inicial, en
|
|
47
|
+
* Cancelar. Por defecto, `primary` (el foco inicial va en Confirmar).
|
|
48
|
+
*/
|
|
49
|
+
tone?: 'primary' | 'danger';
|
|
50
|
+
/** Ícono dentro del círculo de color: una plantilla con un `<svg>`. Sin él, no hay círculo. */
|
|
51
|
+
icon?: TemplateRef<unknown>;
|
|
52
|
+
/** Clases del panel, mezcladas con `cn()` (gana la del usuario). */
|
|
53
|
+
class?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** La función que devuelve `injectMimiConfirm()`. */
|
|
57
|
+
export type MimiConfirmFn = (options: MimiConfirmOptions) => Promise<boolean>;
|
|
58
|
+
|
|
59
|
+
const ICON_CLASSES = {
|
|
60
|
+
primary: 'bg-info-soft text-info-soft-foreground',
|
|
61
|
+
danger:
|
|
62
|
+
'bg-[color:var(--mimi-confirm-icon-bg,var(--mimi-destructive-soft-bg))] text-[color:var(--mimi-confirm-icon-fg,var(--mimi-destructive-soft-foreground))]',
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
/** Crea la plantilla del ícono. Hace lo mismo que NgTemplateOutlet, sin @angular/common. */
|
|
66
|
+
@Directive({ selector: '[mimiConfirmIcon]' })
|
|
67
|
+
class MimiConfirmIcon {
|
|
68
|
+
readonly template = input.required<TemplateRef<unknown>>({ alias: 'mimiConfirmIcon' });
|
|
69
|
+
private readonly viewContainer = inject(ViewContainerRef);
|
|
70
|
+
|
|
71
|
+
private readonly render = effect(() => {
|
|
72
|
+
const template = this.template();
|
|
73
|
+
untracked(() => {
|
|
74
|
+
this.viewContainer.clear();
|
|
75
|
+
this.viewContainer.createEmbeddedView(template);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** El contenido del diálogo: ícono, título, descripción y los dos botones. */
|
|
81
|
+
@Component({
|
|
82
|
+
selector: 'mimi-confirm',
|
|
83
|
+
imports: [MimiButton, MimiDialogTitle, MimiDialogDescription, MimiConfirmIcon],
|
|
84
|
+
template: `
|
|
85
|
+
<div class="flex items-start gap-3.5">
|
|
86
|
+
@if (options.icon) {
|
|
87
|
+
<span aria-hidden="true" data-slot="icon" [class]="iconClasses">
|
|
88
|
+
<ng-container [mimiConfirmIcon]="options.icon" />
|
|
89
|
+
</span>
|
|
90
|
+
}
|
|
91
|
+
<div class="flex min-w-0 flex-col gap-1">
|
|
92
|
+
<h2 mimiDialogTitle class="text-base leading-[1.3] tracking-normal">
|
|
93
|
+
{{ options.title }}
|
|
94
|
+
</h2>
|
|
95
|
+
@if (options.description) {
|
|
96
|
+
<p mimiDialogDescription class="text-[13px] leading-normal text-pretty">
|
|
97
|
+
{{ options.description }}
|
|
98
|
+
</p>
|
|
99
|
+
}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<!-- Los botones miden 36px de alto con 14px a los lados, como en el diseño. -->
|
|
103
|
+
<div class="flex justify-end gap-2">
|
|
104
|
+
<button
|
|
105
|
+
mimiBtn
|
|
106
|
+
type="button"
|
|
107
|
+
variant="outline"
|
|
108
|
+
data-slot="cancel"
|
|
109
|
+
class="h-9 px-3.5"
|
|
110
|
+
(click)="ref.close(false)"
|
|
111
|
+
>
|
|
112
|
+
{{ options.cancelText ?? text.cancel }}
|
|
113
|
+
</button>
|
|
114
|
+
<button
|
|
115
|
+
mimiBtn
|
|
116
|
+
type="button"
|
|
117
|
+
[tone]="tone"
|
|
118
|
+
data-slot="confirm"
|
|
119
|
+
class="h-9 px-3.5"
|
|
120
|
+
(click)="ref.close(true)"
|
|
121
|
+
>
|
|
122
|
+
{{ options.confirmText ?? text.confirm }}
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
`,
|
|
126
|
+
host: { '[attr.data-tone]': 'tone' },
|
|
127
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
128
|
+
})
|
|
129
|
+
class MimiConfirmContent {
|
|
130
|
+
protected readonly options = inject<MimiConfirmOptions>(MIMI_DIALOG_DATA);
|
|
131
|
+
protected readonly ref = inject<MimiDialogRef<boolean>>(MimiDialogRef);
|
|
132
|
+
protected readonly text = inject(MIMI_CONFIRM_TEXT);
|
|
133
|
+
|
|
134
|
+
protected readonly tone = this.options.tone ?? 'primary';
|
|
135
|
+
protected readonly iconClasses = cn(
|
|
136
|
+
'inline-flex size-10 shrink-0 items-center justify-center rounded-full [&_svg]:size-5 [&_svg]:shrink-0',
|
|
137
|
+
ICON_CLASSES[this.tone],
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Prepara `confirm()`, que pregunta y espera la respuesta:
|
|
143
|
+
*
|
|
144
|
+
* ```ts
|
|
145
|
+
* private readonly confirm = injectMimiConfirm();
|
|
146
|
+
*
|
|
147
|
+
* async remove() {
|
|
148
|
+
* if (await this.confirm({ title: '¿Eliminar este proyecto?', tone: 'danger' })) { … }
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* Se llama al declarar la propiedad (o en el constructor): necesita el contexto de inyección.
|
|
153
|
+
* Así toma el injector del componente y los textos de `provideMimiConfirmText` de su ruta o del
|
|
154
|
+
* propio componente. Dentro de un método falla con NG0203.
|
|
155
|
+
*
|
|
156
|
+
* `confirm()` resuelve `true` si se confirmó y `false` en cualquier otro caso (Cancelar, Escape,
|
|
157
|
+
* una navegación, `closeAll()` o el componente que se destruye), después de la animación de
|
|
158
|
+
* salida y con el foco ya devuelto. Nunca rechaza: si el diálogo no se pudo abrir, el error va al
|
|
159
|
+
* `ErrorHandler` y la respuesta es `false`. El clic en el fondo no cierra.
|
|
160
|
+
*
|
|
161
|
+
* Como todo Dialog, se abre después del render en curso: desde un ítem de menú, el foco vuelve
|
|
162
|
+
* al botón del menú al cerrar.
|
|
163
|
+
*/
|
|
164
|
+
export function injectMimiConfirm(): MimiConfirmFn {
|
|
165
|
+
assertInInjectionContext(injectMimiConfirm);
|
|
166
|
+
const dialog = inject(MimiDialogService);
|
|
167
|
+
const injector = inject(Injector);
|
|
168
|
+
const destroyRef = inject(DestroyRef);
|
|
169
|
+
|
|
170
|
+
return (options) =>
|
|
171
|
+
new Promise<boolean>((resolve) => {
|
|
172
|
+
const ref = dialog.open<boolean, MimiConfirmOptions>(MimiConfirmContent, {
|
|
173
|
+
data: options,
|
|
174
|
+
role: 'alertdialog',
|
|
175
|
+
closeButton: false,
|
|
176
|
+
closeOnBackdropClick: false,
|
|
177
|
+
// Lo más seguro primero: en una acción destructiva, Enter no confirma por accidente.
|
|
178
|
+
autoFocus: options.tone === 'danger' ? '[data-slot="cancel"]' : '[data-slot="confirm"]',
|
|
179
|
+
// Por clase y no con `width`: así la clase del usuario puede cambiar el ancho.
|
|
180
|
+
class: cn('w-[var(--mimi-confirm-width,380px)] gap-3.5 p-5', options.class),
|
|
181
|
+
injector,
|
|
182
|
+
});
|
|
183
|
+
// Si el componente que pregunta se destruye antes de la respuesta, se cierra con false.
|
|
184
|
+
const stop = destroyRef.onDestroy(() => ref.close(false));
|
|
185
|
+
ref.afterClosed().subscribe((result) => {
|
|
186
|
+
stop();
|
|
187
|
+
resolve(result === true);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|