@mimi-ng/cli 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -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 +482 -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/calendar/calendar-texts.ts +72 -0
- package/dist/templates/components/calendar/calendar.ts +588 -0
- package/dist/templates/components/calendar/dates.ts +181 -0
- package/dist/templates/components/calendar/index.ts +3 -0
- package/dist/templates/components/card/card.ts +8 -5
- package/dist/templates/components/checkbox/checkbox.ts +3 -3
- package/dist/templates/components/chip/chip-texts.ts +40 -0
- package/dist/templates/components/chip/chip.ts +116 -0
- package/dist/templates/components/chip/index.ts +2 -0
- package/dist/templates/components/color-picker/color-picker-texts.ts +57 -0
- package/dist/templates/components/color-picker/color-picker.ts +718 -0
- package/dist/templates/components/color-picker/color.ts +157 -0
- package/dist/templates/components/color-picker/index.ts +3 -0
- package/dist/templates/components/combobox/combobox-texts.ts +70 -0
- package/dist/templates/components/combobox/combobox.ts +798 -0
- package/dist/templates/components/combobox/index.ts +2 -0
- 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/date-picker/date-picker.ts +240 -0
- package/dist/templates/components/date-picker/index.ts +3 -0
- package/dist/templates/components/date-range/date-range.ts +308 -0
- package/dist/templates/components/date-range/index.ts +1 -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/file-upload/file-upload-texts.ts +77 -0
- package/dist/templates/components/file-upload/file-upload.ts +698 -0
- package/dist/templates/components/file-upload/index.ts +2 -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/input-mask/index.ts +1 -0
- package/dist/templates/components/input-mask/input-mask.ts +319 -0
- package/dist/templates/components/input-otp/index.ts +1 -0
- package/dist/templates/components/input-otp/input-otp.ts +212 -0
- package/dist/templates/components/menu/index.ts +15 -0
- package/dist/templates/components/menu/menu.ts +656 -0
- package/dist/templates/components/number-input/index.ts +2 -0
- package/dist/templates/components/number-input/number-input-texts.ts +46 -0
- package/dist/templates/components/number-input/number-input.ts +450 -0
- package/dist/templates/components/option-list/index.ts +11 -0
- package/dist/templates/components/option-list/option-list.ts +518 -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/password/index.ts +2 -0
- package/dist/templates/components/password/password-texts.ts +68 -0
- package/dist/templates/components/password/password.ts +363 -0
- package/dist/templates/components/popover/index.ts +12 -0
- package/dist/templates/components/popover/popover.ts +407 -0
- package/dist/templates/components/progress/index.ts +1 -0
- package/dist/templates/components/progress/progress.ts +217 -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 +1086 -0
- package/dist/templates/components/separator/separator.variants.ts +10 -7
- package/dist/templates/components/skeleton/skeleton.ts +4 -1
- package/dist/templates/components/slider/index.ts +2 -0
- package/dist/templates/components/slider/slider-texts.ts +26 -0
- package/dist/templates/components/slider/slider.ts +425 -0
- 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/tags-input/index.ts +2 -0
- package/dist/templates/components/tags-input/tags-input-texts.ts +44 -0
- package/dist/templates/components/tags-input/tags-input.ts +303 -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/toggle-group/index.ts +1 -0
- package/dist/templates/components/toggle-group/toggle-group.ts +406 -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 +75 -6
- package/dist/templates/theme/theme-base.css +410 -9
- package/dist/templates/theme/types.ts +494 -4
- package/dist/templates/utils/cn.ts +15 -1
- package/dist/templates/utils/control-styles.ts +2 -2
- package/dist/templates/utils/overlay.ts +153 -0
- package/dist/ui/index.js +184 -8
- package/dist/ui/schema.json +100 -0
- package/dist/utils/write-files.js +166 -59
- package/package.json +1 -1
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
|
2
|
+
import {
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
Component,
|
|
5
|
+
type ElementRef,
|
|
6
|
+
booleanAttribute,
|
|
7
|
+
computed,
|
|
8
|
+
inject,
|
|
9
|
+
input,
|
|
10
|
+
isDevMode,
|
|
11
|
+
model,
|
|
12
|
+
output,
|
|
13
|
+
signal,
|
|
14
|
+
viewChild,
|
|
15
|
+
} from '@angular/core';
|
|
16
|
+
import { cva } from 'class-variance-authority';
|
|
17
|
+
import { MimiButton } from '@/components/ui/button';
|
|
18
|
+
import { MimiInput } from '@/components/ui/input';
|
|
19
|
+
import { cn } from '@/components/ui/utils/cn';
|
|
20
|
+
import { MimiPaginationPageSize } from './pagination-page-size';
|
|
21
|
+
import { MIMI_PAGINATION_TEXT } from './pagination-texts';
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* Pagination (docs/design/Mimi Pagination.dc.html y Mimi F7 Pagination.dc.html; prefijo de tokens
|
|
25
|
+
* `pagination`, spec 13): píldora compacta `[←] [página] / total [→] | registros por página`.
|
|
26
|
+
*
|
|
27
|
+
* Es una región de navegación (`role="navigation"`) con paradas de Tab normales: son tres o cuatro
|
|
28
|
+
* controles distintos, no una fila de ítems iguales, y el campo necesita sus flechas. ← y → usan
|
|
29
|
+
* `aria-disabled` en los extremos (no `disabled`), para que el foco no se pierda al llegar.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** `sm` (por defecto, como el diseño) o `default`, con los nombres de tamaño de Button e Input. */
|
|
33
|
+
export type MimiPaginationSize = 'sm' | 'default';
|
|
34
|
+
export type MimiPaginationVariant = 'inline' | 'floating';
|
|
35
|
+
|
|
36
|
+
/** Lo que emite `paginate`: el estado final de cada acción del usuario. */
|
|
37
|
+
export interface MimiPaginationEvent {
|
|
38
|
+
page: number;
|
|
39
|
+
pageSize: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const paginationVariants = cva(
|
|
43
|
+
[
|
|
44
|
+
'inline-flex max-w-full items-center rounded-full border font-sans text-foreground',
|
|
45
|
+
'border-[color:var(--mimi-pagination-border,var(--mimi-glass-border))] bg-[color:var(--mimi-pagination-bg,var(--mimi-glass))]',
|
|
46
|
+
'backdrop-blur-[var(--mimi-pagination-blur,var(--mimi-glass-blur))] backdrop-saturate-[1.4]',
|
|
47
|
+
],
|
|
48
|
+
{
|
|
49
|
+
variants: {
|
|
50
|
+
size: { sm: 'gap-[4px] p-[3px] text-[13px]', default: 'gap-[6px] p-[4px] text-sm' },
|
|
51
|
+
// La flotante solo cambia la sombra; la posición se da con `class`.
|
|
52
|
+
variant: {
|
|
53
|
+
inline: 'shadow-neutral',
|
|
54
|
+
floating: 'shadow-[shadow:var(--mimi-pagination-shadow,var(--mimi-shadow-popover))]',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
defaultVariants: { size: 'sm', variant: 'inline' },
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const ARROW_CLASSES = {
|
|
62
|
+
sm: 'size-[var(--mimi-pagination-item-size,var(--mimi-control-height-sm,2rem))] rounded-full [&_svg]:size-4',
|
|
63
|
+
default:
|
|
64
|
+
'size-[var(--mimi-pagination-item-size,var(--mimi-control-height,2.5rem))] rounded-full [&_svg]:size-[18px]',
|
|
65
|
+
} satisfies Record<MimiPaginationSize, string>;
|
|
66
|
+
// 0,9 en vez del 0,88 del diseño (plan D6); deshabilitado, sin escala (también con Espacio).
|
|
67
|
+
const ARROW_PRESS = 'active:scale-(--mimi-press-scale-sm) aria-disabled:active:scale-100';
|
|
68
|
+
|
|
69
|
+
const INPUT_CLASSES = cn(
|
|
70
|
+
'rounded-full px-1 text-center font-semibold tabular-nums read-only:cursor-default',
|
|
71
|
+
'hover:not-aria-invalid:border-input-hover',
|
|
72
|
+
'aria-invalid:border-[color:var(--mimi-pagination-error,var(--mimi-destructive))] aria-invalid:focus-visible:border-[color:var(--mimi-pagination-error,var(--mimi-destructive))]',
|
|
73
|
+
'aria-invalid:ring-3 aria-invalid:ring-destructive-ring',
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const ERROR_CLASSES =
|
|
77
|
+
'pointer-events-none absolute bottom-[calc(100%_+_12px)] left-1/2 z-10 -translate-x-1/2 rounded-[7px] bg-[color:var(--mimi-pagination-error,var(--mimi-destructive))] px-[9px] py-[5px] text-xs font-semibold whitespace-nowrap text-destructive-foreground shadow-destructive';
|
|
78
|
+
|
|
79
|
+
let nextId = 0;
|
|
80
|
+
let warnedSize = false;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Paginación:
|
|
84
|
+
*
|
|
85
|
+
* ```html
|
|
86
|
+
* <mimi-pagination [(page)]="page" [(pageSize)]="size" [total]="240"
|
|
87
|
+
* [pageSizeOptions]="[10, 25, 50]" (paginate)="load($event)" />
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* `total` es el número de **registros**. `page` empieza en 1.
|
|
91
|
+
*/
|
|
92
|
+
@Component({
|
|
93
|
+
selector: 'mimi-pagination',
|
|
94
|
+
imports: [MimiButton, MimiInput, MimiPaginationPageSize],
|
|
95
|
+
template: `
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
mimiBtn
|
|
99
|
+
variant="ghost"
|
|
100
|
+
size="icon"
|
|
101
|
+
[class]="arrowClasses()"
|
|
102
|
+
[attr.aria-label]="text.previous"
|
|
103
|
+
[attr.aria-disabled]="first() || disabled() ? 'true' : null"
|
|
104
|
+
(click)="previous()"
|
|
105
|
+
>
|
|
106
|
+
<svg
|
|
107
|
+
aria-hidden="true"
|
|
108
|
+
viewBox="0 0 24 24"
|
|
109
|
+
fill="none"
|
|
110
|
+
stroke="currentColor"
|
|
111
|
+
stroke-width="2"
|
|
112
|
+
stroke-linecap="round"
|
|
113
|
+
stroke-linejoin="round"
|
|
114
|
+
>
|
|
115
|
+
<!-- Lucide chevron-left (ISC) -->
|
|
116
|
+
<path d="m15 18-6-6 6-6" />
|
|
117
|
+
</svg>
|
|
118
|
+
</button>
|
|
119
|
+
|
|
120
|
+
<span class="relative flex items-center gap-[inherit]">
|
|
121
|
+
@for (attempt of [attempt()]; track attempt) {
|
|
122
|
+
@if (error()) {
|
|
123
|
+
<!-- Se recrea en cada intento: así role="alert" se vuelve a anunciar. -->
|
|
124
|
+
<span role="alert" data-slot="error" [id]="errorId" [class]="errorClasses">
|
|
125
|
+
{{ text.outOfRange(pageCount()) }}
|
|
126
|
+
<span
|
|
127
|
+
aria-hidden="true"
|
|
128
|
+
class="absolute -bottom-1 left-1/2 -ml-1 size-2 rotate-45 rounded-[1px] bg-[color:var(--mimi-pagination-error,var(--mimi-destructive))]"
|
|
129
|
+
></span>
|
|
130
|
+
</span>
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
<input
|
|
134
|
+
#field
|
|
135
|
+
mimiInput
|
|
136
|
+
type="text"
|
|
137
|
+
inputmode="numeric"
|
|
138
|
+
autocomplete="off"
|
|
139
|
+
enterkeyhint="go"
|
|
140
|
+
[size]="size()"
|
|
141
|
+
[class]="inputClasses()"
|
|
142
|
+
[showError]="error()"
|
|
143
|
+
[value]="fieldValue()"
|
|
144
|
+
[readOnly]="disabled()"
|
|
145
|
+
[attr.maxlength]="maxLength()"
|
|
146
|
+
[attr.aria-label]="text.page(pageCount())"
|
|
147
|
+
[attr.aria-describedby]="error() ? errorId : null"
|
|
148
|
+
[attr.aria-disabled]="disabled() ? 'true' : null"
|
|
149
|
+
(input)="onInput($event)"
|
|
150
|
+
(keydown)="onKeydown($event)"
|
|
151
|
+
(focus)="onFocus($event)"
|
|
152
|
+
(mouseup)="onMouseup($event)"
|
|
153
|
+
(blur)="onBlur()"
|
|
154
|
+
/>
|
|
155
|
+
<span
|
|
156
|
+
aria-hidden="true"
|
|
157
|
+
data-slot="total"
|
|
158
|
+
class="pe-[2px] whitespace-nowrap text-muted-foreground tabular-nums"
|
|
159
|
+
>
|
|
160
|
+
/ {{ pageCount() }}
|
|
161
|
+
</span>
|
|
162
|
+
</span>
|
|
163
|
+
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
mimiBtn
|
|
167
|
+
variant="ghost"
|
|
168
|
+
size="icon"
|
|
169
|
+
[class]="arrowClasses()"
|
|
170
|
+
[attr.aria-label]="text.next"
|
|
171
|
+
[attr.aria-disabled]="last() || disabled() ? 'true' : null"
|
|
172
|
+
(click)="next()"
|
|
173
|
+
>
|
|
174
|
+
<svg
|
|
175
|
+
aria-hidden="true"
|
|
176
|
+
viewBox="0 0 24 24"
|
|
177
|
+
fill="none"
|
|
178
|
+
stroke="currentColor"
|
|
179
|
+
stroke-width="2"
|
|
180
|
+
stroke-linecap="round"
|
|
181
|
+
stroke-linejoin="round"
|
|
182
|
+
>
|
|
183
|
+
<!-- Lucide chevron-right (ISC) -->
|
|
184
|
+
<path d="m9 18 6-6-6-6" />
|
|
185
|
+
</svg>
|
|
186
|
+
</button>
|
|
187
|
+
|
|
188
|
+
@if (sizeOptions().length) {
|
|
189
|
+
<!-- En móvil (y con class, en una zona estrecha) se oculta: queda ← página / total →. -->
|
|
190
|
+
<span data-slot="page-size" class="flex items-center gap-[inherit] max-sm:hidden">
|
|
191
|
+
<span aria-hidden="true" class="mx-[2px] h-[18px] w-px shrink-0 bg-border"></span>
|
|
192
|
+
@defer (on immediate) {
|
|
193
|
+
<mimi-pagination-page-size
|
|
194
|
+
[options]="sizeOptions()"
|
|
195
|
+
[value]="safeSize()"
|
|
196
|
+
[size]="size()"
|
|
197
|
+
[blocked]="disabled()"
|
|
198
|
+
(changed)="changeSize($event)"
|
|
199
|
+
/>
|
|
200
|
+
} @placeholder {
|
|
201
|
+
<!-- Mientras llega Select: el mismo botón, sin función, para que nada salte. -->
|
|
202
|
+
<span aria-hidden="true" [class]="placeholderClasses()">
|
|
203
|
+
{{ text.pageSizeOption(safeSize()) }}
|
|
204
|
+
<svg
|
|
205
|
+
class="size-3.5 shrink-0 text-muted-foreground"
|
|
206
|
+
viewBox="0 0 24 24"
|
|
207
|
+
fill="none"
|
|
208
|
+
stroke="currentColor"
|
|
209
|
+
stroke-width="2"
|
|
210
|
+
stroke-linecap="round"
|
|
211
|
+
stroke-linejoin="round"
|
|
212
|
+
>
|
|
213
|
+
<!-- Lucide chevron-up (ISC) -->
|
|
214
|
+
<path d="m18 15-6-6-6 6" />
|
|
215
|
+
</svg>
|
|
216
|
+
</span>
|
|
217
|
+
}
|
|
218
|
+
</span>
|
|
219
|
+
}
|
|
220
|
+
`,
|
|
221
|
+
host: {
|
|
222
|
+
role: 'navigation',
|
|
223
|
+
'[attr.aria-label]': 'ariaLabel() || text.label',
|
|
224
|
+
'[class]': 'classes()',
|
|
225
|
+
'[attr.data-size]': 'size()',
|
|
226
|
+
'[attr.data-variant]': 'variant()',
|
|
227
|
+
'[attr.data-disabled]': 'disabled() ? "" : null',
|
|
228
|
+
},
|
|
229
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
230
|
+
})
|
|
231
|
+
export class MimiPagination {
|
|
232
|
+
protected readonly text = inject(MIMI_PAGINATION_TEXT);
|
|
233
|
+
// En una propiedad: la región del anunciador existe antes del primer anuncio.
|
|
234
|
+
private readonly announcer = inject(LiveAnnouncer);
|
|
235
|
+
|
|
236
|
+
/** La página, desde 1. Admite `[(page)]`. */
|
|
237
|
+
readonly page = model(1);
|
|
238
|
+
/** Registros por página. Admite `[(pageSize)]`. */
|
|
239
|
+
readonly pageSize = model(10);
|
|
240
|
+
/** Número total de **registros** (no de páginas). */
|
|
241
|
+
readonly total = input.required<number>();
|
|
242
|
+
/** Tamaños para elegir. Vacío (por defecto), sin selector. */
|
|
243
|
+
readonly pageSizeOptions = input<readonly number[]>([]);
|
|
244
|
+
/** `sm` (por defecto, controles de 32px) o `md` (40px). */
|
|
245
|
+
readonly size = input<MimiPaginationSize>('sm');
|
|
246
|
+
/** `floating` solo cambia la sombra; la posición va en `class`. */
|
|
247
|
+
readonly variant = input<MimiPaginationVariant>('inline');
|
|
248
|
+
/** Todo deshabilitado (por ejemplo, mientras carga), sin sacar el foco de donde esté. */
|
|
249
|
+
readonly disabled = input(false, { transform: booleanAttribute });
|
|
250
|
+
/** Nombre de la región. Por defecto, el de los textos («Pagination»). */
|
|
251
|
+
readonly ariaLabel = input<string>(undefined, { alias: 'aria-label' });
|
|
252
|
+
/** Clases de la píldora, mezcladas con `cn()` (gana la del usuario). */
|
|
253
|
+
readonly userClass = input('', { alias: 'class' });
|
|
254
|
+
|
|
255
|
+
/** Una vez por acción del usuario, con el estado final (la página y el tamaño). */
|
|
256
|
+
readonly paginate = output<MimiPaginationEvent>();
|
|
257
|
+
|
|
258
|
+
private readonly field = viewChild.required<ElementRef<HTMLInputElement>>('field');
|
|
259
|
+
|
|
260
|
+
/** Registros por página, siempre 1 o más. */
|
|
261
|
+
protected readonly safeSize = computed(() => {
|
|
262
|
+
const size = Math.floor(Number(this.pageSize()));
|
|
263
|
+
return size >= 1 ? size : 1;
|
|
264
|
+
});
|
|
265
|
+
/** Número de páginas (al menos 1). Útil para corregir una página que quedó fuera. */
|
|
266
|
+
readonly pageCount = computed(() => {
|
|
267
|
+
const total = Math.floor(Number(this.total()));
|
|
268
|
+
return Math.max(1, Math.ceil((total > 0 ? total : 0) / this.safeSize()));
|
|
269
|
+
});
|
|
270
|
+
/**
|
|
271
|
+
* La página que se pinta: la del modelo, acotada. Una página fuera de rango que llega de fuera
|
|
272
|
+
* (por ejemplo, `total` en 0 mientras cargan los datos) no se reescribe.
|
|
273
|
+
*/
|
|
274
|
+
protected readonly current = computed(() => {
|
|
275
|
+
const page = Math.floor(Number(this.page()));
|
|
276
|
+
return Math.min(Math.max(Number.isFinite(page) ? page : 1, 1), this.pageCount());
|
|
277
|
+
});
|
|
278
|
+
protected readonly first = computed(() => this.current() <= 1);
|
|
279
|
+
protected readonly last = computed(() => this.current() >= this.pageCount());
|
|
280
|
+
|
|
281
|
+
/** Tamaños del selector; si `pageSize` no está, se agrega (y avisa en desarrollo). */
|
|
282
|
+
protected readonly sizeOptions = computed(() => {
|
|
283
|
+
const options = [...this.pageSizeOptions()];
|
|
284
|
+
const size = this.safeSize();
|
|
285
|
+
if (!options.length || options.includes(size)) return options;
|
|
286
|
+
if (isDevMode() && !warnedSize) {
|
|
287
|
+
warnedSize = true;
|
|
288
|
+
console.warn(`[Mimi] Pagination: pageSize (${size}) no está en pageSizeOptions; se agrega.`);
|
|
289
|
+
}
|
|
290
|
+
return [...options, size].sort((a, b) => a - b);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
/** Lo que se está escribiendo en el campo (null: muestra la página actual). */
|
|
294
|
+
private readonly draft = signal<string | null>(null);
|
|
295
|
+
protected readonly error = signal(false);
|
|
296
|
+
/** Sube en cada intento inválido, para recrear el globo de error. */
|
|
297
|
+
protected readonly attempt = signal(0);
|
|
298
|
+
protected readonly fieldValue = computed(() => this.draft() ?? String(this.current()));
|
|
299
|
+
protected readonly maxLength = computed(() => String(this.pageCount()).length);
|
|
300
|
+
protected readonly errorId = `mimi-pagination-error-${nextId++}`;
|
|
301
|
+
|
|
302
|
+
protected readonly classes = computed(() =>
|
|
303
|
+
cn(paginationVariants({ size: this.size(), variant: this.variant() }), this.userClass()),
|
|
304
|
+
);
|
|
305
|
+
/**
|
|
306
|
+
* Clases propias de ← y → sobre las de `mimiBtn` ghost de ícono. No se le pasa `disabled` a
|
|
307
|
+
* Button (sería nativo y el foco se perdería en los extremos): se usa `aria-disabled`.
|
|
308
|
+
*/
|
|
309
|
+
protected readonly arrowClasses = computed(() => cn(ARROW_CLASSES[this.size()], ARROW_PRESS));
|
|
310
|
+
protected readonly inputClasses = computed(() =>
|
|
311
|
+
cn(
|
|
312
|
+
INPUT_CLASSES,
|
|
313
|
+
this.size() === 'default'
|
|
314
|
+
? 'w-[var(--mimi-pagination-input-width,52px)]'
|
|
315
|
+
: 'w-[var(--mimi-pagination-input-width,44px)]',
|
|
316
|
+
),
|
|
317
|
+
);
|
|
318
|
+
protected readonly errorClasses = ERROR_CLASSES;
|
|
319
|
+
protected readonly placeholderClasses = computed(() =>
|
|
320
|
+
cn(
|
|
321
|
+
'inline-flex items-center gap-1 rounded-full pr-2 pl-2.5 font-semibold whitespace-nowrap tabular-nums',
|
|
322
|
+
this.size() === 'default'
|
|
323
|
+
? 'h-[var(--mimi-pagination-item-size,var(--mimi-control-height,2.5rem))] text-sm'
|
|
324
|
+
: 'h-[var(--mimi-pagination-item-size,var(--mimi-control-height-sm,2rem))] text-[13px]',
|
|
325
|
+
),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
private selectOnMouseup = false;
|
|
329
|
+
|
|
330
|
+
protected previous(): void {
|
|
331
|
+
if (!this.first()) this.go(this.current() - 1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
protected next(): void {
|
|
335
|
+
if (!this.last()) this.go(this.current() + 1);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Cambia el tamaño manteniendo visible el primer registro que se veía (como MatPaginator). */
|
|
339
|
+
protected changeSize(size: number): void {
|
|
340
|
+
if (this.disabled() || size === this.safeSize()) return;
|
|
341
|
+
const firstRecord = (this.current() - 1) * this.safeSize();
|
|
342
|
+
this.pageSize.set(size);
|
|
343
|
+
const page = Math.min(Math.floor(firstRecord / size) + 1, this.pageCount());
|
|
344
|
+
this.page.set(page);
|
|
345
|
+
this.paginate.emit({ page, pageSize: size });
|
|
346
|
+
this.announce(page);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
protected onInput(event: Event): void {
|
|
350
|
+
const field = event.target as HTMLInputElement;
|
|
351
|
+
const digits = field.value.replace(/\D/g, '').slice(0, this.maxLength());
|
|
352
|
+
if (digits !== field.value) field.value = digits;
|
|
353
|
+
this.draft.set(digits);
|
|
354
|
+
this.error.set(false);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
protected onKeydown(event: KeyboardEvent): void {
|
|
358
|
+
// Enfocado con Tab: ya no hay un mouseup que deshaga la selección.
|
|
359
|
+
this.selectOnMouseup = false;
|
|
360
|
+
switch (event.key) {
|
|
361
|
+
case 'Enter': {
|
|
362
|
+
// También evita enviar el formulario que la contenga.
|
|
363
|
+
event.preventDefault();
|
|
364
|
+
if (this.disabled() || this.draft() === null) return;
|
|
365
|
+
const page = this.parse(this.draft());
|
|
366
|
+
if (page === null) {
|
|
367
|
+
this.error.set(true);
|
|
368
|
+
this.attempt.update((n) => n + 1);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
this.go(page);
|
|
372
|
+
this.reset();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
case 'Escape':
|
|
376
|
+
if (this.draft() === null && !this.error()) return;
|
|
377
|
+
event.preventDefault();
|
|
378
|
+
this.reset();
|
|
379
|
+
return;
|
|
380
|
+
case 'ArrowUp':
|
|
381
|
+
case 'ArrowDown':
|
|
382
|
+
event.preventDefault();
|
|
383
|
+
if (this.disabled()) return;
|
|
384
|
+
this.reset();
|
|
385
|
+
if (event.key === 'ArrowUp') this.next();
|
|
386
|
+
else this.previous();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Al salir: una página válida se aplica; si no, vuelve a la actual sin error. */
|
|
392
|
+
protected onBlur(): void {
|
|
393
|
+
this.selectOnMouseup = false;
|
|
394
|
+
if (this.draft() === null) return;
|
|
395
|
+
const page = this.disabled() ? null : this.parse(this.draft());
|
|
396
|
+
if (page !== null) this.go(page);
|
|
397
|
+
this.reset();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Al enfocar, selecciona el número (con un clic, después del mouseup, que lo deshace). */
|
|
401
|
+
protected onFocus(event: FocusEvent): void {
|
|
402
|
+
(event.target as HTMLInputElement).select();
|
|
403
|
+
this.selectOnMouseup = true;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
protected onMouseup(event: MouseEvent): void {
|
|
407
|
+
if (!this.selectOnMouseup) return;
|
|
408
|
+
this.selectOnMouseup = false;
|
|
409
|
+
event.preventDefault();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Va a esa página: escribe el modelo, emite y anuncia, solo si cambia. */
|
|
413
|
+
private go(page: number): void {
|
|
414
|
+
if (this.disabled() || page === this.current()) return;
|
|
415
|
+
this.page.set(page);
|
|
416
|
+
this.paginate.emit({ page, pageSize: this.safeSize() });
|
|
417
|
+
this.announce(page);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private announce(page: number): void {
|
|
421
|
+
void this.announcer.announce(this.text.announce(page, this.pageCount()), 'polite');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Vuelve a mostrar la página actual (también en el DOM: el binding no se entera si el texto
|
|
426
|
+
* escrito coincide con la página nueva). Va después de `go()`.
|
|
427
|
+
*/
|
|
428
|
+
private reset(): void {
|
|
429
|
+
this.draft.set(null);
|
|
430
|
+
this.error.set(false);
|
|
431
|
+
this.field().nativeElement.value = String(this.current());
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Página válida (entera, de 1 al total) o null. */
|
|
435
|
+
private parse(text: string | null): number | null {
|
|
436
|
+
if (!text) return null;
|
|
437
|
+
const page = Number(text);
|
|
438
|
+
return Number.isInteger(page) && page >= 1 && page <= this.pageCount() ? page : null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Para importarla con su selector (las piezas internas no se usan en plantillas). */
|
|
443
|
+
export const MimiPaginationImports = [MimiPagination] as const;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { InjectionToken, type Provider } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Textos de Password, en archivo propio: así se pueden proveer en `app.config.ts` sin llevar el
|
|
5
|
+
* componente al chunk inicial (como Select, G1.5).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface MimiPasswordText {
|
|
9
|
+
/**
|
|
10
|
+
* Nombre fijo del botón; el estado lo da `aria-pressed` (guía APG, «Button»: un botón de
|
|
11
|
+
* alternar no cambia de nombre).
|
|
12
|
+
*/
|
|
13
|
+
show: string;
|
|
14
|
+
/** Nombres de los niveles del medidor. */
|
|
15
|
+
weak: string;
|
|
16
|
+
medium: string;
|
|
17
|
+
strong: string;
|
|
18
|
+
/** Lo que anuncia el lector cuando cambia el nivel. */
|
|
19
|
+
announce: (level: string) => string;
|
|
20
|
+
/** Las cuatro reglas por defecto. */
|
|
21
|
+
minLength: string;
|
|
22
|
+
uppercase: string;
|
|
23
|
+
number: string;
|
|
24
|
+
symbol: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Textos predeterminados (inglés). */
|
|
28
|
+
export const MIMI_PASSWORD_TEXT_EN: MimiPasswordText = {
|
|
29
|
+
show: 'Show password',
|
|
30
|
+
weak: 'Weak',
|
|
31
|
+
medium: 'Medium',
|
|
32
|
+
strong: 'Strong',
|
|
33
|
+
announce: (level) => `Password strength: ${level}`,
|
|
34
|
+
minLength: '8 characters or more',
|
|
35
|
+
uppercase: 'An uppercase letter',
|
|
36
|
+
number: 'A number',
|
|
37
|
+
symbol: 'A symbol',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Textos en español: `provideMimiPasswordText(MIMI_PASSWORD_TEXT_ES)`. */
|
|
41
|
+
export const MIMI_PASSWORD_TEXT_ES: MimiPasswordText = {
|
|
42
|
+
show: 'Mostrar contraseña',
|
|
43
|
+
weak: 'Débil',
|
|
44
|
+
medium: 'Media',
|
|
45
|
+
strong: 'Fuerte',
|
|
46
|
+
announce: (level) => `Seguridad de la contraseña: ${level}`,
|
|
47
|
+
minLength: '8 caracteres o más',
|
|
48
|
+
uppercase: 'Una mayúscula',
|
|
49
|
+
number: 'Un número',
|
|
50
|
+
symbol: 'Un símbolo',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Textos activos. Sin `provideMimiPasswordText`, los predeterminados en inglés. */
|
|
54
|
+
export const MIMI_PASSWORD_TEXT = new InjectionToken<MimiPasswordText>('MIMI_PASSWORD_TEXT', {
|
|
55
|
+
providedIn: 'root',
|
|
56
|
+
factory: () => MIMI_PASSWORD_TEXT_EN,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Cambia los textos de Password. Lo que no definas sigue en inglés:
|
|
61
|
+
*
|
|
62
|
+
* ```ts
|
|
63
|
+
* provideMimiPasswordText(MIMI_PASSWORD_TEXT_ES)
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function provideMimiPasswordText(text: Partial<MimiPasswordText>): Provider {
|
|
67
|
+
return { provide: MIMI_PASSWORD_TEXT, useValue: { ...MIMI_PASSWORD_TEXT_EN, ...text } };
|
|
68
|
+
}
|