@lilydesignsystem/angular-text-size-picker 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.
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # TextSizePicker (Angular helper)
2
+
3
+ A reusable Angular 20 headless **text-size picker**. Renders an icon
4
+ button that opens a [WAI-ARIA APG listbox](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/)
5
+ of text-size slugs and, on every change, sets
6
+ `data-text-size="{slug}"` on a target element (default
7
+ `document.documentElement`), optionally persisting the choice to
8
+ `localStorage`. Same shape as the sibling `theme-picker` and
9
+ `locale-picker` helpers. Ships no CSS — the consumer supplies the
10
+ listbox positioning and maps each size slug to real typography, e.g.:
11
+
12
+ ```css
13
+ :root[data-text-size="small"] {
14
+ font-size: 87.5%;
15
+ }
16
+ :root[data-text-size="medium"] {
17
+ font-size: 100%;
18
+ }
19
+ :root[data-text-size="large"] {
20
+ font-size: 112.5%;
21
+ }
22
+ :root[data-text-size="x-large"] {
23
+ font-size: 125%;
24
+ }
25
+ ```
26
+
27
+ This supports WCAG 2.2 — 1.4.4 (Resize Text) and 1.4.12 (Text
28
+ Spacing) — by letting users pick a comfortable reading size that the
29
+ app remembers.
30
+
31
+ ## Usage
32
+
33
+ ```ts
34
+ import { Component, signal } from "@angular/core";
35
+ import { TextSizePicker } from "@lilydesignsystem/angular-text-size-picker";
36
+
37
+ @Component({
38
+ standalone: true,
39
+ imports: [TextSizePicker],
40
+ template: `
41
+ <lily-text-size-picker
42
+ label="Text size"
43
+ [sizes]="['small', 'medium', 'large', 'x-large']"
44
+ [(value)]="size"
45
+ storageKey="lily-text-size"
46
+ />
47
+ `,
48
+ })
49
+ export class SettingsPage {
50
+ size = signal("");
51
+ }
52
+ ```
53
+
54
+ ## Inputs / outputs
55
+
56
+ | Input / output | Type | Required | Description |
57
+ | -------------- | ----------------------- | -------- | ---------------------------------------------------------- |
58
+ | `label` | `string` | yes | Accessible name (`aria-label`) for the button and listbox. |
59
+ | `sizes` | `string[]` | yes | Available size slugs. |
60
+ | `value` | `string` | no | Selected slug. Two-way bindable via `[(value)]`. |
61
+ | `defaultValue` | `string` | no | Initial slug when nothing else is supplied. |
62
+ | `storageKey` | `string` | no | If set, persist the slug to `localStorage`. |
63
+ | `name` | `string` | no | `name` of the hidden input (default `"text-size"`). |
64
+ | `target` | `HTMLElement \| null` | no | Element to receive `data-text-size`. Default `<html>`. |
65
+ | `sizeLabels` | `Record<string,string>` | no | Pretty labels per slug. |
66
+ | `className` | `string` | no | Extra CSS class on the root `<div>`. |
67
+ | `sizeChange` | `output<string>` | no | Emits after a new size is applied. |
68
+
69
+ ## Markup
70
+
71
+ ```html
72
+ <div class="text-size-picker">
73
+ <input type="hidden" name="text-size" value="medium" />
74
+ <button
75
+ type="button"
76
+ class="text-size-picker-button"
77
+ aria-label="Text size"
78
+ aria-haspopup="listbox"
79
+ aria-expanded="false"
80
+ aria-controls="…-list"
81
+ >
82
+ <svg class="text-size-picker-icon" viewBox="0 0 16 16" aria-hidden="true">…</svg>
83
+ </button>
84
+ <ul
85
+ class="text-size-picker-list"
86
+ id="…-list"
87
+ role="listbox"
88
+ aria-label="Text size"
89
+ tabindex="-1"
90
+ hidden
91
+ >
92
+ <li
93
+ class="text-size-picker-option"
94
+ role="option"
95
+ aria-selected="true"
96
+ data-active
97
+ >
98
+ Medium
99
+ </li>
100
+ </ul>
101
+ </div>
102
+ ```
103
+
104
+ The icon is a bundled outline "A" SVG, not a Unicode character
105
+ (reversed 2026-09-16) — it renders identically on every platform
106
+ regardless of the consumer's installed fonts. `data-active` marks the
107
+ keyboard cursor; `aria-selected` marks the size in effect. Style both,
108
+ differently.
109
+
110
+ ## Custom icon
111
+
112
+ Project an `<ng-template>` to replace the icon inside the button. It
113
+ receives `{ value, open, labelFor }`; it does **not** render options.
114
+
115
+ ```html
116
+ <lily-text-size-picker label="Text size" [sizes]="sizes">
117
+ <ng-template lilyTextSizePickerIcon let-args>
118
+ <svg width="16" height="16" aria-hidden="true" focusable="false">…</svg>
119
+ </ng-template>
120
+ </lily-text-size-picker>
121
+ ```
122
+
123
+ ## Behaviour
124
+
125
+ Initial value resolves from `value` > storage > `defaultValue` >
126
+ `"medium"` (if present) > `sizes[0]`. All DOM writes happen inside an
127
+ `effect()` guarded on `typeof document`, so the component is SSR-safe.
128
+
129
+ `labelFor(slug)` returns `sizeLabels[slug]` if present, else delegates
130
+ to the exported `sizeName(slug)`, which title-cases the slug per
131
+ hyphen-word (`x-large` → `X Large`).
132
+
133
+ There is no OS-preference detection: unlike `prefers-color-scheme`
134
+ for themes or `navigator.languages` for locales, no "preferred text
135
+ size" media query exists.
136
+
137
+ ## Keyboard
138
+
139
+ `ArrowDown` / `Enter` / `Space` open the listbox on the selected
140
+ option; `ArrowUp` opens on the last. Inside the listbox: arrows move
141
+ the cursor and clamp (no wrap), `Home` / `End` jump, `PageUp` /
142
+ `PageDown` move by ten (clamped), printable characters typeahead over
143
+ the labels with a 500 ms buffer — a single character advances to the
144
+ next match and repeats cycle; differing characters refine — `Enter` /
145
+ `Space` select and return focus to the button, `Escape` closes
146
+ unchanged, `Tab` closes via the button so the default Tab proceeds
147
+ from the picker's position. Clicking an option selects it; clicking
148
+ outside closes.
149
+
150
+ ## Accessibility
151
+
152
+ - WCAG 2.2 AAA target; directly supports 1.4.4 (Resize Text) and
153
+ 1.4.12 (Text Spacing).
154
+ - The button is icon-only, so `aria-label` is its **entire**
155
+ accessible name. Name the setting ("Text size"), not the widget.
156
+ - This is a hand-rolled listbox, not a native `<select>` — it has
157
+ weaker assistive-technology support. See
158
+ [docs/accessibility.md](./docs/accessibility.md) for the full
159
+ tradeoffs.
160
+ - Default labels title-case the slug.
161
+
162
+ ## Conventions this package follows
163
+
164
+ - Angular 20 standalone component with `input<T>()` /
165
+ `input.required<T>()`, `model<string>()`, `output<T>()`.
166
+ - `ChangeDetectionStrategy.OnPush`.
167
+ - `@for` control flow.
168
+ - Strict TypeScript on the public surface.
169
+ - No runtime dependency beyond `@angular/core` / `@angular/common`.
170
+ - No bundled CSS, fonts, icons, or images.
171
+ - All user-facing strings come from inputs.
172
+
173
+ ## License
174
+
175
+ Dual-licensed under MIT or Apache-2.0 or GPL-2.0 or GPL-3.0 or
176
+ BSD-3-Clause. Contact joel@joelparkerhenderson.com for other terms.
177
+
178
+ ---
179
+
180
+ Lily™ and Lily Design System™ are trademarks.
@@ -0,0 +1,509 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { Directive, input, model, output, contentChild, TemplateRef, viewChild, signal, computed, inject, ChangeDetectorRef, DestroyRef, effect, ChangeDetectionStrategy, Component } from '@angular/core';
4
+
5
+ /**
6
+ * Resolve a size slug to its display label: each hyphen-separated word
7
+ * title-cased, so a slug like "x-large" renders as "X Large". Mirrors
8
+ * `themeName` in theme-picker and `localeName` in locale-picker.
9
+ */
10
+ function sizeName(size) {
11
+ return size
12
+ .split("-")
13
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
14
+ .join(" ");
15
+ }
16
+ let uid = 0;
17
+ /** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */
18
+ function nextTextSizePickerId() {
19
+ uid += 1;
20
+ return `text-size-picker-${uid}`;
21
+ }
22
+ /**
23
+ * Optional marker for the projected icon template. Gives consumers typed
24
+ * `let-` variables:
25
+ *
26
+ * ```html
27
+ * <lily-text-size-picker ...>
28
+ * <ng-template lilyTextSizePickerIcon let-args>{{ args.labelFor(args.value) }}</ng-template>
29
+ * </lily-text-size-picker>
30
+ * ```
31
+ *
32
+ * The component queries any projected `<ng-template>`, so the marker is
33
+ * for type-checking and readability, not for matching.
34
+ */
35
+ class TextSizePickerIcon {
36
+ static ngTemplateContextGuard(_dir, _ctx) {
37
+ return true;
38
+ }
39
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: TextSizePickerIcon, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
40
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.4", type: TextSizePickerIcon, isStandalone: true, selector: "ng-template[lilyTextSizePickerIcon]", ngImport: i0 }); }
41
+ }
42
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: TextSizePickerIcon, decorators: [{
43
+ type: Directive,
44
+ args: [{
45
+ selector: "ng-template[lilyTextSizePickerIcon]",
46
+ standalone: true,
47
+ }]
48
+ }] });
49
+ /**
50
+ * TextSizePicker — `data-text-size` text-size picker.
51
+ *
52
+ * Renders an icon button that opens a WAI-ARIA APG listbox of sizes. On
53
+ * every size change the component sets `data-text-size="{slug}"` on the
54
+ * document root (or on a consumer-supplied target), with optional
55
+ * `localStorage` persistence. Ships no CSS; the consumer maps each
56
+ * `[data-text-size="…"]` slug to real typography. See `spec/index.md`
57
+ * for the full contract.
58
+ */
59
+ class TextSizePicker {
60
+ constructor() {
61
+ this.label = input.required(/* @ts-ignore */
62
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
63
+ this.sizes = input.required(/* @ts-ignore */
64
+ ...(ngDevMode ? [{ debugName: "sizes" }] : /* istanbul ignore next */ []));
65
+ this.value = model("", /* @ts-ignore */
66
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
67
+ this.defaultValue = input("", /* @ts-ignore */
68
+ ...(ngDevMode ? [{ debugName: "defaultValue" }] : /* istanbul ignore next */ []));
69
+ this.storageKey = input("", /* @ts-ignore */
70
+ ...(ngDevMode ? [{ debugName: "storageKey" }] : /* istanbul ignore next */ []));
71
+ this.name = input("text-size", /* @ts-ignore */
72
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
73
+ this.target = input(null, /* @ts-ignore */
74
+ ...(ngDevMode ? [{ debugName: "target" }] : /* istanbul ignore next */ []));
75
+ this.sizeLabels = input({}, /* @ts-ignore */
76
+ ...(ngDevMode ? [{ debugName: "sizeLabels" }] : /* istanbul ignore next */ []));
77
+ this.className = input("", /* @ts-ignore */
78
+ ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
79
+ this.sizeChange = output();
80
+ /** Projected icon template; replaces the default icon when supplied. */
81
+ this.iconTemplate = contentChild(TemplateRef, /* @ts-ignore */
82
+ ...(ngDevMode ? [{ debugName: "iconTemplate" }] : /* istanbul ignore next */ []));
83
+ this.rootRef = viewChild.required("rootEl", /* @ts-ignore */
84
+ ...(ngDevMode ? [{ debugName: "rootRef" }] : /* istanbul ignore next */ []));
85
+ this.buttonRef = viewChild.required("buttonEl", /* @ts-ignore */
86
+ ...(ngDevMode ? [{ debugName: "buttonRef" }] : /* istanbul ignore next */ []));
87
+ this.listRef = viewChild.required("listEl", /* @ts-ignore */
88
+ ...(ngDevMode ? [{ debugName: "listRef" }] : /* istanbul ignore next */ []));
89
+ this.baseId = nextTextSizePickerId();
90
+ this.listId = `${this.baseId}-list`;
91
+ this.open = signal(false, /* @ts-ignore */
92
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
93
+ this.activeIndex = signal(-1, /* @ts-ignore */
94
+ ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
95
+ /** `aria-activedescendant` is only meaningful while the listbox is open. */
96
+ this.activeDescendant = computed(() => {
97
+ const i = this.activeIndex();
98
+ return this.open() && i >= 0 ? this.optionId(i) : null;
99
+ }, /* @ts-ignore */
100
+ ...(ngDevMode ? [{ debugName: "activeDescendant" }] : /* istanbul ignore next */ []));
101
+ this.childContext = computed(() => {
102
+ const args = {
103
+ value: this.value(),
104
+ open: this.open(),
105
+ labelFor: (size) => this.labelFor(size),
106
+ };
107
+ return { $implicit: args, ...args };
108
+ }, /* @ts-ignore */
109
+ ...(ngDevMode ? [{ debugName: "childContext" }] : /* istanbul ignore next */ []));
110
+ // Typeahead buffer: APG listbox behaviour. Reset after a pause.
111
+ this.typeahead = "";
112
+ this.initialised = false;
113
+ // ---------------------------------------------------------------
114
+ // Open / close
115
+ // ---------------------------------------------------------------
116
+ /** Under zoneless change detection the signal write that un-hides the
117
+ * list has not reached the DOM when a microtask runs, so focusing the
118
+ * still-hidden list fails silently and Escape lands on the button.
119
+ * Flushing detection first is the Angular equivalent of Svelte's
120
+ * synchronous update (same fix the date-time-picker port recorded). */
121
+ this.cdr = inject(ChangeDetectorRef);
122
+ inject(DestroyRef).onDestroy(() => clearTimeout(this.typeaheadTimer));
123
+ effect(() => {
124
+ const current = this.value();
125
+ if (!this.initialised) {
126
+ this.initialised = true;
127
+ let initial = current;
128
+ const sk = this.storageKey();
129
+ if (!initial && sk) {
130
+ try {
131
+ initial =
132
+ (typeof localStorage !== "undefined"
133
+ ? localStorage.getItem(sk)
134
+ : null) ?? "";
135
+ }
136
+ catch {
137
+ // ignore privacy errors
138
+ }
139
+ }
140
+ if (!initial) {
141
+ const sizes = this.sizes();
142
+ const dv = this.defaultValue();
143
+ initial =
144
+ dv || (sizes.includes("medium") ? "medium" : sizes[0]) || "";
145
+ }
146
+ if (initial && initial !== current) {
147
+ this.value.set(initial);
148
+ return;
149
+ }
150
+ }
151
+ if (current)
152
+ this.applySize(current);
153
+ });
154
+ }
155
+ optionId(index) {
156
+ return `${this.baseId}-option-${index}`;
157
+ }
158
+ labelFor(size) {
159
+ const labels = this.sizeLabels();
160
+ if (size in labels)
161
+ return labels[size];
162
+ return sizeName(size);
163
+ }
164
+ toggle() {
165
+ if (this.open())
166
+ this.closeList();
167
+ else
168
+ this.openList();
169
+ }
170
+ /** Open the listbox, activating `startIndex` (default: the selection). */
171
+ openList(startIndex) {
172
+ const selected = this.sizes().indexOf(this.value());
173
+ // An empty list has no option to activate; -1 keeps
174
+ // aria-activedescendant off rather than pointing at an id that
175
+ // does not exist.
176
+ this.activeIndex.set(this.sizes().length === 0
177
+ ? -1
178
+ : (startIndex ?? (selected >= 0 ? selected : 0)));
179
+ this.open.set(true);
180
+ // Focus moves to the listbox; the active option is conveyed via
181
+ // aria-activedescendant, per the APG listbox pattern.
182
+ this.cdr.detectChanges();
183
+ queueMicrotask(() => {
184
+ this.listRef().nativeElement.focus({ preventScroll: true });
185
+ this.scrollActiveIntoView();
186
+ });
187
+ }
188
+ /** Close the listbox; `refocus` returns focus to the button. */
189
+ closeList(refocus = true) {
190
+ if (!this.open())
191
+ return;
192
+ this.open.set(false);
193
+ this.activeIndex.set(-1);
194
+ if (refocus)
195
+ queueMicrotask(() => this.buttonRef().nativeElement.focus({ preventScroll: true }));
196
+ }
197
+ choose(index) {
198
+ const slug = this.sizes()[index];
199
+ if (slug)
200
+ this.value.set(slug);
201
+ this.closeList();
202
+ }
203
+ scrollActiveIntoView() {
204
+ const i = this.activeIndex();
205
+ if (i < 0)
206
+ return;
207
+ const el = this.listRef().nativeElement.children[i];
208
+ // jsdom does not implement scrollIntoView; call it only if present.
209
+ el?.scrollIntoView?.({ block: "nearest" });
210
+ }
211
+ moveActive(delta) {
212
+ const count = this.sizes().length;
213
+ if (count === 0)
214
+ return;
215
+ // Clamp rather than wrap, per the APG listbox pattern.
216
+ this.activeIndex.set(Math.min(Math.max(this.activeIndex() + delta, 0), count - 1));
217
+ this.scrollActiveIntoView();
218
+ }
219
+ runTypeahead(char) {
220
+ const lower = char.toLowerCase();
221
+ // APG listbox typeahead: a single character moves to the NEXT
222
+ // option starting with it, and repeating that character keeps
223
+ // cycling. Only a buffer of differing characters refines the
224
+ // match, and that buffer stays anchored on the active option.
225
+ const sameCharRun = this.typeahead === "" || [...this.typeahead].every((c) => c === lower);
226
+ this.typeahead += lower;
227
+ clearTimeout(this.typeaheadTimer);
228
+ this.typeaheadTimer = setTimeout(() => (this.typeahead = ""), 500);
229
+ const query = sameCharRun ? lower : this.typeahead;
230
+ const sizes = this.sizes();
231
+ const anchor = this.activeIndex() < 0 ? 0 : this.activeIndex();
232
+ const start = sameCharRun ? anchor + 1 : anchor;
233
+ // Search forward, wrapping once — typeahead wraps even though the
234
+ // arrows clamp, or options above the cursor would be untypable.
235
+ for (let n = 0; n < sizes.length; n++) {
236
+ const i = (start + n) % sizes.length;
237
+ if (this.labelFor(sizes[i]).toLowerCase().startsWith(query)) {
238
+ this.activeIndex.set(i);
239
+ this.scrollActiveIntoView();
240
+ return;
241
+ }
242
+ }
243
+ }
244
+ // ---------------------------------------------------------------
245
+ // Keyboard
246
+ // ---------------------------------------------------------------
247
+ onButtonKeydown(event) {
248
+ switch (event.key) {
249
+ case "ArrowDown":
250
+ case "Enter":
251
+ case " ":
252
+ event.preventDefault();
253
+ this.openList();
254
+ break;
255
+ case "ArrowUp":
256
+ event.preventDefault();
257
+ this.openList(this.sizes().length - 1);
258
+ break;
259
+ }
260
+ }
261
+ onListKeydown(event) {
262
+ switch (event.key) {
263
+ case "ArrowDown":
264
+ event.preventDefault();
265
+ this.moveActive(1);
266
+ break;
267
+ case "ArrowUp":
268
+ event.preventDefault();
269
+ this.moveActive(-1);
270
+ break;
271
+ case "Home":
272
+ event.preventDefault();
273
+ this.activeIndex.set(0);
274
+ this.scrollActiveIntoView();
275
+ break;
276
+ case "End":
277
+ event.preventDefault();
278
+ this.activeIndex.set(this.sizes().length - 1);
279
+ this.scrollActiveIntoView();
280
+ break;
281
+ case "Enter":
282
+ case " ":
283
+ event.preventDefault();
284
+ if (this.activeIndex() >= 0)
285
+ this.choose(this.activeIndex());
286
+ break;
287
+ case "Escape":
288
+ event.preventDefault();
289
+ this.closeList();
290
+ break;
291
+ case "PageUp":
292
+ event.preventDefault();
293
+ this.moveActive(-10);
294
+ break;
295
+ case "PageDown":
296
+ // ±10, clamped: an APG-optional key for long size lists.
297
+ event.preventDefault();
298
+ this.moveActive(10);
299
+ break;
300
+ case "Tab":
301
+ // Tab moves on — but focus goes to the button FIRST, without
302
+ // cancelling the key. Hiding the focused list drops focus to
303
+ // <body>, and the browser then computes the default Tab move
304
+ // from the top of the document, so tabbing out of an open
305
+ // picker teleported the user to the page's first tab stop.
306
+ // From the button, the default Tab lands exactly where leaving
307
+ // the picker should. The button always exists, so no
308
+ // detectChanges is needed before the focus move; guard the
309
+ // METHOD because jsdom-shaped hosts may not implement it.
310
+ this.buttonRef().nativeElement.focus?.({ preventScroll: true });
311
+ this.closeList(false);
312
+ break;
313
+ default:
314
+ if (event.key.length === 1 &&
315
+ !event.ctrlKey &&
316
+ !event.metaKey &&
317
+ !event.altKey) {
318
+ this.runTypeahead(event.key);
319
+ }
320
+ }
321
+ }
322
+ onRootFocusOut(event) {
323
+ const next = event.relatedTarget;
324
+ if (next && this.rootRef().nativeElement.contains(next))
325
+ return;
326
+ this.closeList(false);
327
+ }
328
+ onDocumentClick(event) {
329
+ if (!this.open())
330
+ return;
331
+ const t = event.target;
332
+ if (t && !this.rootRef().nativeElement.contains(t))
333
+ this.closeList(false);
334
+ }
335
+ // ---------------------------------------------------------------
336
+ // Apply
337
+ // ---------------------------------------------------------------
338
+ applySize(slug) {
339
+ if (typeof document === "undefined" || !slug)
340
+ return;
341
+ (this.target() ?? document.documentElement).setAttribute("data-text-size", slug);
342
+ const sk = this.storageKey();
343
+ if (sk) {
344
+ try {
345
+ localStorage.setItem(sk, slug);
346
+ }
347
+ catch {
348
+ // ignore quota / privacy errors
349
+ }
350
+ }
351
+ this.sizeChange.emit(slug);
352
+ }
353
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: TextSizePicker, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
354
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: TextSizePicker, isStandalone: true, selector: "lily-text-size-picker", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, sizes: { classPropertyName: "sizes", publicName: "sizes", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: true, isRequired: false, transformFunction: null }, storageKey: { classPropertyName: "storageKey", publicName: "storageKey", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, sizeLabels: { classPropertyName: "sizeLabels", publicName: "sizeLabels", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", sizeChange: "sizeChange" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, queries: [{ propertyName: "iconTemplate", first: true, predicate: TemplateRef, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "rootRef", first: true, predicate: ["rootEl"], descendants: true, isSignal: true }, { propertyName: "buttonRef", first: true, predicate: ["buttonEl"], descendants: true, isSignal: true }, { propertyName: "listRef", first: true, predicate: ["listEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
355
+ <div
356
+ #rootEl
357
+ class="text-size-picker {{ className() }}"
358
+ (focusout)="onRootFocusOut($event)"
359
+ >
360
+ <input type="hidden" [name]="name()" [value]="value()" />
361
+
362
+ <button
363
+ #buttonEl
364
+ type="button"
365
+ class="text-size-picker-button"
366
+ [attr.aria-label]="label() || null"
367
+ aria-haspopup="listbox"
368
+ [attr.aria-expanded]="open()"
369
+ [attr.aria-controls]="listId"
370
+ (click)="toggle()"
371
+ (keydown)="onButtonKeydown($event)"
372
+ >
373
+ @if (iconTemplate(); as tpl) {
374
+ <ng-container
375
+ [ngTemplateOutlet]="tpl"
376
+ [ngTemplateOutletContext]="childContext()"
377
+ />
378
+ } @else {
379
+ <svg
380
+ class="text-size-picker-icon"
381
+ viewBox="0 0 16 16"
382
+ width="1.05rem"
383
+ height="1.05rem"
384
+ aria-hidden="true"
385
+ fill="none"
386
+ stroke="currentColor"
387
+ stroke-width="1.6"
388
+ stroke-linecap="round"
389
+ stroke-linejoin="round"
390
+ >
391
+ <path d="M4 13 7.2 3h1.6L12 13M5.4 9.5h5.2" />
392
+ </svg>
393
+ }
394
+ </button>
395
+
396
+ <ul
397
+ #listEl
398
+ class="text-size-picker-list"
399
+ [id]="listId"
400
+ role="listbox"
401
+ [attr.aria-label]="label() || null"
402
+ [attr.aria-activedescendant]="activeDescendant()"
403
+ tabindex="-1"
404
+ [attr.hidden]="open() ? null : ''"
405
+ (keydown)="onListKeydown($event)"
406
+ >
407
+ @for (size of sizes(); track size; let i = $index) {
408
+ <li
409
+ class="text-size-picker-option"
410
+ [id]="optionId(i)"
411
+ role="option"
412
+ [attr.aria-selected]="size === value()"
413
+ [attr.data-active]="i === activeIndex() ? '' : null"
414
+ (click)="choose(i)"
415
+ >
416
+ {{ labelFor(size) }}
417
+ </li>
418
+ }
419
+ </ul>
420
+ </div>
421
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
422
+ }
423
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: TextSizePicker, decorators: [{
424
+ type: Component,
425
+ args: [{
426
+ selector: "lily-text-size-picker",
427
+ standalone: true,
428
+ imports: [NgTemplateOutlet],
429
+ changeDetection: ChangeDetectionStrategy.OnPush,
430
+ host: {
431
+ "(document:click)": "onDocumentClick($event)",
432
+ },
433
+ template: `
434
+ <div
435
+ #rootEl
436
+ class="text-size-picker {{ className() }}"
437
+ (focusout)="onRootFocusOut($event)"
438
+ >
439
+ <input type="hidden" [name]="name()" [value]="value()" />
440
+
441
+ <button
442
+ #buttonEl
443
+ type="button"
444
+ class="text-size-picker-button"
445
+ [attr.aria-label]="label() || null"
446
+ aria-haspopup="listbox"
447
+ [attr.aria-expanded]="open()"
448
+ [attr.aria-controls]="listId"
449
+ (click)="toggle()"
450
+ (keydown)="onButtonKeydown($event)"
451
+ >
452
+ @if (iconTemplate(); as tpl) {
453
+ <ng-container
454
+ [ngTemplateOutlet]="tpl"
455
+ [ngTemplateOutletContext]="childContext()"
456
+ />
457
+ } @else {
458
+ <svg
459
+ class="text-size-picker-icon"
460
+ viewBox="0 0 16 16"
461
+ width="1.05rem"
462
+ height="1.05rem"
463
+ aria-hidden="true"
464
+ fill="none"
465
+ stroke="currentColor"
466
+ stroke-width="1.6"
467
+ stroke-linecap="round"
468
+ stroke-linejoin="round"
469
+ >
470
+ <path d="M4 13 7.2 3h1.6L12 13M5.4 9.5h5.2" />
471
+ </svg>
472
+ }
473
+ </button>
474
+
475
+ <ul
476
+ #listEl
477
+ class="text-size-picker-list"
478
+ [id]="listId"
479
+ role="listbox"
480
+ [attr.aria-label]="label() || null"
481
+ [attr.aria-activedescendant]="activeDescendant()"
482
+ tabindex="-1"
483
+ [attr.hidden]="open() ? null : ''"
484
+ (keydown)="onListKeydown($event)"
485
+ >
486
+ @for (size of sizes(); track size; let i = $index) {
487
+ <li
488
+ class="text-size-picker-option"
489
+ [id]="optionId(i)"
490
+ role="option"
491
+ [attr.aria-selected]="size === value()"
492
+ [attr.data-active]="i === activeIndex() ? '' : null"
493
+ (click)="choose(i)"
494
+ >
495
+ {{ labelFor(size) }}
496
+ </li>
497
+ }
498
+ </ul>
499
+ </div>
500
+ `,
501
+ }]
502
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], sizes: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizes", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], defaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValue", required: false }] }], storageKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "storageKey", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], target: [{ type: i0.Input, args: [{ isSignal: true, alias: "target", required: false }] }], sizeLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizeLabels", required: false }] }], className: [{ type: i0.Input, args: [{ isSignal: true, alias: "className", required: false }] }], sizeChange: [{ type: i0.Output, args: ["sizeChange"] }], iconTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }], rootRef: [{ type: i0.ViewChild, args: ["rootEl", { isSignal: true }] }], buttonRef: [{ type: i0.ViewChild, args: ["buttonEl", { isSignal: true }] }], listRef: [{ type: i0.ViewChild, args: ["listEl", { isSignal: true }] }] } });
503
+
504
+ /**
505
+ * Generated bundle index. Do not edit.
506
+ */
507
+
508
+ export { TextSizePicker, TextSizePickerIcon, nextTextSizePickerId, sizeName };
509
+ //# sourceMappingURL=lilydesignsystem-angular-text-size-picker.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lilydesignsystem-angular-text-size-picker.mjs","sources":["../../text-size-picker.component.ts","../../lilydesignsystem-angular-text-size-picker.ts"],"sourcesContent":["import { NgTemplateOutlet } from \"@angular/common\";\nimport {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n DestroyRef,\n Directive,\n ElementRef,\n TemplateRef,\n computed,\n contentChild,\n effect,\n inject,\n input,\n model,\n output,\n signal,\n viewChild,\n} from \"@angular/core\";\n\n/**\n * Default button icon: a bundled SVG (a stroke-drawn \"A\"), not a\n * Unicode character. Reversed 2026-09-16 from the font-dependent-glyph\n * convention (was the plain letter U+0041, exported as\n * `LATIN_CAPITAL_LETTER_A` — removed, not renamed). \"A\" itself needed\n * no escaping and had no font-fallback risk, but it still varied in\n * weight and proportions across font stacks; a bundled outline SVG\n * matches the other four picker icons as one consistent visual family\n * regardless of the consumer's fonts.\n */\n\n/** Context passed to a custom icon `<ng-template>` (the button icon). */\nexport type ChildArgs = {\n /** Currently selected size slug. */\n value: string;\n /** Is the listbox open? */\n open: boolean;\n /** Resolve a slug to its display label. */\n labelFor: (size: string) => string;\n};\n\n/**\n * Resolve a size slug to its display label: each hyphen-separated word\n * title-cased, so a slug like \"x-large\" renders as \"X Large\". Mirrors\n * `themeName` in theme-picker and `localeName` in locale-picker.\n */\nexport function sizeName(size: string): string {\n return size\n .split(\"-\")\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(\" \");\n}\n\nlet uid = 0;\n/** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */\nexport function nextTextSizePickerId(): string {\n uid += 1;\n return `text-size-picker-${uid}`;\n}\n\n/**\n * Optional marker for the projected icon template. Gives consumers typed\n * `let-` variables:\n *\n * ```html\n * <lily-text-size-picker ...>\n * <ng-template lilyTextSizePickerIcon let-args>{{ args.labelFor(args.value) }}</ng-template>\n * </lily-text-size-picker>\n * ```\n *\n * The component queries any projected `<ng-template>`, so the marker is\n * for type-checking and readability, not for matching.\n */\n@Directive({\n selector: \"ng-template[lilyTextSizePickerIcon]\",\n standalone: true,\n})\nexport class TextSizePickerIcon {\n static ngTemplateContextGuard(\n _dir: TextSizePickerIcon,\n _ctx: unknown,\n ): _ctx is ChildArgs & { $implicit: ChildArgs } {\n return true;\n }\n}\n\n/**\n * TextSizePicker — `data-text-size` text-size picker.\n *\n * Renders an icon button that opens a WAI-ARIA APG listbox of sizes. On\n * every size change the component sets `data-text-size=\"{slug}\"` on the\n * document root (or on a consumer-supplied target), with optional\n * `localStorage` persistence. Ships no CSS; the consumer maps each\n * `[data-text-size=\"…\"]` slug to real typography. See `spec/index.md`\n * for the full contract.\n */\n@Component({\n selector: \"lily-text-size-picker\",\n standalone: true,\n imports: [NgTemplateOutlet],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n \"(document:click)\": \"onDocumentClick($event)\",\n },\n template: `\n <div\n #rootEl\n class=\"text-size-picker {{ className() }}\"\n (focusout)=\"onRootFocusOut($event)\"\n >\n <input type=\"hidden\" [name]=\"name()\" [value]=\"value()\" />\n\n <button\n #buttonEl\n type=\"button\"\n class=\"text-size-picker-button\"\n [attr.aria-label]=\"label() || null\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"listId\"\n (click)=\"toggle()\"\n (keydown)=\"onButtonKeydown($event)\"\n >\n @if (iconTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"childContext()\"\n />\n } @else {\n <svg\n class=\"text-size-picker-icon\"\n viewBox=\"0 0 16 16\"\n width=\"1.05rem\"\n height=\"1.05rem\"\n aria-hidden=\"true\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"M4 13 7.2 3h1.6L12 13M5.4 9.5h5.2\" />\n </svg>\n }\n </button>\n\n <ul\n #listEl\n class=\"text-size-picker-list\"\n [id]=\"listId\"\n role=\"listbox\"\n [attr.aria-label]=\"label() || null\"\n [attr.aria-activedescendant]=\"activeDescendant()\"\n tabindex=\"-1\"\n [attr.hidden]=\"open() ? null : ''\"\n (keydown)=\"onListKeydown($event)\"\n >\n @for (size of sizes(); track size; let i = $index) {\n <li\n class=\"text-size-picker-option\"\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"size === value()\"\n [attr.data-active]=\"i === activeIndex() ? '' : null\"\n (click)=\"choose(i)\"\n >\n {{ labelFor(size) }}\n </li>\n }\n </ul>\n </div>\n `,\n})\nexport class TextSizePicker {\n readonly label = input.required<string>();\n readonly sizes = input.required<string[]>();\n readonly value = model<string>(\"\");\n readonly defaultValue = input<string>(\"\");\n readonly storageKey = input<string>(\"\");\n readonly name = input<string>(\"text-size\");\n readonly target = input<HTMLElement | null>(null);\n readonly sizeLabels = input<Record<string, string>>({});\n readonly className = input<string>(\"\");\n readonly sizeChange = output<string>();\n\n /** Projected icon template; replaces the default icon when supplied. */\n protected readonly iconTemplate = contentChild(TemplateRef);\n\n private readonly rootRef =\n viewChild.required<ElementRef<HTMLDivElement>>(\"rootEl\");\n private readonly buttonRef =\n viewChild.required<ElementRef<HTMLButtonElement>>(\"buttonEl\");\n private readonly listRef =\n viewChild.required<ElementRef<HTMLUListElement>>(\"listEl\");\n\n private readonly baseId = nextTextSizePickerId();\n protected readonly listId = `${this.baseId}-list`;\n\n protected readonly open = signal(false);\n protected readonly activeIndex = signal(-1);\n\n /** `aria-activedescendant` is only meaningful while the listbox is open. */\n protected readonly activeDescendant = computed(() => {\n const i = this.activeIndex();\n return this.open() && i >= 0 ? this.optionId(i) : null;\n });\n\n protected readonly childContext = computed(() => {\n const args: ChildArgs = {\n value: this.value(),\n open: this.open(),\n labelFor: (size: string) => this.labelFor(size),\n };\n return { $implicit: args, ...args };\n });\n\n // Typeahead buffer: APG listbox behaviour. Reset after a pause.\n private typeahead = \"\";\n private typeaheadTimer: ReturnType<typeof setTimeout> | undefined;\n\n private initialised = false;\n\n constructor() {\n inject(DestroyRef).onDestroy(() => clearTimeout(this.typeaheadTimer));\n\n effect(() => {\n const current = this.value();\n\n if (!this.initialised) {\n this.initialised = true;\n let initial = current;\n\n const sk = this.storageKey();\n if (!initial && sk) {\n try {\n initial =\n (typeof localStorage !== \"undefined\"\n ? localStorage.getItem(sk)\n : null) ?? \"\";\n } catch {\n // ignore privacy errors\n }\n }\n\n if (!initial) {\n const sizes = this.sizes();\n const dv = this.defaultValue();\n initial =\n dv || (sizes.includes(\"medium\") ? \"medium\" : sizes[0]) || \"\";\n }\n\n if (initial && initial !== current) {\n this.value.set(initial);\n return;\n }\n }\n\n if (current) this.applySize(current);\n });\n }\n\n protected optionId(index: number): string {\n return `${this.baseId}-option-${index}`;\n }\n\n labelFor(size: string): string {\n const labels = this.sizeLabels();\n if (size in labels) return labels[size];\n return sizeName(size);\n }\n\n // ---------------------------------------------------------------\n // Open / close\n // ---------------------------------------------------------------\n\n /** Under zoneless change detection the signal write that un-hides the\n * list has not reached the DOM when a microtask runs, so focusing the\n * still-hidden list fails silently and Escape lands on the button.\n * Flushing detection first is the Angular equivalent of Svelte's\n * synchronous update (same fix the date-time-picker port recorded). */\n private readonly cdr = inject(ChangeDetectorRef);\n\n protected toggle(): void {\n if (this.open()) this.closeList();\n else this.openList();\n }\n\n /** Open the listbox, activating `startIndex` (default: the selection). */\n openList(startIndex?: number): void {\n const selected = this.sizes().indexOf(this.value());\n // An empty list has no option to activate; -1 keeps\n // aria-activedescendant off rather than pointing at an id that\n // does not exist.\n this.activeIndex.set(\n this.sizes().length === 0\n ? -1\n : (startIndex ?? (selected >= 0 ? selected : 0)),\n );\n this.open.set(true);\n // Focus moves to the listbox; the active option is conveyed via\n // aria-activedescendant, per the APG listbox pattern.\n this.cdr.detectChanges();\n queueMicrotask(() => {\n this.listRef().nativeElement.focus({ preventScroll: true });\n this.scrollActiveIntoView();\n });\n }\n\n /** Close the listbox; `refocus` returns focus to the button. */\n closeList(refocus = true): void {\n if (!this.open()) return;\n this.open.set(false);\n this.activeIndex.set(-1);\n if (refocus) queueMicrotask(() => this.buttonRef().nativeElement.focus({ preventScroll: true }));\n }\n\n protected choose(index: number): void {\n const slug = this.sizes()[index];\n if (slug) this.value.set(slug);\n this.closeList();\n }\n\n private scrollActiveIntoView(): void {\n const i = this.activeIndex();\n if (i < 0) return;\n const el = this.listRef().nativeElement.children[i] as\n HTMLElement | undefined;\n // jsdom does not implement scrollIntoView; call it only if present.\n el?.scrollIntoView?.({ block: \"nearest\" });\n }\n\n private moveActive(delta: number): void {\n const count = this.sizes().length;\n if (count === 0) return;\n // Clamp rather than wrap, per the APG listbox pattern.\n this.activeIndex.set(\n Math.min(Math.max(this.activeIndex() + delta, 0), count - 1),\n );\n this.scrollActiveIntoView();\n }\n\n private runTypeahead(char: string): void {\n const lower = char.toLowerCase();\n // APG listbox typeahead: a single character moves to the NEXT\n // option starting with it, and repeating that character keeps\n // cycling. Only a buffer of differing characters refines the\n // match, and that buffer stays anchored on the active option.\n const sameCharRun =\n this.typeahead === \"\" || [...this.typeahead].every((c) => c === lower);\n this.typeahead += lower;\n clearTimeout(this.typeaheadTimer);\n this.typeaheadTimer = setTimeout(() => (this.typeahead = \"\"), 500);\n const query = sameCharRun ? lower : this.typeahead;\n const sizes = this.sizes();\n const anchor = this.activeIndex() < 0 ? 0 : this.activeIndex();\n const start = sameCharRun ? anchor + 1 : anchor;\n // Search forward, wrapping once — typeahead wraps even though the\n // arrows clamp, or options above the cursor would be untypable.\n for (let n = 0; n < sizes.length; n++) {\n const i = (start + n) % sizes.length;\n if (this.labelFor(sizes[i]).toLowerCase().startsWith(query)) {\n this.activeIndex.set(i);\n this.scrollActiveIntoView();\n return;\n }\n }\n }\n\n // ---------------------------------------------------------------\n // Keyboard\n // ---------------------------------------------------------------\n\n protected onButtonKeydown(event: KeyboardEvent): void {\n switch (event.key) {\n case \"ArrowDown\":\n case \"Enter\":\n case \" \":\n event.preventDefault();\n this.openList();\n break;\n case \"ArrowUp\":\n event.preventDefault();\n this.openList(this.sizes().length - 1);\n break;\n }\n }\n\n protected onListKeydown(event: KeyboardEvent): void {\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n this.moveActive(1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n this.moveActive(-1);\n break;\n case \"Home\":\n event.preventDefault();\n this.activeIndex.set(0);\n this.scrollActiveIntoView();\n break;\n case \"End\":\n event.preventDefault();\n this.activeIndex.set(this.sizes().length - 1);\n this.scrollActiveIntoView();\n break;\n case \"Enter\":\n case \" \":\n event.preventDefault();\n if (this.activeIndex() >= 0) this.choose(this.activeIndex());\n break;\n case \"Escape\":\n event.preventDefault();\n this.closeList();\n break;\n case \"PageUp\":\n event.preventDefault();\n this.moveActive(-10);\n break;\n case \"PageDown\":\n // ±10, clamped: an APG-optional key for long size lists.\n event.preventDefault();\n this.moveActive(10);\n break;\n case \"Tab\":\n // Tab moves on — but focus goes to the button FIRST, without\n // cancelling the key. Hiding the focused list drops focus to\n // <body>, and the browser then computes the default Tab move\n // from the top of the document, so tabbing out of an open\n // picker teleported the user to the page's first tab stop.\n // From the button, the default Tab lands exactly where leaving\n // the picker should. The button always exists, so no\n // detectChanges is needed before the focus move; guard the\n // METHOD because jsdom-shaped hosts may not implement it.\n this.buttonRef().nativeElement.focus?.({ preventScroll: true });\n this.closeList(false);\n break;\n default:\n if (\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey &&\n !event.altKey\n ) {\n this.runTypeahead(event.key);\n }\n }\n }\n\n protected onRootFocusOut(event: FocusEvent): void {\n const next = event.relatedTarget as Node | null;\n if (next && this.rootRef().nativeElement.contains(next)) return;\n this.closeList(false);\n }\n\n protected onDocumentClick(event: Event): void {\n if (!this.open()) return;\n const t = event.target as Node | null;\n if (t && !this.rootRef().nativeElement.contains(t)) this.closeList(false);\n }\n\n // ---------------------------------------------------------------\n // Apply\n // ---------------------------------------------------------------\n\n private applySize(slug: string): void {\n if (typeof document === \"undefined\" || !slug) return;\n (this.target() ?? document.documentElement).setAttribute(\n \"data-text-size\",\n slug,\n );\n\n const sk = this.storageKey();\n if (sk) {\n try {\n localStorage.setItem(sk, slug);\n } catch {\n // ignore quota / privacy errors\n }\n }\n this.sizeChange.emit(slug);\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAyCA;;;;AAIG;AACG,SAAU,QAAQ,CAAC,IAAY,EAAA;AACnC,IAAA,OAAO;SACJ,KAAK,CAAC,GAAG;SACT,GAAG,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;SAC1D,IAAI,CAAC,GAAG,CAAC;AACd;AAEA,IAAI,GAAG,GAAG,CAAC;AACX;SACgB,oBAAoB,GAAA;IAClC,GAAG,IAAI,CAAC;IACR,OAAO,CAAA,iBAAA,EAAoB,GAAG,CAAA,CAAE;AAClC;AAEA;;;;;;;;;;;;AAYG;MAKU,kBAAkB,CAAA;AAC7B,IAAA,OAAO,sBAAsB,CAC3B,IAAwB,EACxB,IAAa,EAAA;AAEb,QAAA,OAAO,IAAI;IACb;8GANW,kBAAkB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;kGAAlB,kBAAkB,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,qCAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA,CAAA;;2FAAlB,kBAAkB,EAAA,UAAA,EAAA,CAAA;kBAJ9B,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,qCAAqC;AAC/C,oBAAA,UAAU,EAAE,IAAI;AACjB,iBAAA;;AAUD;;;;;;;;;AASG;MA8EU,cAAc,CAAA;AAiDzB,IAAA,WAAA,GAAA;QAhDS,IAAA,CAAA,KAAK,GAAG,KAAK,CAAC,QAAQ;kFAAU;QAChC,IAAA,CAAA,KAAK,GAAG,KAAK,CAAC,QAAQ;kFAAY;QAClC,IAAA,CAAA,KAAK,GAAG,KAAK,CAAS,EAAE;kFAAC;QACzB,IAAA,CAAA,YAAY,GAAG,KAAK,CAAS,EAAE;yFAAC;QAChC,IAAA,CAAA,UAAU,GAAG,KAAK,CAAS,EAAE;uFAAC;QAC9B,IAAA,CAAA,IAAI,GAAG,KAAK,CAAS,WAAW;iFAAC;QACjC,IAAA,CAAA,MAAM,GAAG,KAAK,CAAqB,IAAI;mFAAC;QACxC,IAAA,CAAA,UAAU,GAAG,KAAK,CAAyB,EAAE;uFAAC;QAC9C,IAAA,CAAA,SAAS,GAAG,KAAK,CAAS,EAAE;sFAAC;QAC7B,IAAA,CAAA,UAAU,GAAG,MAAM,EAAU;;QAGnB,IAAA,CAAA,YAAY,GAAG,YAAY,CAAC,WAAW;yFAAC;AAE1C,QAAA,IAAA,CAAA,OAAO,GACtB,SAAS,CAAC,QAAQ,CAA6B,QAAQ;oFAAC;AACzC,QAAA,IAAA,CAAA,SAAS,GACxB,SAAS,CAAC,QAAQ,CAAgC,UAAU;sFAAC;AAC9C,QAAA,IAAA,CAAA,OAAO,GACtB,SAAS,CAAC,QAAQ,CAA+B,QAAQ;oFAAC;QAE3C,IAAA,CAAA,MAAM,GAAG,oBAAoB,EAAE;AAC7B,QAAA,IAAA,CAAA,MAAM,GAAG,CAAA,EAAG,IAAI,CAAC,MAAM,OAAO;QAE9B,IAAA,CAAA,IAAI,GAAG,MAAM,CAAC,KAAK;iFAAC;AACpB,QAAA,IAAA,CAAA,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC;wFAAC;;AAGxB,QAAA,IAAA,CAAA,gBAAgB,GAAG,QAAQ,CAAC,MAAK;AAClD,YAAA,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE;YAC5B,OAAO,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI;QACxD,CAAC;6FAAC;AAEiB,QAAA,IAAA,CAAA,YAAY,GAAG,QAAQ,CAAC,MAAK;AAC9C,YAAA,MAAM,IAAI,GAAc;AACtB,gBAAA,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE;AACnB,gBAAA,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;gBACjB,QAAQ,EAAE,CAAC,IAAY,KAAK,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;aAChD;YACD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE;QACrC,CAAC;yFAAC;;QAGM,IAAA,CAAA,SAAS,GAAG,EAAE;QAGd,IAAA,CAAA,WAAW,GAAG,KAAK;;;;AAuD3B;;;;AAIuE;AACtD,QAAA,IAAA,CAAA,GAAG,GAAG,MAAM,CAAC,iBAAiB,CAAC;AAzD9C,QAAA,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,MAAM,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAErE,MAAM,CAAC,MAAK;AACV,YAAA,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,EAAE;AAE5B,YAAA,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE;AACrB,gBAAA,IAAI,CAAC,WAAW,GAAG,IAAI;gBACvB,IAAI,OAAO,GAAG,OAAO;AAErB,gBAAA,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE;AAC5B,gBAAA,IAAI,CAAC,OAAO,IAAI,EAAE,EAAE;AAClB,oBAAA,IAAI;wBACF,OAAO;4BACL,CAAC,OAAO,YAAY,KAAK;AACvB,kCAAE,YAAY,CAAC,OAAO,CAAC,EAAE;AACzB,kCAAE,IAAI,KAAK,EAAE;oBACnB;AAAE,oBAAA,MAAM;;oBAER;gBACF;gBAEA,IAAI,CAAC,OAAO,EAAE;AACZ,oBAAA,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE;AAC1B,oBAAA,MAAM,EAAE,GAAG,IAAI,CAAC,YAAY,EAAE;oBAC9B,OAAO;wBACL,EAAE,KAAK,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE;gBAChE;AAEA,gBAAA,IAAI,OAAO,IAAI,OAAO,KAAK,OAAO,EAAE;AAClC,oBAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;oBACvB;gBACF;YACF;AAEA,YAAA,IAAI,OAAO;AAAE,gBAAA,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;AACtC,QAAA,CAAC,CAAC;IACJ;AAEU,IAAA,QAAQ,CAAC,KAAa,EAAA;AAC9B,QAAA,OAAO,GAAG,IAAI,CAAC,MAAM,CAAA,QAAA,EAAW,KAAK,EAAE;IACzC;AAEA,IAAA,QAAQ,CAAC,IAAY,EAAA;AACnB,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE;QAChC,IAAI,IAAI,IAAI,MAAM;AAAE,YAAA,OAAO,MAAM,CAAC,IAAI,CAAC;AACvC,QAAA,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB;IAaU,MAAM,GAAA;QACd,IAAI,IAAI,CAAC,IAAI,EAAE;YAAE,IAAI,CAAC,SAAS,EAAE;;YAC5B,IAAI,CAAC,QAAQ,EAAE;IACtB;;AAGA,IAAA,QAAQ,CAAC,UAAmB,EAAA;AAC1B,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;;;;AAInD,QAAA,IAAI,CAAC,WAAW,CAAC,GAAG,CAClB,IAAI,CAAC,KAAK,EAAE,CAAC,MAAM,KAAK;cACpB,CAAC;AACH,eAAG,UAAU,KAAK,QAAQ,IAAI,CAAC,GAAG,QAAQ,GAAG,CAAC,CAAC,CAAC,CACnD;AACD,QAAA,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;;;AAGnB,QAAA,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE;QACxB,cAAc,CAAC,MAAK;AAClB,YAAA,IAAI,CAAC,OAAO,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;YAC3D,IAAI,CAAC,oBAAoB,EAAE;AAC7B,QAAA,CAAC,CAAC;IACJ;;IAGA,SAAS,CAAC,OAAO,GAAG,IAAI,EAAA;AACtB,QAAA,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE;AAClB,QAAA,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;QACpB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACxB,QAAA,IAAI,OAAO;YAAE,cAAc,CAAC,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAClG;AAEU,IAAA,MAAM,CAAC,KAAa,EAAA;QAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC;AAChC,QAAA,IAAI,IAAI;AAAE,YAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;QAC9B,IAAI,CAAC,SAAS,EAAE;IAClB;IAEQ,oBAAoB,GAAA;AAC1B,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE;QAC5B,IAAI,CAAC,GAAG,CAAC;YAAE;AACX,QAAA,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CACzB;;QAEzB,EAAE,EAAE,cAAc,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAC5C;AAEQ,IAAA,UAAU,CAAC,KAAa,EAAA;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,MAAM;QACjC,IAAI,KAAK,KAAK,CAAC;YAAE;;AAEjB,QAAA,IAAI,CAAC,WAAW,CAAC,GAAG,CAClB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAC7D;QACD,IAAI,CAAC,oBAAoB,EAAE;IAC7B;AAEQ,IAAA,YAAY,CAAC,IAAY,EAAA;AAC/B,QAAA,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE;;;;;QAKhC,MAAM,WAAW,GACf,IAAI,CAAC,SAAS,KAAK,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,KAAK,CAAC;AACxE,QAAA,IAAI,CAAC,SAAS,IAAI,KAAK;AACvB,QAAA,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC;AACjC,QAAA,IAAI,CAAC,cAAc,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC,EAAE,GAAG,CAAC;AAClE,QAAA,MAAM,KAAK,GAAG,WAAW,GAAG,KAAK,GAAG,IAAI,CAAC,SAAS;AAClD,QAAA,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE;AAC1B,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE;AAC9D,QAAA,MAAM,KAAK,GAAG,WAAW,GAAG,MAAM,GAAG,CAAC,GAAG,MAAM;;;AAG/C,QAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;YACrC,MAAM,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM;AACpC,YAAA,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE;AAC3D,gBAAA,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;gBACvB,IAAI,CAAC,oBAAoB,EAAE;gBAC3B;YACF;QACF;IACF;;;;AAMU,IAAA,eAAe,CAAC,KAAoB,EAAA;AAC5C,QAAA,QAAQ,KAAK,CAAC,GAAG;AACf,YAAA,KAAK,WAAW;AAChB,YAAA,KAAK,OAAO;AACZ,YAAA,KAAK,GAAG;gBACN,KAAK,CAAC,cAAc,EAAE;gBACtB,IAAI,CAAC,QAAQ,EAAE;gBACf;AACF,YAAA,KAAK,SAAS;gBACZ,KAAK,CAAC,cAAc,EAAE;AACtB,gBAAA,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;gBACtC;;IAEN;AAEU,IAAA,aAAa,CAAC,KAAoB,EAAA;AAC1C,QAAA,QAAQ,KAAK,CAAC,GAAG;AACf,YAAA,KAAK,WAAW;gBACd,KAAK,CAAC,cAAc,EAAE;AACtB,gBAAA,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;gBAClB;AACF,YAAA,KAAK,SAAS;gBACZ,KAAK,CAAC,cAAc,EAAE;AACtB,gBAAA,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;gBACnB;AACF,YAAA,KAAK,MAAM;gBACT,KAAK,CAAC,cAAc,EAAE;AACtB,gBAAA,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;gBACvB,IAAI,CAAC,oBAAoB,EAAE;gBAC3B;AACF,YAAA,KAAK,KAAK;gBACR,KAAK,CAAC,cAAc,EAAE;AACtB,gBAAA,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;gBAC7C,IAAI,CAAC,oBAAoB,EAAE;gBAC3B;AACF,YAAA,KAAK,OAAO;AACZ,YAAA,KAAK,GAAG;gBACN,KAAK,CAAC,cAAc,EAAE;AACtB,gBAAA,IAAI,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC;oBAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;gBAC5D;AACF,YAAA,KAAK,QAAQ;gBACX,KAAK,CAAC,cAAc,EAAE;gBACtB,IAAI,CAAC,SAAS,EAAE;gBAChB;AACF,YAAA,KAAK,QAAQ;gBACX,KAAK,CAAC,cAAc,EAAE;AACtB,gBAAA,IAAI,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC;gBACpB;AACF,YAAA,KAAK,UAAU;;gBAEb,KAAK,CAAC,cAAc,EAAE;AACtB,gBAAA,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;gBACnB;AACF,YAAA,KAAK,KAAK;;;;;;;;;;AAUR,gBAAA,IAAI,CAAC,SAAS,EAAE,CAAC,aAAa,CAAC,KAAK,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;AAC/D,gBAAA,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;gBACrB;AACF,YAAA;AACE,gBAAA,IACE,KAAK,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC;oBACtB,CAAC,KAAK,CAAC,OAAO;oBACd,CAAC,KAAK,CAAC,OAAO;AACd,oBAAA,CAAC,KAAK,CAAC,MAAM,EACb;AACA,oBAAA,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC;gBAC9B;;IAEN;AAEU,IAAA,cAAc,CAAC,KAAiB,EAAA;AACxC,QAAA,MAAM,IAAI,GAAG,KAAK,CAAC,aAA4B;AAC/C,QAAA,IAAI,IAAI,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE;AACzD,QAAA,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;IACvB;AAEU,IAAA,eAAe,CAAC,KAAY,EAAA;AACpC,QAAA,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE;AAClB,QAAA,MAAM,CAAC,GAAG,KAAK,CAAC,MAAqB;AACrC,QAAA,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC;AAAE,YAAA,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;IAC3E;;;;AAMQ,IAAA,SAAS,CAAC,IAAY,EAAA;AAC5B,QAAA,IAAI,OAAO,QAAQ,KAAK,WAAW,IAAI,CAAC,IAAI;YAAE;AAC9C,QAAA,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,QAAQ,CAAC,eAAe,EAAE,YAAY,CACtD,gBAAgB,EAChB,IAAI,CACL;AAED,QAAA,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE;QAC5B,IAAI,EAAE,EAAE;AACN,YAAA,IAAI;AACF,gBAAA,YAAY,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,CAAC;YAChC;AAAE,YAAA,MAAM;;YAER;QACF;AACA,QAAA,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;IAC5B;8GArTW,cAAc,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA,CAAA;kGAAd,cAAc,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,uBAAA,EAAA,MAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,YAAA,EAAA,EAAA,iBAAA,EAAA,cAAA,EAAA,UAAA,EAAA,cAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,UAAA,EAAA,EAAA,iBAAA,EAAA,YAAA,EAAA,UAAA,EAAA,YAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,IAAA,EAAA,EAAA,iBAAA,EAAA,MAAA,EAAA,UAAA,EAAA,MAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,MAAA,EAAA,EAAA,iBAAA,EAAA,QAAA,EAAA,UAAA,EAAA,QAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,UAAA,EAAA,EAAA,iBAAA,EAAA,YAAA,EAAA,UAAA,EAAA,YAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,SAAA,EAAA,EAAA,iBAAA,EAAA,WAAA,EAAA,UAAA,EAAA,WAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,OAAA,EAAA,EAAA,KAAA,EAAA,aAAA,EAAA,UAAA,EAAA,YAAA,EAAA,EAAA,IAAA,EAAA,EAAA,SAAA,EAAA,EAAA,gBAAA,EAAA,yBAAA,EAAA,EAAA,EAAA,OAAA,EAAA,CAAA,EAAA,YAAA,EAAA,cAAA,EAAA,KAAA,EAAA,IAAA,EAAA,SAAA,EAasB,WAAW,EAAA,WAAA,EAAA,IAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,WAAA,EAAA,CAAA,EAAA,YAAA,EAAA,SAAA,EAAA,KAAA,EAAA,IAAA,EAAA,SAAA,EAAA,CAAA,QAAA,CAAA,EAAA,WAAA,EAAA,IAAA,EAAA,QAAA,EAAA,IAAA,EAAA,EAAA,EAAA,YAAA,EAAA,WAAA,EAAA,KAAA,EAAA,IAAA,EAAA,SAAA,EAAA,CAAA,UAAA,CAAA,EAAA,WAAA,EAAA,IAAA,EAAA,QAAA,EAAA,IAAA,EAAA,EAAA,EAAA,YAAA,EAAA,SAAA,EAAA,KAAA,EAAA,IAAA,EAAA,SAAA,EAAA,CAAA,QAAA,CAAA,EAAA,WAAA,EAAA,IAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EAlFhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmET,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAxES,gBAAgB,EAAA,QAAA,EAAA,oBAAA,EAAA,MAAA,EAAA,CAAA,yBAAA,EAAA,kBAAA,EAAA,0BAAA,CAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA,CAAA;;2FA0Ef,cAAc,EAAA,UAAA,EAAA,CAAA;kBA7E1B,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,uBAAuB;AACjC,oBAAA,UAAU,EAAE,IAAI;oBAChB,OAAO,EAAE,CAAC,gBAAgB,CAAC;oBAC3B,eAAe,EAAE,uBAAuB,CAAC,MAAM;AAC/C,oBAAA,IAAI,EAAE;AACJ,wBAAA,kBAAkB,EAAE,yBAAyB;AAC9C,qBAAA;AACD,oBAAA,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmET,EAAA,CAAA;AACF,iBAAA;AAcgD,SAAA,CAAA,EAAA,cAAA,EAAA,MAAA,EAAA,EAAA,cAAA,EAAA,EAAA,KAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,CAAA,EAAA,KAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,CAAA,EAAA,KAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,OAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,EAAA,CAAA,MAAA,EAAA,IAAA,EAAA,CAAA,aAAA,CAAA,EAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,cAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,UAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,YAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,IAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,MAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,MAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,QAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,UAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,YAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,SAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,WAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,UAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,MAAA,EAAA,IAAA,EAAA,CAAA,YAAA,CAAA,EAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,YAAA,EAAA,IAAA,EAAA,CAAA,EAAA,CAAA,UAAA,CAAA,MAAA,WAAW,CAAA,EAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,CAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,SAAA,EAAA,IAAA,EAAA,CAGT,QAAQ,EAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,CAAA,EAAA,SAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,SAAA,EAAA,IAAA,EAAA,CAEL,UAAU,iEAEX,QAAQ,EAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,CAAA,EAAA,EAAA,CAAA;;ACjM7D;;AAEG;;"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@lilydesignsystem/angular-text-size-picker",
3
+ "version": "0.1.0",
4
+ "engines": {
5
+ "node": "=26"
6
+ },
7
+ "description": "Lily Design System Angular 20 text size picker: an icon button opening an APG listbox that sets data-text-size on the document. Headless, SSR-safe, no CSS.",
8
+ "peerDependencies": {
9
+ "@angular/core": ">=20.0.0 <23.0.0",
10
+ "@angular/common": ">=20.0.0 <23.0.0"
11
+ },
12
+ "sideEffects": false,
13
+ "keywords": [
14
+ "lily",
15
+ "design",
16
+ "system",
17
+ "angular",
18
+ "text-size",
19
+ "picker"
20
+ ],
21
+ "author": "Joel Parker Henderson <joel@joelparkerhenderson.com>",
22
+ "license": "MIT OR Apache-2.0 OR GPL-2.0-only OR GPL-3.0-only OR BSD-3-Clause",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/LilyDesignSystem/lily-design-system-angular-helpers.git",
26
+ "directory": "lily-design-system-angular-text-size-picker"
27
+ },
28
+ "homepage": "https://lilydesignsystem.com/",
29
+ "bugs": {
30
+ "url": "https://github.com/LilyDesignSystem/lily-design-system/issues"
31
+ },
32
+ "module": "fesm2022/lilydesignsystem-angular-text-size-picker.mjs",
33
+ "typings": "types/lilydesignsystem-angular-text-size-picker.d.ts",
34
+ "exports": {
35
+ "./package.json": {
36
+ "default": "./package.json"
37
+ },
38
+ ".": {
39
+ "types": "./types/lilydesignsystem-angular-text-size-picker.d.ts",
40
+ "default": "./fesm2022/lilydesignsystem-angular-text-size-picker.mjs"
41
+ }
42
+ },
43
+ "type": "module",
44
+ "dependencies": {
45
+ "tslib": "^2.3.0"
46
+ }
47
+ }
@@ -0,0 +1,124 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { TemplateRef } from '@angular/core';
3
+
4
+ /**
5
+ * Default button icon: a bundled SVG (a stroke-drawn "A"), not a
6
+ * Unicode character. Reversed 2026-09-16 from the font-dependent-glyph
7
+ * convention (was the plain letter U+0041, exported as
8
+ * `LATIN_CAPITAL_LETTER_A` — removed, not renamed). "A" itself needed
9
+ * no escaping and had no font-fallback risk, but it still varied in
10
+ * weight and proportions across font stacks; a bundled outline SVG
11
+ * matches the other four picker icons as one consistent visual family
12
+ * regardless of the consumer's fonts.
13
+ */
14
+ /** Context passed to a custom icon `<ng-template>` (the button icon). */
15
+ type ChildArgs = {
16
+ /** Currently selected size slug. */
17
+ value: string;
18
+ /** Is the listbox open? */
19
+ open: boolean;
20
+ /** Resolve a slug to its display label. */
21
+ labelFor: (size: string) => string;
22
+ };
23
+ /**
24
+ * Resolve a size slug to its display label: each hyphen-separated word
25
+ * title-cased, so a slug like "x-large" renders as "X Large". Mirrors
26
+ * `themeName` in theme-picker and `localeName` in locale-picker.
27
+ */
28
+ declare function sizeName(size: string): string;
29
+ /** Stable per-instance id prefix; SSR-safe (no Math.random / Date.now). */
30
+ declare function nextTextSizePickerId(): string;
31
+ /**
32
+ * Optional marker for the projected icon template. Gives consumers typed
33
+ * `let-` variables:
34
+ *
35
+ * ```html
36
+ * <lily-text-size-picker ...>
37
+ * <ng-template lilyTextSizePickerIcon let-args>{{ args.labelFor(args.value) }}</ng-template>
38
+ * </lily-text-size-picker>
39
+ * ```
40
+ *
41
+ * The component queries any projected `<ng-template>`, so the marker is
42
+ * for type-checking and readability, not for matching.
43
+ */
44
+ declare class TextSizePickerIcon {
45
+ static ngTemplateContextGuard(_dir: TextSizePickerIcon, _ctx: unknown): _ctx is ChildArgs & {
46
+ $implicit: ChildArgs;
47
+ };
48
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TextSizePickerIcon, never>;
49
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<TextSizePickerIcon, "ng-template[lilyTextSizePickerIcon]", never, {}, {}, never, never, true, never>;
50
+ }
51
+ /**
52
+ * TextSizePicker — `data-text-size` text-size picker.
53
+ *
54
+ * Renders an icon button that opens a WAI-ARIA APG listbox of sizes. On
55
+ * every size change the component sets `data-text-size="{slug}"` on the
56
+ * document root (or on a consumer-supplied target), with optional
57
+ * `localStorage` persistence. Ships no CSS; the consumer maps each
58
+ * `[data-text-size="…"]` slug to real typography. See `spec/index.md`
59
+ * for the full contract.
60
+ */
61
+ declare class TextSizePicker {
62
+ readonly label: _angular_core.InputSignal<string>;
63
+ readonly sizes: _angular_core.InputSignal<string[]>;
64
+ readonly value: _angular_core.ModelSignal<string>;
65
+ readonly defaultValue: _angular_core.InputSignal<string>;
66
+ readonly storageKey: _angular_core.InputSignal<string>;
67
+ readonly name: _angular_core.InputSignal<string>;
68
+ readonly target: _angular_core.InputSignal<HTMLElement | null>;
69
+ readonly sizeLabels: _angular_core.InputSignal<Record<string, string>>;
70
+ readonly className: _angular_core.InputSignal<string>;
71
+ readonly sizeChange: _angular_core.OutputEmitterRef<string>;
72
+ /** Projected icon template; replaces the default icon when supplied. */
73
+ protected readonly iconTemplate: _angular_core.Signal<TemplateRef<any> | undefined>;
74
+ private readonly rootRef;
75
+ private readonly buttonRef;
76
+ private readonly listRef;
77
+ private readonly baseId;
78
+ protected readonly listId: string;
79
+ protected readonly open: _angular_core.WritableSignal<boolean>;
80
+ protected readonly activeIndex: _angular_core.WritableSignal<number>;
81
+ /** `aria-activedescendant` is only meaningful while the listbox is open. */
82
+ protected readonly activeDescendant: _angular_core.Signal<string | null>;
83
+ protected readonly childContext: _angular_core.Signal<{
84
+ /** Currently selected size slug. */
85
+ value: string;
86
+ /** Is the listbox open? */
87
+ open: boolean;
88
+ /** Resolve a slug to its display label. */
89
+ labelFor: (size: string) => string;
90
+ $implicit: ChildArgs;
91
+ }>;
92
+ private typeahead;
93
+ private typeaheadTimer;
94
+ private initialised;
95
+ constructor();
96
+ protected optionId(index: number): string;
97
+ labelFor(size: string): string;
98
+ /** Under zoneless change detection the signal write that un-hides the
99
+ * list has not reached the DOM when a microtask runs, so focusing the
100
+ * still-hidden list fails silently and Escape lands on the button.
101
+ * Flushing detection first is the Angular equivalent of Svelte's
102
+ * synchronous update (same fix the date-time-picker port recorded). */
103
+ private readonly cdr;
104
+ protected toggle(): void;
105
+ /** Open the listbox, activating `startIndex` (default: the selection). */
106
+ openList(startIndex?: number): void;
107
+ /** Close the listbox; `refocus` returns focus to the button. */
108
+ closeList(refocus?: boolean): void;
109
+ protected choose(index: number): void;
110
+ private scrollActiveIntoView;
111
+ private moveActive;
112
+ private runTypeahead;
113
+ protected onButtonKeydown(event: KeyboardEvent): void;
114
+ protected onListKeydown(event: KeyboardEvent): void;
115
+ protected onRootFocusOut(event: FocusEvent): void;
116
+ protected onDocumentClick(event: Event): void;
117
+ private applySize;
118
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<TextSizePicker, never>;
119
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<TextSizePicker, "lily-text-size-picker", never, { "label": { "alias": "label"; "required": true; "isSignal": true; }; "sizes": { "alias": "sizes"; "required": true; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "defaultValue": { "alias": "defaultValue"; "required": false; "isSignal": true; }; "storageKey": { "alias": "storageKey"; "required": false; "isSignal": true; }; "name": { "alias": "name"; "required": false; "isSignal": true; }; "target": { "alias": "target"; "required": false; "isSignal": true; }; "sizeLabels": { "alias": "sizeLabels"; "required": false; "isSignal": true; }; "className": { "alias": "className"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "sizeChange": "sizeChange"; }, ["iconTemplate"], never, true, never>;
120
+ }
121
+
122
+ export { TextSizePicker, TextSizePickerIcon, nextTextSizePickerId, sizeName };
123
+ export type { ChildArgs };
124
+ //# sourceMappingURL=lilydesignsystem-angular-text-size-picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lilydesignsystem-angular-text-size-picker.d.ts","sources":["../../text-size-picker.component.ts"],"mappings":";;;AAoBA;;;;;;;;;AASG;AAEH;AACM,KAAM,SAAS;;;;;;AAMnB;;AAGF;;;;AAIG;AACH,iBAAgB,QAAQ;AAQxB;AACA,iBAAgB,oBAAoB;AAKpC;;;;;;;;;;;;AAYG;AACH,cAIa,kBAAkB;AAC7B,wCACQ,kBAAkB,0BAEf,SAAS;mBAAgB,SAAS;AAAE;oDAJpC,kBAAkB;sDAAlB,kBAAkB;AAO9B;AAED;;;;;;;;;AASG;AACH,cA6Ea,cAAc;oBACX,aAAA,CAAA,WAAA;oBACA,aAAA,CAAA,WAAA;oBACA,aAAA,CAAA,WAAA;2BACO,aAAA,CAAA,WAAA;yBACF,aAAA,CAAA,WAAA;mBACN,aAAA,CAAA,WAAA;qBACE,aAAA,CAAA,WAAA,CAAA,WAAA;yBACI,aAAA,CAAA,WAAA,CAAA,MAAA;wBACD,aAAA,CAAA,WAAA;yBACC,aAAA,CAAA,gBAAA;;AAGnB,qCAA+B,aAAA,CAAA,MAAA,CAAA,WAAA;AAE/B;AAEA;AAEA;AAGA;AACA;AAEA,6BAAuB,aAAA,CAAA,cAAA;AACvB,oCAA8B,aAAA,CAAA,cAAA;;AAG9B,yCAAmC,aAAA,CAAA,MAAA;qCAKJ,aAAA,CAAA,MAAA;;;;;;AAzKrB;;AAgLP;;;;;AA+CH;AAIA;AAUA;;;;AAIuE;AACvE;;;AAQA;;AAqBA;AAOA;AAMA;AASA;AAUA;AA+BA,qCAAiC,aAAa;AAe9C,mCAA+B,aAAa;AA+D5C,oCAAgC,UAAU;AAM1C,qCAAiC,KAAK;AAUtC;oDArSW,cAAc;sDAAd,cAAc;AAsT1B;;","names":[]}