@mimi-ng/cli 0.4.0 → 0.5.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 (35) hide show
  1. package/README.md +29 -7
  2. package/dist/init/index.js +8 -0
  3. package/dist/registry.js +4 -1
  4. package/dist/registry.json +87 -1
  5. package/dist/templates/blocks/login-01/login-01.ts +12 -3
  6. package/dist/templates/blocks/login-02/login-02.ts +12 -3
  7. package/dist/templates/blocks/otp-01/otp-01.ts +12 -3
  8. package/dist/templates/blocks/pricing-01/pricing-01.ts +1 -1
  9. package/dist/templates/blocks/settings-01/settings-01.ts +13 -3
  10. package/dist/templates/blocks/signup-01/signup-01.ts +12 -2
  11. package/dist/templates/blocks/table-01/index.ts +1 -0
  12. package/dist/templates/blocks/table-01/table-01.html +214 -0
  13. package/dist/templates/blocks/table-01/table-01.ts +266 -0
  14. package/dist/templates/components/checkbox/checkbox.ts +14 -5
  15. package/dist/templates/components/command/command-texts.ts +65 -0
  16. package/dist/templates/components/command/command.ts +553 -0
  17. package/dist/templates/components/command/index.ts +13 -0
  18. package/dist/templates/components/data-table/data-table-texts.ts +110 -0
  19. package/dist/templates/components/data-table/data-table.ts +1153 -0
  20. package/dist/templates/components/data-table/index.ts +18 -0
  21. package/dist/templates/components/sortable-list/index.ts +9 -0
  22. package/dist/templates/components/sortable-list/sortable-list-texts.ts +71 -0
  23. package/dist/templates/components/sortable-list/sortable-list.ts +510 -0
  24. package/dist/templates/components/tree/index.ts +9 -0
  25. package/dist/templates/components/tree/tree-texts.ts +45 -0
  26. package/dist/templates/components/tree/tree.ts +730 -0
  27. package/dist/templates/theme/provider.ts +4 -0
  28. package/dist/templates/theme/types.ts +84 -0
  29. package/dist/ui/index.js +232 -40
  30. package/dist/ui/schema.json +30 -6
  31. package/dist/utils/pages.js +138 -0
  32. package/dist/utils/project.js +33 -9
  33. package/dist/utils/tailwind.js +15 -11
  34. package/dist/utils/write-files.js +32 -7
  35. package/package.json +1 -1
@@ -0,0 +1,266 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ LOCALE_ID,
5
+ computed,
6
+ inject,
7
+ input,
8
+ model,
9
+ output,
10
+ } from '@angular/core';
11
+ import { MimiBadge, type BadgeTone } from '@/components/ui/badge';
12
+ import { MimiButton } from '@/components/ui/button';
13
+ import { MimiCheckbox } from '@/components/ui/checkbox';
14
+ import {
15
+ MimiDataTableImports,
16
+ type MimiTableColumn,
17
+ type MimiTableFilterOption,
18
+ } from '@/components/ui/data-table';
19
+ import { MimiInput } from '@/components/ui/input';
20
+ import { cn } from '@/components/ui/utils/cn';
21
+
22
+ export type Table01Status = 'active' | 'trial' | 'past-due' | 'canceled';
23
+
24
+ export interface Table01Customer {
25
+ /** Identificador estable (sale en `deleted` y `exported`). */
26
+ id: number;
27
+ name: string;
28
+ email: string;
29
+ status: Table01Status;
30
+ plan: string;
31
+ /** Ingreso mensual recurrente. */
32
+ mrr: number;
33
+ users: number;
34
+ country: string;
35
+ }
36
+
37
+ /** Cuenta de cada estado en el resumen y en las píldoras de la lista móvil. */
38
+ const STATUSES: { id: Table01Status; label: string; plural: string; tone: BadgeTone }[] = [
39
+ { id: 'active', label: 'Active', plural: 'Active', tone: 'success' },
40
+ { id: 'trial', label: 'Trial', plural: 'Trial', tone: 'info' },
41
+ { id: 'past-due', label: 'Past due', plural: 'Past due', tone: 'warning' },
42
+ { id: 'canceled', label: 'Canceled', plural: 'Canceled', tone: 'danger' },
43
+ ];
44
+
45
+ const NAMES = [
46
+ 'Hotel Brisa',
47
+ 'Café Altura',
48
+ 'Taller Motor',
49
+ 'Grupo Talento',
50
+ 'Óptica Clara',
51
+ 'Editorial Faro',
52
+ 'Clínica Sol',
53
+ 'Panadería Trigo',
54
+ 'Estudio Norte',
55
+ ];
56
+ const PLANS = ['Starter', 'Team', 'Enterprise'];
57
+ const COUNTRIES = ['Colombia', 'México', 'Chile', 'Perú'];
58
+ const SEQUENCE: Table01Status[] = ['active', 'trial', 'active', 'past-due', 'active', 'canceled'];
59
+
60
+ /** 36 clientes de ejemplo, siempre los mismos, para que la pantalla funcione sin datos. */
61
+ export const TABLE_01_CUSTOMERS: Table01Customer[] = Array.from({ length: 36 }, (_, i) => {
62
+ const name = `${NAMES[i % NAMES.length]} ${Math.floor(i / NAMES.length) + 1}`;
63
+ return {
64
+ id: i + 1,
65
+ name,
66
+ email: `hello@${name
67
+ .normalize('NFD')
68
+ .replace(/[̀-ͯ]/g, '')
69
+ .toLowerCase()
70
+ .replace(/[^a-z0-9]+/g, '-')}.com`,
71
+ status: SEQUENCE[i % SEQUENCE.length],
72
+ plan: PLANS[i % PLANS.length],
73
+ mrr: 300 + ((i * 7919) % 180) * 40,
74
+ users: 2 + ((i * 13) % 60),
75
+ country: COUNTRIES[i % COUNTRIES.length],
76
+ };
77
+ });
78
+
79
+ /**
80
+ * table-01 (Block de Mimi): la pantalla «Clientes». En escritorio, un resumen por estado y la
81
+ * Data Table de Mimi (buscar, filtrar por estado o plan, ordenar, paginar y elegir filas con sus
82
+ * acciones); en móvil, una lista de tarjetas con búsqueda, píldoras de estado y una barra de
83
+ * acciones. Diseño: docs/design/Block table-01.dc.html.
84
+ *
85
+ * Es una página: `ng g mimi table-01` la genera en tu carpeta de páginas (`aliases.pages` de
86
+ * mimi.json; por defecto, `src/app/pages/table/`, u otro nombre con `--name`) junto con su
87
+ * archivo de rutas (`table.routes.ts`, con la página en carga diferida), y te da la línea para
88
+ * `app.routes.ts`:
89
+ *
90
+ * ```ts
91
+ * { path: 'table', loadChildren: () => import('./pages/table/table.routes').then((m) => m.TABLE_ROUTES) },
92
+ * ```
93
+ *
94
+ * El archivo es tuyo: da tus clientes con `customers` (sin ellos, los de ejemplo), conecta
95
+ * «New customer», «Import CSV», «Export» y «Delete» a tu servicio con sus salidas, o cambia las
96
+ * columnas. Con muchos clientes, usa la Data Table en modo servidor (`[total]`).
97
+ *
98
+ * ```html
99
+ * <mimi-table-01 [customers]="customers" (deleted)="remove($event)" (newCustomer)="create()" />
100
+ * ```
101
+ */
102
+ @Component({
103
+ selector: 'mimi-table-01',
104
+ imports: [MimiBadge, MimiButton, MimiCheckbox, MimiDataTableImports, MimiInput],
105
+ templateUrl: './table-01.html',
106
+ host: { '[class]': 'classes()' },
107
+ changeDetection: ChangeDetectionStrategy.OnPush,
108
+ })
109
+ export class Table01 {
110
+ readonly customers = input<readonly Table01Customer[]>(TABLE_01_CUSTOMERS);
111
+ /** Moneda del ingreso mensual (ISO 4217). */
112
+ readonly currency = input('USD');
113
+ /** Idioma de los números. Por defecto, el `LOCALE_ID` de la app. */
114
+ readonly locale = input<string | undefined>(undefined);
115
+ /** Ids de las filas elegidas. Admite `[(selection)]`. */
116
+ readonly selection = model<unknown[]>([]);
117
+ readonly userClass = input('', { alias: 'class' });
118
+
119
+ /** «New customer». */
120
+ readonly newCustomer = output<void>();
121
+ /** «Import CSV» (solo en escritorio). */
122
+ readonly importCsv = output<void>();
123
+ /** «Export» con filas elegidas: sus ids. */
124
+ readonly exported = output<unknown[]>();
125
+ /** «Delete» con filas elegidas: sus ids. La pantalla no las quita: lo haces tú en `customers`. */
126
+ readonly deleted = output<unknown[]>();
127
+
128
+ private readonly appLocale = inject(LOCALE_ID);
129
+ protected readonly statuses = STATUSES;
130
+
131
+ /** La búsqueda y el estado elegido de la lista móvil. */
132
+ protected readonly query = model('');
133
+ protected readonly statusFilter = model<Table01Status | null>(null);
134
+
135
+ private readonly money = computed(
136
+ () =>
137
+ new Intl.NumberFormat(this.locale() ?? this.appLocale, {
138
+ style: 'currency',
139
+ currency: this.currency(),
140
+ maximumFractionDigits: 0,
141
+ }),
142
+ );
143
+
144
+ protected format(amount: number): string {
145
+ return this.money().format(amount);
146
+ }
147
+
148
+ /** El punto de color de cada estado (clases literales para Tailwind). */
149
+ protected dotClass(tone: BadgeTone): string {
150
+ return (
151
+ {
152
+ success: 'bg-success',
153
+ info: 'bg-info',
154
+ warning: 'bg-warning',
155
+ danger: 'bg-destructive',
156
+ primary: 'bg-primary',
157
+ secondary: 'bg-secondary',
158
+ } satisfies Record<BadgeTone, string>
159
+ )[tone];
160
+ }
161
+
162
+ protected statusOf(status: Table01Status) {
163
+ return STATUSES.find((s) => s.id === status)!;
164
+ }
165
+
166
+ /** Cuántos clientes hay de cada estado. */
167
+ protected readonly counts = computed(() =>
168
+ STATUSES.map((s) => ({
169
+ ...s,
170
+ count: this.customers().filter((c) => c.status === s.id).length,
171
+ })),
172
+ );
173
+
174
+ protected readonly subtitle = computed(() => {
175
+ const customers = this.customers();
176
+ const mrr = customers.reduce((sum, c) => sum + c.mrr, 0);
177
+ return `${customers.length} accounts · ${this.format(mrr)} MRR`;
178
+ });
179
+
180
+ protected readonly columns = computed<MimiTableColumn<Table01Customer>[]>(() => {
181
+ const options = (values: string[], label: (v: string) => string): MimiTableFilterOption[] =>
182
+ values.map((value) => ({ value, label: label(value) }));
183
+ return [
184
+ { id: 'name', header: 'Customer', sortable: true, sticky: true, width: '280px' },
185
+ {
186
+ id: 'status',
187
+ header: 'Status',
188
+ width: '130px',
189
+ filterOptions: options(
190
+ STATUSES.map((s) => s.id),
191
+ (id) => this.statusOf(id as Table01Status).label,
192
+ ),
193
+ value: (row) => row.status,
194
+ },
195
+ {
196
+ id: 'plan',
197
+ header: 'Plan',
198
+ width: '130px',
199
+ filterOptions: options(PLANS, (p) => p),
200
+ },
201
+ {
202
+ id: 'mrr',
203
+ header: 'MRR',
204
+ sortable: true,
205
+ align: 'end',
206
+ width: '120px',
207
+ searchable: false,
208
+ },
209
+ {
210
+ id: 'users',
211
+ header: 'Users',
212
+ sortable: true,
213
+ align: 'end',
214
+ width: '100px',
215
+ searchable: false,
216
+ },
217
+ { id: 'country', header: 'Country', sortable: true, width: '140px' },
218
+ ];
219
+ });
220
+
221
+ /** La lista móvil: la búsqueda (sin tildes) y el estado elegido. */
222
+ protected readonly list = computed(() => {
223
+ const fold = (t: string) => t.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
224
+ const q = fold(this.query().trim());
225
+ const status = this.statusFilter();
226
+ return this.customers().filter(
227
+ (c) => (!status || c.status === status) && (!q || fold(c.name).includes(q)),
228
+ );
229
+ });
230
+
231
+ protected readonly listIds = computed(() => this.list().map((c) => c.id));
232
+ protected readonly allState = computed<'all' | 'some' | 'none'>(() => {
233
+ const ids = this.listIds();
234
+ const n = ids.filter((id) => this.selection().includes(id)).length;
235
+ return n === 0 ? 'none' : n === ids.length ? 'all' : 'some';
236
+ });
237
+
238
+ protected readonly classes = computed(() =>
239
+ cn('flex w-full flex-col bg-background text-foreground', this.userClass()),
240
+ );
241
+
242
+ protected isSelected(id: number): boolean {
243
+ return this.selection().includes(id);
244
+ }
245
+
246
+ protected toggle(id: number): void {
247
+ this.selection.update((ids) => (ids.includes(id) ? ids.filter((x) => x !== id) : [...ids, id]));
248
+ }
249
+
250
+ protected toggleAll(checked: boolean): void {
251
+ const ids = this.listIds();
252
+ this.selection.update((sel) =>
253
+ checked
254
+ ? [...sel, ...ids.filter((id) => !sel.includes(id))]
255
+ : sel.filter((id) => !ids.includes(id as number)),
256
+ );
257
+ }
258
+
259
+ protected pickStatus(status: Table01Status | null): void {
260
+ this.statusFilter.set(this.statusFilter() === status ? null : status);
261
+ }
262
+
263
+ protected selectedText(count: number): string {
264
+ return count === 1 ? '1 selected' : `${count} selected`;
265
+ }
266
+ }
@@ -20,6 +20,18 @@ import {
20
20
 
21
21
  let nextId = 0;
22
22
 
23
+ /**
24
+ * La caja de la casilla (16px; marcada o mixta, `primary` con su sombra), según su
25
+ * `data-state="checked | indeterminate | unchecked"`. La usa también el Tree para dibujar sus
26
+ * casillas sin un control enfocable dentro del nodo (plan, G3.1-d).
27
+ */
28
+ export const MIMI_CHECKBOX_BOX_STYLES = cn(
29
+ '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))]',
30
+ 'mimi-transition active:scale-(--mimi-press-scale-sm)',
31
+ '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',
32
+ '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',
33
+ );
34
+
23
35
  /**
24
36
  * Casilla de verificación (docs/design/Mimi Componentes.dc.html, sección 7):
25
37
  * `<mimi-checkbox [(checked)]="recordarme">Recordarme</mimi-checkbox>`.
@@ -148,12 +160,9 @@ export class MimiCheckbox implements FormCheckboxControl, MimiFieldControl {
148
160
  cn('inline-flex items-center gap-2.5 text-sm', this.userClass()),
149
161
  );
150
162
 
151
- // Caja de 16px. Marcada o mixta: primary con su sombra. Foco: borde ring (sin marcar) y contorno.
152
163
  protected readonly boxClasses = cn(
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
- 'mimi-transition active:scale-(--mimi-press-scale-sm)',
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',
164
+ 'peer',
165
+ MIMI_CHECKBOX_BOX_STYLES,
157
166
  'data-[state=unchecked]:focus-visible:border-ring data-[state=unchecked]:aria-invalid:border-destructive',
158
167
  'disabled:shadow-none',
159
168
  buttonFocusStyles,
@@ -0,0 +1,65 @@
1
+ import { InjectionToken, type Provider } from '@angular/core';
2
+
3
+ /*
4
+ * Textos de Command, en archivo propio y sin @angular/aria ni el cdk: así se pueden proveer en
5
+ * `app.config.ts` sin llevar la paleta al chunk inicial.
6
+ */
7
+
8
+ /** Textos que la paleta muestra o anuncia. */
9
+ export interface MimiCommandText {
10
+ /** Texto del buscador vacío. */
11
+ placeholder: string;
12
+ /** Nombre del diálogo y de la lista, si no se da `aria-label`. */
13
+ label: string;
14
+ /** Ayuda del pie: las flechas. */
15
+ navigate: string;
16
+ /** Ayuda del pie: Enter. */
17
+ run: string;
18
+ /** Número de resultados (pie y región `status`). */
19
+ results: (count: number) => string;
20
+ /** Título de «sin resultados». */
21
+ noResults: (query: string) => string;
22
+ /** Texto de «sin resultados». */
23
+ noResultsHint: string;
24
+ }
25
+
26
+ /** Textos predeterminados (inglés). */
27
+ export const MIMI_COMMAND_TEXT_EN: MimiCommandText = {
28
+ placeholder: 'Type a command or search…',
29
+ label: 'Command palette',
30
+ navigate: 'Navigate',
31
+ run: 'Open',
32
+ results: (count) => (count === 1 ? '1 result' : `${count} results`),
33
+ noResults: (query) => `No results for “${query}”`,
34
+ noResultsHint: 'Check the spelling or try another word.',
35
+ };
36
+
37
+ /** Textos en español: `provideMimiCommandText(MIMI_COMMAND_TEXT_ES)`. */
38
+ export const MIMI_COMMAND_TEXT_ES: MimiCommandText = {
39
+ placeholder: 'Escribe un comando o busca…',
40
+ label: 'Paleta de comandos',
41
+ navigate: 'Navegar',
42
+ run: 'Abrir',
43
+ results: (count) => (count === 1 ? '1 resultado' : `${count} resultados`),
44
+ noResults: (query) => `Sin resultados para «${query}»`,
45
+ noResultsHint: 'Revisa la ortografía o prueba con otra palabra.',
46
+ };
47
+
48
+ /** Textos activos. Sin `provideMimiCommandText`, los predeterminados en inglés. */
49
+ export const MIMI_COMMAND_TEXT = new InjectionToken<MimiCommandText>('MIMI_COMMAND_TEXT', {
50
+ providedIn: 'root',
51
+ factory: () => MIMI_COMMAND_TEXT_EN,
52
+ });
53
+
54
+ /**
55
+ * Cambia los textos de Command. Lo que no definas sigue en inglés:
56
+ *
57
+ * ```ts
58
+ * provideMimiCommandText(MIMI_COMMAND_TEXT_ES)
59
+ * ```
60
+ *
61
+ * Va en `app.config.ts` o en los `providers` de un componente o ruta.
62
+ */
63
+ export function provideMimiCommandText(text: Partial<MimiCommandText>): Provider {
64
+ return { provide: MIMI_COMMAND_TEXT, useValue: { ...MIMI_COMMAND_TEXT_EN, ...text } };
65
+ }