forty-cdk 0.18.0 → 0.20.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/combobox/README.md +8 -6
- package/context-menu/README.md +20 -20
- package/date-picker/README.md +3 -2
- package/dropdown-menu/README.md +20 -20
- package/fesm2022/forty-cdk-avatar.mjs +4 -1
- package/fesm2022/forty-cdk-avatar.mjs.map +1 -1
- package/fesm2022/forty-cdk-breakpoints.mjs +7 -2
- package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -1
- package/fesm2022/forty-cdk-button.mjs +21 -27
- package/fesm2022/forty-cdk-button.mjs.map +1 -1
- package/fesm2022/forty-cdk-calendar.mjs +14 -10
- package/fesm2022/forty-cdk-calendar.mjs.map +1 -1
- package/fesm2022/forty-cdk-carousel.mjs +9 -16
- package/fesm2022/forty-cdk-carousel.mjs.map +1 -1
- package/fesm2022/forty-cdk-combobox.mjs +219 -139
- package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
- package/fesm2022/forty-cdk-context-menu.mjs +6 -2
- package/fesm2022/forty-cdk-context-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-core.mjs +1008 -347
- package/fesm2022/forty-cdk-core.mjs.map +1 -1
- package/fesm2022/forty-cdk-date-field.mjs +9 -14
- package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
- package/fesm2022/forty-cdk-date-picker.mjs +7 -1
- package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
- package/fesm2022/forty-cdk-date-range-field.mjs +10 -9
- package/fesm2022/forty-cdk-date-range-field.mjs.map +1 -1
- package/fesm2022/forty-cdk-dialog.mjs +7 -2
- package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
- package/fesm2022/forty-cdk-drag-drop.mjs +3 -2
- package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
- package/fesm2022/forty-cdk-drawer.mjs +7 -2
- package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
- package/fesm2022/forty-cdk-dropdown-menu.mjs +6 -2
- package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-fieldset.mjs +54 -7
- package/fesm2022/forty-cdk-fieldset.mjs.map +1 -1
- package/fesm2022/forty-cdk-hover-card.mjs +7 -1
- package/fesm2022/forty-cdk-hover-card.mjs.map +1 -1
- package/fesm2022/forty-cdk-listbox.mjs +50 -30
- package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
- package/fesm2022/forty-cdk-menu.mjs +36 -15
- package/fesm2022/forty-cdk-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-menubar.mjs +8 -3
- package/fesm2022/forty-cdk-menubar.mjs.map +1 -1
- package/fesm2022/forty-cdk-navigation-menu.mjs +114 -34
- package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
- package/fesm2022/forty-cdk-otp-input.mjs +15 -10
- package/fesm2022/forty-cdk-otp-input.mjs.map +1 -1
- package/fesm2022/forty-cdk-popover.mjs +7 -1
- package/fesm2022/forty-cdk-popover.mjs.map +1 -1
- package/fesm2022/forty-cdk-radio-group.mjs +16 -13
- package/fesm2022/forty-cdk-radio-group.mjs.map +1 -1
- package/fesm2022/forty-cdk-scroll-area.mjs +16 -14
- package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -1
- package/fesm2022/forty-cdk-select.mjs +88 -74
- package/fesm2022/forty-cdk-select.mjs.map +1 -1
- package/fesm2022/forty-cdk-table.mjs +175 -40
- package/fesm2022/forty-cdk-table.mjs.map +1 -1
- package/fesm2022/forty-cdk-tabs.mjs +34 -12
- package/fesm2022/forty-cdk-tabs.mjs.map +1 -1
- package/fesm2022/forty-cdk-time-field.mjs +7 -2
- package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
- package/fesm2022/forty-cdk-time-picker.mjs +7 -1
- package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
- package/fesm2022/forty-cdk-time-range-field.mjs +8 -2
- package/fesm2022/forty-cdk-time-range-field.mjs.map +1 -1
- package/fesm2022/forty-cdk-toast.mjs +41 -14
- package/fesm2022/forty-cdk-toast.mjs.map +1 -1
- package/fesm2022/forty-cdk-toggle.mjs.map +1 -1
- package/fesm2022/forty-cdk-tooltip.mjs +7 -1
- package/fesm2022/forty-cdk-tooltip.mjs.map +1 -1
- package/fesm2022/forty-cdk-tree.mjs +96 -33
- package/fesm2022/forty-cdk-tree.mjs.map +1 -1
- package/fieldset/README.md +15 -3
- package/hover-card/README.md +2 -1
- package/listbox/README.md +4 -4
- package/menu/README.md +15 -2
- package/menubar/README.md +7 -7
- package/navigation-menu/README.md +3 -1
- package/package.json +1 -5
- package/popover/README.md +2 -1
- package/select/README.md +6 -3
- package/shared/README.md +59 -0
- package/table/README.md +14 -14
- package/time-picker/README.md +2 -1
- package/toast/README.md +3 -1
- package/tooltip/README.md +19 -10
- package/types/forty-cdk-avatar.d.ts +5 -0
- package/types/forty-cdk-combobox.d.ts +86 -34
- package/types/forty-cdk-context-menu.d.ts +12 -1
- package/types/forty-cdk-core.d.ts +567 -219
- package/types/forty-cdk-dropdown-menu.d.ts +12 -1
- package/types/forty-cdk-fieldset.d.ts +35 -4
- package/types/forty-cdk-listbox.d.ts +8 -3
- package/types/forty-cdk-menu.d.ts +23 -8
- package/types/forty-cdk-menubar.d.ts +20 -2
- package/types/forty-cdk-navigation-menu.d.ts +61 -3
- package/types/forty-cdk-radio-group.d.ts +9 -0
- package/types/forty-cdk-select.d.ts +10 -5
- package/types/forty-cdk-table.d.ts +37 -9
- package/types/forty-cdk-tabs.d.ts +19 -5
- package/types/forty-cdk-time-picker.d.ts +5 -0
- package/types/forty-cdk-toast.d.ts +46 -16
- package/types/forty-cdk-toggle.d.ts +5 -0
- package/types/forty-cdk-tree.d.ts +45 -7
- package/fesm2022/forty-cdk-signal-forms.mjs +0 -134
- package/fesm2022/forty-cdk-signal-forms.mjs.map +0 -1
- package/signal-forms/README.md +0 -72
- package/types/forty-cdk-signal-forms.d.ts +0 -69
package/combobox/README.md
CHANGED
|
@@ -20,11 +20,11 @@ Two anatomies share the same core:
|
|
|
20
20
|
The editable (default) anatomy — an `<input>` that filters a portaled listbox in place:
|
|
21
21
|
|
|
22
22
|
```html
|
|
23
|
-
<div forCombobox [(query)]="query" [(value)]="value">
|
|
23
|
+
<div forCombobox #combobox="forCombobox" [(query)]="query" [(value)]="value">
|
|
24
24
|
<input forComboboxInput placeholder="Search…" />
|
|
25
25
|
<button forComboboxClear>×</button>
|
|
26
26
|
|
|
27
|
-
<!-- @if (open()) { -->
|
|
27
|
+
<!-- @if (combobox.open()) { -->
|
|
28
28
|
<div forComboboxContent>
|
|
29
29
|
<div forComboboxOption [value]="item.id" [label]="item.label">
|
|
30
30
|
<span forComboboxIndicator>✓</span>
|
|
@@ -106,7 +106,7 @@ Static and `@for`-rendered options share the same registry, navigation order (DO
|
|
|
106
106
|
|
|
107
107
|
For a legacy `<form action="…">` flow, set `[name]` — the directive mirrors `[(value)]` into N `<input type="hidden">` siblings (one per array entry; zero when empty). String values land verbatim in the hidden input; object values default to `JSON.stringify` (override via `[itemToFormValue]`, see below).
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
A single-select field is modeled as the same `readonly T[]`, kept at length ≤ 1, and bound with `[formField]` directly — single mode needs no adapter. A `FieldTree<T | null>` cannot bind here; map to that shape at the edge that needs it. See [the selection value-type contract](../../../docs/selection-value-type-contract.md).
|
|
110
110
|
|
|
111
111
|
## API
|
|
112
112
|
|
|
@@ -191,6 +191,8 @@ Why the list part is required, not optional: a `role="listbox"` may only own `op
|
|
|
191
191
|
|
|
192
192
|
**Focus hand-off.** Registering a trigger opts the combobox into the standard trigger-anchored focus model: on open, focus moves into the input (the search field inside the panel); on close, focus returns to the trigger. Both moves are vetoable via `(autoFocusOnOpen)` / `(autoFocusOnClose)` on `[forCombobox]`, and the return is gated by `[returnFocus]` (default `true`). Escape stays owned by the input. See [Focus & the `(autoFocusOnOpen)` / `(autoFocusOnClose)` hooks](#focus--the-autofocusonopen--autofocusonclose-hooks).
|
|
193
193
|
|
|
194
|
+
**A trigger that registers late still owns the hand-off.** The trigger does not have to be declared before `[forComboboxContent]`, and does not have to exist when the panel first mounts — project it through `<ng-content>` from a wrapper, put it under a `@defer`, or gate it on an `@if` over loaded data. One caveat: focus moving _into_ the panel is a mount-time event, so a trigger that arrives while the panel is **already open** does not retroactively pull focus out of wherever you left it. From that moment on it does own the return focus, `(autoFocusOnClose)` and the Escape fallback, and the next open moves focus into the input as usual.
|
|
195
|
+
|
|
194
196
|
**Trigger keyboard.** Click / Enter / Space toggle (open moves focus into the input). ArrowDown opens with the first enabled option highlighted; ArrowUp opens with the last.
|
|
195
197
|
|
|
196
198
|
**Anchor preference.** With a trigger present the panel anchors to it by default. An explicit `[forComboboxAnchor]` still wins (explicit anchor → trigger → input), so you can wrap a decorated trigger box and anchor against it.
|
|
@@ -415,7 +417,7 @@ The `autocompleteMode` input mirrors the WAI-ARIA `aria-autocomplete` property:
|
|
|
415
417
|
|
|
416
418
|
Inline completion preserves the user's typed prefix as unselected and selects the appended remainder, so the next keystroke replaces the selection (matching native browser autofill behavior). Backspace deletes the selection without re-completing, so the user can always shorten the query.
|
|
417
419
|
|
|
418
|
-
> **Pure `'inline'` needs a warm cache.** `'inline'` never opens the popup (per APG — `aria-autocomplete="inline"` has no listbox), so in the default `@if (open())` anatomy no `[forComboboxOption]` ever renders and the label cache starts cold. A first keystroke into a combobox that has never been opened completes against nothing; inline completion only works once the options have rendered at least once (the user opened the popup via ArrowDown or `[openOnFocus]`, warming the cache). If completion must work from the very first keystroke, use `'both'`
|
|
420
|
+
> **Pure `'inline'` needs a warm cache.** `'inline'` never opens the popup (per APG — `aria-autocomplete="inline"` has no listbox), so in the default `@if (open())` anatomy no `[forComboboxOption]` ever renders and the label cache starts cold. A first keystroke into a combobox that has never been opened completes against nothing; inline completion only works once the options have rendered at least once (the user opened the popup via ArrowDown or `[openOnFocus]`, warming the cache). If completion must work from the very first keystroke, use `'both'` — it opens the popup, so the options render and the cache warms. Leaving `[forComboboxContent]` permanently mounted instead is not a supported shape and warns in dev mode: mount **is** the open state for this surface, so it never runs `animate.enter` / `animate.leave`, and its dismissible layer stays active while closed.
|
|
419
421
|
|
|
420
422
|
## Dismiss events
|
|
421
423
|
|
|
@@ -518,7 +520,7 @@ How navigation flows when virtualizing:
|
|
|
518
520
|
3. Your virtualizer scrolls; the directive's `@for` mounts the option for index 999.
|
|
519
521
|
4. As soon as that option registers (at the matching `posInSet`), the directive seeds `aria-activedescendant` to its id.
|
|
520
522
|
|
|
521
|
-
|
|
523
|
+
Inline autocomplete matches against the most recently rendered window overlaid with the position snapshot, so completion against off-screen labels still works. `selected().label` reads a separate, selection-keyed cache instead — bounded by the selection, so the label of a selected option survives close / re-open and any number of query rebuilds, but it is **not** resolved from the position snapshot: a value that enters the selection while its option is outside the rendered window (a `[(value)]` write restoring a saved selection, say) falls back to `[itemToStringLabel]` until that option renders once. Supply `[itemToStringLabel]` whenever the selection can be seeded from outside the list.
|
|
522
524
|
|
|
523
525
|
```html
|
|
524
526
|
<div
|
|
@@ -619,7 +621,7 @@ Implements the [WAI-ARIA Combobox pattern](https://www.w3.org/WAI/ARIA/apg/patte
|
|
|
619
621
|
- Disabled options keep the host `aria-disabled="true"`. Click and hover (activedescendant pinning) are no-ops on disabled options.
|
|
620
622
|
- `[forComboboxSeparator]` never registers with the listbox's option collection — keyboard navigation skips it automatically. It carries `role="separator"` and emits `aria-orientation` only for `orientation="vertical"`, because `horizontal` is the ARIA default; `data-orientation` is always stamped for styling. Set `decorative` when the surrounding options already convey the split — it switches the line to `role="none"` and drops `aria-orientation`, matching the [shared separator emission policy](../separator/README.md#accessibility).
|
|
621
623
|
- `[forComboboxGroup]` is purely advisory grouping — options inside still register flatly with the root, so navigation flows through groups without interruption.
|
|
622
|
-
- `[forComboboxEmpty]`
|
|
624
|
+
- `[forComboboxEmpty]` and `[forComboboxStatus]` carry `role="status"` and nothing else — the role already implies `aria-live="polite"` and `aria-atomic="true"`, so the empty-state message is announced, whole, when filtering removes all matches. The role is the channel they keep (rather than the attribute pair) because `[forComboboxEmpty]` self-hides and comes back with its message already in the DOM: a live _role_ is what screen readers read reliably on insertion.
|
|
623
625
|
- Non-option pieces (`[forComboboxAction]`, and ideally `[forComboboxEmpty]` / `[forComboboxStatus]`) belong inside `[forComboboxContent]` but **outside** `[forComboboxList]` — `role="listbox"` may only own `option` / `group` children (`aria-required-owned-elements`). Wrapping the options in a `[forComboboxList]` (the "editable + list" shape) makes those pieces siblings of the listbox. `[forComboboxAction]` **requires** a `[forComboboxList]` and throws `[forty-cdk/combobox]` without one; `[forComboboxEmpty]` / `[forComboboxStatus]` stay lenient in the bare editable anatomy (documented compromise) — see the [editable-anatomy caveat](#anatomy).
|
|
624
626
|
- The input element is exempt from the listbox's outside-pointer dismissal layer, so a click on the input while the listbox is open routes through `(click)` (toggle / focus open) instead of double-firing as an outside dismissal.
|
|
625
627
|
|
package/context-menu/README.md
CHANGED
|
@@ -117,26 +117,26 @@ Both triggers carry `[menuPositioning]`, a partial `{ side, align, sideOffset, a
|
|
|
117
117
|
|
|
118
118
|
### `ForContextMenu`
|
|
119
119
|
|
|
120
|
-
| Property | Type | Description
|
|
121
|
-
| --------------------------- | --------------------------------------------------------- |
|
|
122
|
-
| `open` | `model<boolean>` | Two-way bindable. Whether the menu is shown.<br>**Default:** `false`
|
|
123
|
-
| `side` | `input<string>` | Anchor side relative to the pointer.<br>**Default:** `'bottom'`
|
|
124
|
-
| `align` | `input<string>` | Alignment along `side` (`'start'` / `'center'` / `'end'`).<br>**Default:** `'start'`
|
|
125
|
-
| `sideOffset` | `input<number>` | Gap (px) between the pointer and the menu along the main axis.<br>**Default:** `0`
|
|
126
|
-
| `alignOffset` | `input<number>` | Gap (px) along the cross axis (parallel to `side`).<br>**Default:** `0`
|
|
127
|
-
| `fallbackAxisSideDirection` | `input<'none' \| 'start' \| 'end'>` | When both sides of the preferred axis overflow, lets `flip` drop the menu to a perpendicular side instead of clipping. `'none'` keeps only the opposite same-axis placement.<br>**Default:** `'none'`
|
|
128
|
-
| `loop` | `input<boolean>` | Whether arrow navigation wraps.<br>**Default:** `true`
|
|
129
|
-
| `dir` | `input<string>` | Writing direction. In RTL, ArrowLeft opens submenus and ArrowRight closes them — the swap is automatic. Inherited by every nested `[forMenuSub]` underneath unless overridden.<br>**Default:** `'ltr'`
|
|
130
|
-
| `disabled` | `input<boolean>` | When `true`, the contextmenu event falls through to the native browser menu.<br>**Default:** `false`
|
|
131
|
-
| `dismissible` | `input<boolean>` | When `false`, Escape and outside interactions don't close.<br>**Default:** `true`
|
|
132
|
-
| `returnFocus` | `input<boolean>` | When `true`, focus returns to the trigger element on close.<br>**Default:** `true`
|
|
133
|
-
| `ariaLabel` | `input<string \| null>` | Accessible name reflected as `aria-label` on `[forMenuContent]`. The root's only name hook for a context menu — the right-click region is never used as an `aria-labelledby` target.<br>**Default:** `null`
|
|
134
|
-
| `escapeKeyDown` | `output<VetoableNativeEvent<KeyboardEvent>>` | Output. Escape pressed while the menu is the topmost dismissible layer.<br>**Default:** —
|
|
135
|
-
| `pointerDownOutside` | `output<VetoableNativeEvent<PointerEvent>>` | Output. Pointer-down on a target outside content + trigger.<br>**Default:** —
|
|
136
|
-
| `focusOutside` | `output<VetoableNativeEvent<FocusEvent>>` | Output. Focus moves outside content + trigger.<br>**Default:** —
|
|
137
|
-
| `interactOutside` | `output<VetoableNativeEvent<PointerEvent \| FocusEvent>>` | Output. Composite — fires alongside the two above (and shares their veto state).<br>**Default:** —
|
|
138
|
-
| `autoFocusOnOpen` | `output<VetoableEvent>` | Output. Just before the imperative focus move on mount.<br>**Default:** —
|
|
139
|
-
| `autoFocusOnClose` | `output<VetoableEvent>` | Output. Just before the imperative focus move on unmount.<br>**Default:** —
|
|
120
|
+
| Property | Type | Description |
|
|
121
|
+
| --------------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
122
|
+
| `open` | `model<boolean>` | Two-way bindable. Whether the menu is shown.<br>**Default:** `false` |
|
|
123
|
+
| `side` | `input<string>` | Anchor side relative to the pointer.<br>**Default:** `'bottom'` |
|
|
124
|
+
| `align` | `input<string>` | Alignment along `side` (`'start'` / `'center'` / `'end'`).<br>**Default:** `'start'` |
|
|
125
|
+
| `sideOffset` | `input<number>` | Gap (px) between the pointer and the menu along the main axis.<br>**Default:** `0` |
|
|
126
|
+
| `alignOffset` | `input<number>` | Gap (px) along the cross axis (parallel to `side`).<br>**Default:** `0` |
|
|
127
|
+
| `fallbackAxisSideDirection` | `input<'none' \| 'start' \| 'end'>` | When both sides of the preferred axis overflow, lets `flip` drop the menu to a perpendicular side instead of clipping. `'none'` keeps only the opposite same-axis placement. The default is read from `provideForContextMenuDefaults` for the surrounding scope — set it once for the whole app rather than per call site.<br>**Default:** `'none'` |
|
|
128
|
+
| `loop` | `input<boolean>` | Whether arrow navigation wraps.<br>**Default:** `true` |
|
|
129
|
+
| `dir` | `input<string>` | Writing direction. In RTL, ArrowLeft opens submenus and ArrowRight closes them — the swap is automatic. Inherited by every nested `[forMenuSub]` underneath unless overridden.<br>**Default:** `'ltr'` |
|
|
130
|
+
| `disabled` | `input<boolean>` | When `true`, the contextmenu event falls through to the native browser menu.<br>**Default:** `false` |
|
|
131
|
+
| `dismissible` | `input<boolean>` | When `false`, Escape and outside interactions don't close.<br>**Default:** `true` |
|
|
132
|
+
| `returnFocus` | `input<boolean>` | When `true`, focus returns to the trigger element on close.<br>**Default:** `true` |
|
|
133
|
+
| `ariaLabel` | `input<string \| null>` | Accessible name reflected as `aria-label` on `[forMenuContent]`. The root's only name hook for a context menu — the right-click region is never used as an `aria-labelledby` target.<br>**Default:** `null` |
|
|
134
|
+
| `escapeKeyDown` | `output<VetoableNativeEvent<KeyboardEvent>>` | Output. Escape pressed while the menu is the topmost dismissible layer.<br>**Default:** — |
|
|
135
|
+
| `pointerDownOutside` | `output<VetoableNativeEvent<PointerEvent>>` | Output. Pointer-down on a target outside content + trigger.<br>**Default:** — |
|
|
136
|
+
| `focusOutside` | `output<VetoableNativeEvent<FocusEvent>>` | Output. Focus moves outside content + trigger.<br>**Default:** — |
|
|
137
|
+
| `interactOutside` | `output<VetoableNativeEvent<PointerEvent \| FocusEvent>>` | Output. Composite — fires alongside the two above (and shares their veto state).<br>**Default:** — |
|
|
138
|
+
| `autoFocusOnOpen` | `output<VetoableEvent>` | Output. Just before the imperative focus move on mount.<br>**Default:** — |
|
|
139
|
+
| `autoFocusOnClose` | `output<VetoableEvent>` | Output. Just before the imperative focus move on unmount.<br>**Default:** — |
|
|
140
140
|
|
|
141
141
|
Same vetoable dismiss API as DropdownMenu. Call `preventDefault()` on the emitted veto to suppress the directive's default action; the original DOM event, when present, is on `.event`.
|
|
142
142
|
|
package/date-picker/README.md
CHANGED
|
@@ -18,18 +18,19 @@ All date math and formatting go through a `DateAdapter<D>`, shared with `ForCale
|
|
|
18
18
|
## Anatomy
|
|
19
19
|
|
|
20
20
|
```html
|
|
21
|
-
<div forDatePicker [(value)]="date" [(open)]="open" name="dob">
|
|
21
|
+
<div forDatePicker #picker="forDatePicker" [(value)]="date" [(open)]="open" name="dob">
|
|
22
22
|
<!-- optional: wrap a decorated field box in [forDatePickerAnchor] to position against it -->
|
|
23
23
|
<button forDatePickerTrigger>
|
|
24
24
|
<span forDatePickerValue placeholder="Pick a date"></span>
|
|
25
25
|
</button>
|
|
26
26
|
|
|
27
|
-
<!--
|
|
27
|
+
<!-- @if (picker.open()) { -->
|
|
28
28
|
<div forDatePickerContent>
|
|
29
29
|
<div forCalendar [(value)]="date">
|
|
30
30
|
<!-- …calendar header + grid… -->
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
33
|
+
<!-- } -->
|
|
33
34
|
</div>
|
|
34
35
|
```
|
|
35
36
|
|
package/dropdown-menu/README.md
CHANGED
|
@@ -133,26 +133,26 @@ Angular resolves `ng-template` DI at the template's **declaration** site, not wh
|
|
|
133
133
|
|
|
134
134
|
### `ForDropdownMenu`
|
|
135
135
|
|
|
136
|
-
| Property | Type | Description
|
|
137
|
-
| --------------------------- | --------------------------------------------------------- |
|
|
138
|
-
| `open` | `model<boolean>` | Two-way bindable. Whether the menu is shown.<br>**Default:** `false`
|
|
139
|
-
| `side` | `input<string>` | Anchor side of `[forMenuContent]` against the trigger.<br>**Default:** `'bottom'`
|
|
140
|
-
| `align` | `input<string>` | Alignment along `side` (`'start'` / `'center'` / `'end'`).<br>**Default:** `'start'`
|
|
141
|
-
| `sideOffset` | `input<number>` | Gap (px) between the trigger and the content along the main axis.<br>**Default:** `4`
|
|
142
|
-
| `alignOffset` | `input<number>` | Gap (px) along the cross axis (parallel to `side`).<br>**Default:** `0`
|
|
143
|
-
| `fallbackAxisSideDirection` | `input<'none' \| 'start' \| 'end'>` | When both sides of the preferred axis overflow, lets `flip` drop the menu to a perpendicular side instead of clipping. `'none'` keeps only the opposite same-axis placement.<br>**Default:** `'none'`
|
|
144
|
-
| `loop` | `input<boolean>` | Whether arrow navigation wraps at the ends.<br>**Default:** `true`
|
|
145
|
-
| `dir` | `input<string>` | Writing direction. In RTL, ArrowLeft opens submenus and ArrowRight closes them — the swap is automatic. Inherited by every nested `[forMenuSub]` underneath unless overridden.<br>**Default:** `'ltr'`
|
|
146
|
-
| `disabled` | `input<boolean>` | When `true`, trigger interactions are ignored.<br>**Default:** `false`
|
|
147
|
-
| `dismissible` | `input<boolean>` | When `false`, Escape and outside interactions don't close.<br>**Default:** `true`
|
|
148
|
-
| `returnFocus` | `input<boolean>` | When `true`, focus returns to the trigger on close.<br>**Default:** `true`
|
|
149
|
-
| `ariaLabel` | `input<string \| null>` | Manual `aria-label` on `[forMenuContent]` if the trigger isn't a meaningful name.<br>**Default:** `null`
|
|
150
|
-
| `escapeKeyDown` | `output<VetoableNativeEvent<KeyboardEvent>>` | Output. Escape pressed while the menu is the topmost dismissible layer.<br>**Default:** —
|
|
151
|
-
| `pointerDownOutside` | `output<VetoableNativeEvent<PointerEvent>>` | Output. Pointer-down on a target outside content + trigger.<br>**Default:** —
|
|
152
|
-
| `focusOutside` | `output<VetoableNativeEvent<FocusEvent>>` | Output. Focus moves outside content + trigger.<br>**Default:** —
|
|
153
|
-
| `interactOutside` | `output<VetoableNativeEvent<PointerEvent \| FocusEvent>>` | Output. Composite — fires alongside the two above (and shares their veto state).<br>**Default:** —
|
|
154
|
-
| `autoFocusOnOpen` | `output<VetoableEvent>` | Output. Just before focus moves to the first / last enabled item on mount.<br>**Default:** —
|
|
155
|
-
| `autoFocusOnClose` | `output<VetoableEvent>` | Output. Just before focus returns to the trigger on unmount.<br>**Default:** —
|
|
136
|
+
| Property | Type | Description |
|
|
137
|
+
| --------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
138
|
+
| `open` | `model<boolean>` | Two-way bindable. Whether the menu is shown.<br>**Default:** `false` |
|
|
139
|
+
| `side` | `input<string>` | Anchor side of `[forMenuContent]` against the trigger.<br>**Default:** `'bottom'` |
|
|
140
|
+
| `align` | `input<string>` | Alignment along `side` (`'start'` / `'center'` / `'end'`).<br>**Default:** `'start'` |
|
|
141
|
+
| `sideOffset` | `input<number>` | Gap (px) between the trigger and the content along the main axis.<br>**Default:** `4` |
|
|
142
|
+
| `alignOffset` | `input<number>` | Gap (px) along the cross axis (parallel to `side`).<br>**Default:** `0` |
|
|
143
|
+
| `fallbackAxisSideDirection` | `input<'none' \| 'start' \| 'end'>` | When both sides of the preferred axis overflow, lets `flip` drop the menu to a perpendicular side instead of clipping. `'none'` keeps only the opposite same-axis placement. The default is read from `provideForDropdownMenuDefaults` for the surrounding scope — set it once for the whole app rather than per call site.<br>**Default:** `'none'` |
|
|
144
|
+
| `loop` | `input<boolean>` | Whether arrow navigation wraps at the ends.<br>**Default:** `true` |
|
|
145
|
+
| `dir` | `input<string>` | Writing direction. In RTL, ArrowLeft opens submenus and ArrowRight closes them — the swap is automatic. Inherited by every nested `[forMenuSub]` underneath unless overridden.<br>**Default:** `'ltr'` |
|
|
146
|
+
| `disabled` | `input<boolean>` | When `true`, trigger interactions are ignored.<br>**Default:** `false` |
|
|
147
|
+
| `dismissible` | `input<boolean>` | When `false`, Escape and outside interactions don't close.<br>**Default:** `true` |
|
|
148
|
+
| `returnFocus` | `input<boolean>` | When `true`, focus returns to the trigger on close.<br>**Default:** `true` |
|
|
149
|
+
| `ariaLabel` | `input<string \| null>` | Manual `aria-label` on `[forMenuContent]` if the trigger isn't a meaningful name.<br>**Default:** `null` |
|
|
150
|
+
| `escapeKeyDown` | `output<VetoableNativeEvent<KeyboardEvent>>` | Output. Escape pressed while the menu is the topmost dismissible layer.<br>**Default:** — |
|
|
151
|
+
| `pointerDownOutside` | `output<VetoableNativeEvent<PointerEvent>>` | Output. Pointer-down on a target outside content + trigger.<br>**Default:** — |
|
|
152
|
+
| `focusOutside` | `output<VetoableNativeEvent<FocusEvent>>` | Output. Focus moves outside content + trigger.<br>**Default:** — |
|
|
153
|
+
| `interactOutside` | `output<VetoableNativeEvent<PointerEvent \| FocusEvent>>` | Output. Composite — fires alongside the two above (and shares their veto state).<br>**Default:** — |
|
|
154
|
+
| `autoFocusOnOpen` | `output<VetoableEvent>` | Output. Just before focus moves to the first / last enabled item on mount.<br>**Default:** — |
|
|
155
|
+
| `autoFocusOnClose` | `output<VetoableEvent>` | Output. Just before focus returns to the trigger on unmount.<br>**Default:** — |
|
|
156
156
|
|
|
157
157
|
Every output above is vetoable — each handler receives a `VetoableEvent` (or `VetoableNativeEvent<E>` when there is a native DOM event). Call `preventDefault()` on the emitted veto to suppress the directive's default action; the original DOM event, when present, is on `.event`.
|
|
158
158
|
|
|
@@ -97,7 +97,10 @@ class ForAvatar {
|
|
|
97
97
|
return this.#timerElapsed();
|
|
98
98
|
}, /* @ts-ignore */
|
|
99
99
|
...(ngDevMode ? [{ debugName: "shouldShowFallback" }] : /* istanbul ignore next */ []));
|
|
100
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Reported by `[forAvatarImage]` so the root can transition its state
|
|
102
|
+
* machine. Consumers should not call this.
|
|
103
|
+
*/
|
|
101
104
|
reportStatus(status) {
|
|
102
105
|
this.#status.set(status);
|
|
103
106
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forty-cdk-avatar.mjs","sources":["../../../projects/forty-cdk/avatar/src/avatar-context.ts","../../../projects/forty-cdk/avatar/src/avatar-defaults.ts","../../../projects/forty-cdk/avatar/src/avatar.ts","../../../projects/forty-cdk/avatar/src/avatar-image.ts","../../../projects/forty-cdk/avatar/src/avatar-fallback.ts","../../../projects/forty-cdk/avatar/src/forty-cdk-avatar.ts"],"sourcesContent":["import { inject, InjectionToken, type Signal } from '@angular/core';\n\n/**\n * Lifecycle stages of the avatar's image:\n * - `idle`: no `src` set yet.\n * - `loading`: `src` set, waiting for the network.\n * - `loaded`: image rendered successfully.\n * - `error`: image failed to load.\n */\nexport type ForAvatarStatus = 'idle' | 'loading' | 'loaded' | 'error';\n\nexport interface ForAvatarContext {\n /** Current status of the avatar's image. */\n readonly status: Signal<ForAvatarStatus>;\n /**\n * `true` when the consumer should render the fallback. Drives `@if` in\n * the consumer's template. Honors `fallbackDelayMs` for `idle` / `loading`\n * states; `error` flips it to `true` immediately.\n */\n readonly shouldShowFallback: Signal<boolean>;\n /**\n * Internal — reported by `[forAvatarImage]` so the root can transition\n * its state machine. Consumers should not call this.\n */\n reportStatus(status: ForAvatarStatus): void;\n}\n\nexport const FOR_AVATAR_CONTEXT = new InjectionToken<ForAvatarContext>('FOR_AVATAR_CONTEXT');\n\n/**\n * Injects the nearest {@link ForAvatarContext}, throwing a descriptive error\n * when used outside a `[forAvatar]` element.\n *\n * @param piece Name of the calling directive, used in the error message.\n */\nexport function injectAvatarContext(piece: string): ForAvatarContext {\n const ctx = inject(FOR_AVATAR_CONTEXT, { optional: true });\n if (!ctx) {\n throw new Error(`[forty-cdk/avatar] ${piece} must be used inside a [forAvatar] element.`);\n }\n return ctx;\n}\n","import { type Provider } from '@angular/core';\n\nimport { createDefaults } from 'forty-cdk/core';\n\n/**\n * Defaults inherited by descendant avatars in the surrounding injector\n * scope. Configure with `provideForAvatarDefaults` either at the\n * application root or in any component's `providers` array; partial\n * overrides merge with the parent scope.\n */\nexport interface ForAvatarDefaults {\n /**\n * Milliseconds to defer the fallback for fast loads, avoiding a brief\n * \"initials → image\" flicker. Set to `0` to show immediately during\n * `idle` / `loading`, or to e.g. `500` to skip rendering during quick\n * cached loads. `error` always shows the fallback immediately.\n */\n fallbackDelayMs: number;\n}\n\n/**\n * Library fallback for avatar defaults, read at the root injector when no\n * consumer has called `provideForAvatarDefaults`. Exported for the shared defaults\n * contract spec; not re-exported from the primitive's public entry.\n */\nexport const FOR_AVATAR_FALLBACK_DEFAULTS: ForAvatarDefaults = {\n fallbackDelayMs: 0,\n};\n\nconst { token, provideDefaults } = createDefaults<ForAvatarDefaults>(\n 'FOR_AVATAR_DEFAULTS',\n FOR_AVATAR_FALLBACK_DEFAULTS,\n);\n\n/** Token holding the resolved avatar defaults for the current scope. */\nexport const FOR_AVATAR_DEFAULTS = token;\n\n/**\n * Configures forty-cdk avatar defaults for this injector scope. Partial\n * overrides inherit unspecified keys from the parent scope (or library\n * defaults at the root).\n */\nexport function provideForAvatarDefaults(defaults: Partial<ForAvatarDefaults> = {}): Provider[] {\n return provideDefaults(defaults);\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n computed,\n DestroyRef,\n Directive,\n effect,\n inject,\n input,\n linkedSignal,\n numberAttribute,\n PLATFORM_ID,\n signal,\n} from '@angular/core';\n\nimport { FOR_AVATAR_CONTEXT, type ForAvatarContext, type ForAvatarStatus } from './avatar-context';\nimport { FOR_AVATAR_DEFAULTS } from './avatar-defaults';\n\n/**\n * Headless avatar root. Tracks the load lifecycle of an inner\n * `[forAvatarImage]` and exposes `status` + `shouldShowFallback` so the\n * consumer can swap the image for fallback content (initials, generic icon)\n * via `@if`.\n *\n * No ARIA role is imposed — avatars are typically presentational. If the\n * surrounding component conveys identity (a person's name in the header,\n * a chat bubble), the consumer should provide an `aria-label` on the\n * relevant ancestor or pair the avatar with visible text.\n *\n * @example\n * ```html\n * <span forAvatar #a=\"forAvatar\" fallbackDelayMs=\"500\">\n * <img forAvatarImage [src]=\"user.avatarUrl\" [alt]=\"user.name\" />\n * @if (a.shouldShowFallback()) {\n * <span forAvatarFallback>{{ user.initials }}</span>\n * }\n * </span>\n * ```\n */\n@Directive({\n selector: '[forAvatar]',\n exportAs: 'forAvatar',\n host: {\n '[attr.data-status]': 'status()',\n },\n providers: [{ provide: FOR_AVATAR_CONTEXT, useExisting: ForAvatar }],\n})\nexport class ForAvatar implements ForAvatarContext {\n readonly #defaults = inject(FOR_AVATAR_DEFAULTS);\n\n /**\n * Milliseconds to defer the fallback for fast loads, avoiding a brief\n * \"initials → image\" flicker. Set to `0` (default) to show immediately\n * during `idle` / `loading`, or to e.g. `500` to skip rendering during\n * quick cached loads. `error` always shows the fallback immediately.\n * The default is read from `provideForAvatarDefaults` for the\n * surrounding scope.\n */\n readonly fallbackDelayMs = input(this.#defaults.fallbackDelayMs, {\n transform: (value: unknown) => {\n const parsed = numberAttribute(value);\n return Number.isFinite(parsed) ? parsed : this.#defaults.fallbackDelayMs;\n },\n });\n\n readonly #status = signal<ForAvatarStatus>('idle');\n readonly status = this.#status.asReadonly();\n\n // Whether the deferred-fallback timer has fired for the current status/delay.\n // `linkedSignal` resets it to `false` whenever the source changes (the\n // canonical replacement for resetting state inside an effect); the timer\n // callback in the constructor flips it to `true`.\n readonly #timerElapsed = linkedSignal<{ status: ForAvatarStatus; delay: number }, boolean>({\n source: () => ({ status: this.#status(), delay: this.fallbackDelayMs() }),\n computation: () => false,\n });\n #timer: ReturnType<typeof setTimeout> | null = null;\n readonly #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n\n readonly shouldShowFallback = computed(() => {\n const status = this.#status();\n if (status === 'loaded') return false;\n if (status === 'error') return true;\n // idle | loading: an immediate (delay <= 0) gate shows the fallback right\n // away; a deferred gate waits for the timer armed in the effect below.\n if (this.fallbackDelayMs() <= 0) return true;\n return this.#timerElapsed();\n });\n\n /** @internal */\n reportStatus(status: ForAvatarStatus): void {\n this.#status.set(status);\n }\n\n constructor() {\n // Imperative timer integration: the effect's only job is to (re)arm a\n // setTimeout for the deferred-fallback case (idle | loading with a positive\n // delay). The synchronous branches (loaded / error / delay <= 0) are pure\n // derivations and live in `shouldShowFallback`. The signal write happens\n // inside the timer callback, which runs outside the effect's reactive\n // scope, so this does NOT create a self-cycle in the reactive graph.\n effect(() => {\n const status = this.#status();\n const delay = this.fallbackDelayMs();\n this.#cancelTimer();\n if (this.#isBrowser && (status === 'idle' || status === 'loading') && delay > 0) {\n this.#timer = setTimeout(() => {\n this.#timer = null;\n this.#timerElapsed.set(true);\n }, delay);\n }\n });\n\n inject(DestroyRef).onDestroy(() => this.#cancelTimer());\n }\n\n #cancelTimer(): void {\n if (this.#timer !== null) {\n clearTimeout(this.#timer);\n this.#timer = null;\n }\n }\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n afterNextRender,\n DestroyRef,\n Directive,\n ElementRef,\n inject,\n output,\n PLATFORM_ID,\n} from '@angular/core';\n\nimport { FOR_AVATAR_CONTEXT, type ForAvatarStatus } from './avatar-context';\n\n/**\n * Wraps an `<img>` and reports its load lifecycle to the surrounding\n * `[forAvatar]`. The native `src` / `alt` bindings are owned by the\n * consumer — this directive only observes.\n *\n * Reflects `data-status` on the image element so the consumer can hide it\n * via CSS while loading or after an error (e.g. `img[data-status=\"error\"]\n * { display: none }`).\n */\n@Directive({\n selector: 'img[forAvatarImage]',\n exportAs: 'forAvatarImage',\n host: {\n '[attr.data-status]': 'status()',\n '(load)': 'onLoad()',\n '(error)': 'onError()',\n },\n})\nexport class ForAvatarImage {\n readonly #host = inject<ElementRef<HTMLImageElement>>(ElementRef).nativeElement;\n readonly #parent = inject(FOR_AVATAR_CONTEXT, { optional: true });\n\n /**\n * Emits whenever the image lifecycle transitions to a new state. Useful\n * if the consumer wants to log analytics, swap to a different src on\n * error, etc.\n */\n readonly loadStatusChange = output<ForAvatarStatus>();\n\n constructor() {\n if (!this.#parent) {\n throw new Error(\n '[forty-cdk/avatar] ForAvatarImage must be used inside a [forAvatar] element.',\n );\n }\n const parent = this.#parent;\n const host = this.#host;\n\n let destroyed = false;\n inject(DestroyRef).onDestroy(() => (destroyed = true));\n\n afterNextRender(() => {\n this.#syncFromAttr();\n });\n\n if (isPlatformBrowser(inject(PLATFORM_ID)) && typeof MutationObserver !== 'undefined') {\n const observer = new MutationObserver(() => {\n this.#syncFromAttr();\n });\n observer.observe(host, { attributes: true, attributeFilter: ['src'] });\n inject(DestroyRef).onDestroy(() => observer.disconnect());\n }\n\n // Re-emit upstream whenever the parent's status changes due to our writes.\n // De-dupe on the (request, status) pair rather than status alone: a new\n // `src` opens a fresh request token, so an identical status (e.g. a second\n // image that also `loaded`) still re-reports for the new request, while a\n // repeated status within the same request is suppressed.\n let last: ForAvatarStatus | null = null;\n let lastReportedToken = -1;\n const emit = (status: ForAvatarStatus): void => {\n if (destroyed) return;\n if (last === status && lastReportedToken === this.#requestToken) return;\n last = status;\n lastReportedToken = this.#requestToken;\n parent.reportStatus(status);\n this.loadStatusChange.emit(status);\n };\n this.#emit = emit;\n }\n\n protected status(): ForAvatarStatus {\n return this.#parent!.status();\n }\n\n protected onLoad(): void {\n // Ignore a `load` that fires for a request the host has already moved past\n // (the native event can arrive after `src` was reassigned).\n if (!this.#isCurrentRequest()) return;\n if (!this.#host.complete) return;\n this.#emit('loaded');\n }\n\n protected onError(): void {\n if (!this.#isCurrentRequest()) return;\n if (!this.#host.complete) return;\n this.#emit('error');\n }\n\n /**\n * Cheap string-identity fast-path: true when the `src` the host currently\n * carries is the one the latest `#syncFromAttr` observed, so a native\n * load/error event has not drifted away from the in-flight request. This\n * proves src-attribute stability only; request *completeness* (a late event\n * from a superseded resource that happens to share the current src) is\n * verified separately in onLoad/onError via `this.#host.complete`.\n */\n #isCurrentRequest(): boolean {\n return this.#host.getAttribute('src') === this.#requestSrc;\n }\n\n #syncFromAttr(): void {\n const src = this.#host.getAttribute('src');\n this.#requestToken++;\n this.#requestSrc = src;\n if (!src) {\n this.#emit('idle');\n return;\n }\n if (this.#host.complete) {\n // Cached image: load/error events may not fire — derive from naturalWidth.\n if (this.#host.naturalWidth > 0) {\n this.#emit('loaded');\n return;\n }\n // A complete image with zero intrinsic width is ambiguous: it may be a\n // broken image OR a valid SVG with no intrinsic dimensions. Don't\n // pessimistically flag `error` and hide a valid image — verify with\n // `decode()`, which resolves for a decodable (even zero-size) image and\n // rejects for a broken one. Stay `loading` until it settles.\n this.#emit('loading');\n this.#verifyCachedZeroSize(src);\n return;\n }\n this.#emit('loading');\n }\n\n #verifyCachedZeroSize(src: string): void {\n const decode = this.#host.decode?.bind(this.#host);\n if (!decode) {\n // No `decode()` (older engines / jsdom): keep the legacy heuristic.\n this.#emit('error');\n return;\n }\n decode().then(\n () => {\n if (this.#host.getAttribute('src') === src) {\n this.#emit('loaded');\n }\n },\n () => {\n if (this.#host.getAttribute('src') === src) {\n this.#emit('error');\n }\n },\n );\n }\n\n // Initialized in constructor.\n #emit!: (status: ForAvatarStatus) => void;\n\n /** Monotonic id bumped on every `src` change so stale events are ignored. */\n #requestToken = 0;\n /** The `src` the latest `#syncFromAttr` observed, used to match native events. */\n #requestSrc: string | null = null;\n}\n","import { Directive } from '@angular/core';\n\nimport { injectAvatarContext } from './avatar-context';\n\n/**\n * Marker for the avatar's fallback content (initials, generic icon, …).\n * Reflects `data-status` from the parent `[forAvatar]` so the consumer can\n * style for the `error` state separately from `idle` / `loading` if needed.\n *\n * The decision of *whether* to render the fallback lives on the root —\n * drive `@if` with `forAvatar.shouldShowFallback()`.\n */\n@Directive({\n selector: '[forAvatarFallback]',\n exportAs: 'forAvatarFallback',\n host: {\n '[attr.data-status]': 'parent.status()',\n },\n})\nexport class ForAvatarFallback {\n protected readonly parent = injectAvatarContext('ForAvatarFallback');\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"forty-cdk-avatar.mjs","sources":["../../../projects/forty-cdk/avatar/src/avatar-context.ts","../../../projects/forty-cdk/avatar/src/avatar-defaults.ts","../../../projects/forty-cdk/avatar/src/avatar.ts","../../../projects/forty-cdk/avatar/src/avatar-image.ts","../../../projects/forty-cdk/avatar/src/avatar-fallback.ts","../../../projects/forty-cdk/avatar/src/forty-cdk-avatar.ts"],"sourcesContent":["import { inject, InjectionToken, type Signal } from '@angular/core';\n\n/**\n * Lifecycle stages of the avatar's image:\n * - `idle`: no `src` set yet.\n * - `loading`: `src` set, waiting for the network.\n * - `loaded`: image rendered successfully.\n * - `error`: image failed to load.\n */\nexport type ForAvatarStatus = 'idle' | 'loading' | 'loaded' | 'error';\n\nexport interface ForAvatarContext {\n /** Current status of the avatar's image. */\n readonly status: Signal<ForAvatarStatus>;\n /**\n * `true` when the consumer should render the fallback. Drives `@if` in\n * the consumer's template. Honors `fallbackDelayMs` for `idle` / `loading`\n * states; `error` flips it to `true` immediately.\n */\n readonly shouldShowFallback: Signal<boolean>;\n /**\n * Internal — reported by `[forAvatarImage]` so the root can transition\n * its state machine. Consumers should not call this.\n */\n reportStatus(status: ForAvatarStatus): void;\n}\n\nexport const FOR_AVATAR_CONTEXT = new InjectionToken<ForAvatarContext>('FOR_AVATAR_CONTEXT');\n\n/**\n * Injects the nearest {@link ForAvatarContext}, throwing a descriptive error\n * when used outside a `[forAvatar]` element.\n *\n * @param piece Name of the calling directive, used in the error message.\n */\nexport function injectAvatarContext(piece: string): ForAvatarContext {\n const ctx = inject(FOR_AVATAR_CONTEXT, { optional: true });\n if (!ctx) {\n throw new Error(`[forty-cdk/avatar] ${piece} must be used inside a [forAvatar] element.`);\n }\n return ctx;\n}\n","import { type Provider } from '@angular/core';\n\nimport { createDefaults } from 'forty-cdk/core';\n\n/**\n * Defaults inherited by descendant avatars in the surrounding injector\n * scope. Configure with `provideForAvatarDefaults` either at the\n * application root or in any component's `providers` array; partial\n * overrides merge with the parent scope.\n */\nexport interface ForAvatarDefaults {\n /**\n * Milliseconds to defer the fallback for fast loads, avoiding a brief\n * \"initials → image\" flicker. Set to `0` to show immediately during\n * `idle` / `loading`, or to e.g. `500` to skip rendering during quick\n * cached loads. `error` always shows the fallback immediately.\n */\n fallbackDelayMs: number;\n}\n\n/**\n * Library fallback for avatar defaults, read at the root injector when no\n * consumer has called `provideForAvatarDefaults`. Exported for the shared defaults\n * contract spec; not re-exported from the primitive's public entry.\n */\nexport const FOR_AVATAR_FALLBACK_DEFAULTS: ForAvatarDefaults = {\n fallbackDelayMs: 0,\n};\n\nconst { token, provideDefaults } = createDefaults<ForAvatarDefaults>(\n 'FOR_AVATAR_DEFAULTS',\n FOR_AVATAR_FALLBACK_DEFAULTS,\n);\n\n/** Token holding the resolved avatar defaults for the current scope. */\nexport const FOR_AVATAR_DEFAULTS = token;\n\n/**\n * Configures forty-cdk avatar defaults for this injector scope. Partial\n * overrides inherit unspecified keys from the parent scope (or library\n * defaults at the root).\n */\nexport function provideForAvatarDefaults(defaults: Partial<ForAvatarDefaults> = {}): Provider[] {\n return provideDefaults(defaults);\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n computed,\n DestroyRef,\n Directive,\n effect,\n inject,\n input,\n linkedSignal,\n numberAttribute,\n PLATFORM_ID,\n signal,\n} from '@angular/core';\n\nimport { FOR_AVATAR_CONTEXT, type ForAvatarContext, type ForAvatarStatus } from './avatar-context';\nimport { FOR_AVATAR_DEFAULTS } from './avatar-defaults';\n\n/**\n * Headless avatar root. Tracks the load lifecycle of an inner\n * `[forAvatarImage]` and exposes `status` + `shouldShowFallback` so the\n * consumer can swap the image for fallback content (initials, generic icon)\n * via `@if`.\n *\n * No ARIA role is imposed — avatars are typically presentational. If the\n * surrounding component conveys identity (a person's name in the header,\n * a chat bubble), the consumer should provide an `aria-label` on the\n * relevant ancestor or pair the avatar with visible text.\n *\n * @example\n * ```html\n * <span forAvatar #a=\"forAvatar\" fallbackDelayMs=\"500\">\n * <img forAvatarImage [src]=\"user.avatarUrl\" [alt]=\"user.name\" />\n * @if (a.shouldShowFallback()) {\n * <span forAvatarFallback>{{ user.initials }}</span>\n * }\n * </span>\n * ```\n */\n@Directive({\n selector: '[forAvatar]',\n exportAs: 'forAvatar',\n host: {\n '[attr.data-status]': 'status()',\n },\n providers: [{ provide: FOR_AVATAR_CONTEXT, useExisting: ForAvatar }],\n})\nexport class ForAvatar implements ForAvatarContext {\n readonly #defaults = inject(FOR_AVATAR_DEFAULTS);\n\n /**\n * Milliseconds to defer the fallback for fast loads, avoiding a brief\n * \"initials → image\" flicker. Set to `0` (default) to show immediately\n * during `idle` / `loading`, or to e.g. `500` to skip rendering during\n * quick cached loads. `error` always shows the fallback immediately.\n * The default is read from `provideForAvatarDefaults` for the\n * surrounding scope.\n */\n readonly fallbackDelayMs = input(this.#defaults.fallbackDelayMs, {\n transform: (value: unknown) => {\n const parsed = numberAttribute(value);\n return Number.isFinite(parsed) ? parsed : this.#defaults.fallbackDelayMs;\n },\n });\n\n readonly #status = signal<ForAvatarStatus>('idle');\n readonly status = this.#status.asReadonly();\n\n // Whether the deferred-fallback timer has fired for the current status/delay.\n // `linkedSignal` resets it to `false` whenever the source changes (the\n // canonical replacement for resetting state inside an effect); the timer\n // callback in the constructor flips it to `true`.\n readonly #timerElapsed = linkedSignal<{ status: ForAvatarStatus; delay: number }, boolean>({\n source: () => ({ status: this.#status(), delay: this.fallbackDelayMs() }),\n computation: () => false,\n });\n #timer: ReturnType<typeof setTimeout> | null = null;\n readonly #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n\n readonly shouldShowFallback = computed(() => {\n const status = this.#status();\n if (status === 'loaded') return false;\n if (status === 'error') return true;\n // idle | loading: an immediate (delay <= 0) gate shows the fallback right\n // away; a deferred gate waits for the timer armed in the effect below.\n if (this.fallbackDelayMs() <= 0) return true;\n return this.#timerElapsed();\n });\n\n /**\n * Reported by `[forAvatarImage]` so the root can transition its state\n * machine. Consumers should not call this.\n */\n reportStatus(status: ForAvatarStatus): void {\n this.#status.set(status);\n }\n\n constructor() {\n // Imperative timer integration: the effect's only job is to (re)arm a\n // setTimeout for the deferred-fallback case (idle | loading with a positive\n // delay). The synchronous branches (loaded / error / delay <= 0) are pure\n // derivations and live in `shouldShowFallback`. The signal write happens\n // inside the timer callback, which runs outside the effect's reactive\n // scope, so this does NOT create a self-cycle in the reactive graph.\n effect(() => {\n const status = this.#status();\n const delay = this.fallbackDelayMs();\n this.#cancelTimer();\n if (this.#isBrowser && (status === 'idle' || status === 'loading') && delay > 0) {\n this.#timer = setTimeout(() => {\n this.#timer = null;\n this.#timerElapsed.set(true);\n }, delay);\n }\n });\n\n inject(DestroyRef).onDestroy(() => this.#cancelTimer());\n }\n\n #cancelTimer(): void {\n if (this.#timer !== null) {\n clearTimeout(this.#timer);\n this.#timer = null;\n }\n }\n}\n","import { isPlatformBrowser } from '@angular/common';\nimport {\n afterNextRender,\n DestroyRef,\n Directive,\n ElementRef,\n inject,\n output,\n PLATFORM_ID,\n} from '@angular/core';\n\nimport { FOR_AVATAR_CONTEXT, type ForAvatarStatus } from './avatar-context';\n\n/**\n * Wraps an `<img>` and reports its load lifecycle to the surrounding\n * `[forAvatar]`. The native `src` / `alt` bindings are owned by the\n * consumer — this directive only observes.\n *\n * Reflects `data-status` on the image element so the consumer can hide it\n * via CSS while loading or after an error (e.g. `img[data-status=\"error\"]\n * { display: none }`).\n */\n@Directive({\n selector: 'img[forAvatarImage]',\n exportAs: 'forAvatarImage',\n host: {\n '[attr.data-status]': 'status()',\n '(load)': 'onLoad()',\n '(error)': 'onError()',\n },\n})\nexport class ForAvatarImage {\n readonly #host = inject<ElementRef<HTMLImageElement>>(ElementRef).nativeElement;\n readonly #parent = inject(FOR_AVATAR_CONTEXT, { optional: true });\n\n /**\n * Emits whenever the image lifecycle transitions to a new state. Useful\n * if the consumer wants to log analytics, swap to a different src on\n * error, etc.\n */\n readonly loadStatusChange = output<ForAvatarStatus>();\n\n constructor() {\n if (!this.#parent) {\n throw new Error(\n '[forty-cdk/avatar] ForAvatarImage must be used inside a [forAvatar] element.',\n );\n }\n const parent = this.#parent;\n const host = this.#host;\n\n let destroyed = false;\n inject(DestroyRef).onDestroy(() => (destroyed = true));\n\n afterNextRender(() => {\n this.#syncFromAttr();\n });\n\n if (isPlatformBrowser(inject(PLATFORM_ID)) && typeof MutationObserver !== 'undefined') {\n const observer = new MutationObserver(() => {\n this.#syncFromAttr();\n });\n observer.observe(host, { attributes: true, attributeFilter: ['src'] });\n inject(DestroyRef).onDestroy(() => observer.disconnect());\n }\n\n // Re-emit upstream whenever the parent's status changes due to our writes.\n // De-dupe on the (request, status) pair rather than status alone: a new\n // `src` opens a fresh request token, so an identical status (e.g. a second\n // image that also `loaded`) still re-reports for the new request, while a\n // repeated status within the same request is suppressed.\n let last: ForAvatarStatus | null = null;\n let lastReportedToken = -1;\n const emit = (status: ForAvatarStatus): void => {\n if (destroyed) return;\n if (last === status && lastReportedToken === this.#requestToken) return;\n last = status;\n lastReportedToken = this.#requestToken;\n parent.reportStatus(status);\n this.loadStatusChange.emit(status);\n };\n this.#emit = emit;\n }\n\n protected status(): ForAvatarStatus {\n return this.#parent!.status();\n }\n\n protected onLoad(): void {\n // Ignore a `load` that fires for a request the host has already moved past\n // (the native event can arrive after `src` was reassigned).\n if (!this.#isCurrentRequest()) return;\n if (!this.#host.complete) return;\n this.#emit('loaded');\n }\n\n protected onError(): void {\n if (!this.#isCurrentRequest()) return;\n if (!this.#host.complete) return;\n this.#emit('error');\n }\n\n /**\n * Cheap string-identity fast-path: true when the `src` the host currently\n * carries is the one the latest `#syncFromAttr` observed, so a native\n * load/error event has not drifted away from the in-flight request. This\n * proves src-attribute stability only; request *completeness* (a late event\n * from a superseded resource that happens to share the current src) is\n * verified separately in onLoad/onError via `this.#host.complete`.\n */\n #isCurrentRequest(): boolean {\n return this.#host.getAttribute('src') === this.#requestSrc;\n }\n\n #syncFromAttr(): void {\n const src = this.#host.getAttribute('src');\n this.#requestToken++;\n this.#requestSrc = src;\n if (!src) {\n this.#emit('idle');\n return;\n }\n if (this.#host.complete) {\n // Cached image: load/error events may not fire — derive from naturalWidth.\n if (this.#host.naturalWidth > 0) {\n this.#emit('loaded');\n return;\n }\n // A complete image with zero intrinsic width is ambiguous: it may be a\n // broken image OR a valid SVG with no intrinsic dimensions. Don't\n // pessimistically flag `error` and hide a valid image — verify with\n // `decode()`, which resolves for a decodable (even zero-size) image and\n // rejects for a broken one. Stay `loading` until it settles.\n this.#emit('loading');\n this.#verifyCachedZeroSize(src);\n return;\n }\n this.#emit('loading');\n }\n\n #verifyCachedZeroSize(src: string): void {\n const decode = this.#host.decode?.bind(this.#host);\n if (!decode) {\n // No `decode()` (older engines / jsdom): keep the legacy heuristic.\n this.#emit('error');\n return;\n }\n decode().then(\n () => {\n if (this.#host.getAttribute('src') === src) {\n this.#emit('loaded');\n }\n },\n () => {\n if (this.#host.getAttribute('src') === src) {\n this.#emit('error');\n }\n },\n );\n }\n\n // Initialized in constructor.\n #emit!: (status: ForAvatarStatus) => void;\n\n /** Monotonic id bumped on every `src` change so stale events are ignored. */\n #requestToken = 0;\n /** The `src` the latest `#syncFromAttr` observed, used to match native events. */\n #requestSrc: string | null = null;\n}\n","import { Directive } from '@angular/core';\n\nimport { injectAvatarContext } from './avatar-context';\n\n/**\n * Marker for the avatar's fallback content (initials, generic icon, …).\n * Reflects `data-status` from the parent `[forAvatar]` so the consumer can\n * style for the `error` state separately from `idle` / `loading` if needed.\n *\n * The decision of *whether* to render the fallback lives on the root —\n * drive `@if` with `forAvatar.shouldShowFallback()`.\n */\n@Directive({\n selector: '[forAvatarFallback]',\n exportAs: 'forAvatarFallback',\n host: {\n '[attr.data-status]': 'parent.status()',\n },\n})\nexport class ForAvatarFallback {\n protected readonly parent = injectAvatarContext('ForAvatarFallback');\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;;;MA2Ba,kBAAkB,GAAG,IAAI,cAAc,CAAmB,oBAAoB;AAE3F;;;;;AAKG;AACG,SAAU,mBAAmB,CAAC,KAAa,EAAA;AAC/C,IAAA,MAAM,GAAG,GAAG,MAAM,CAAC,kBAAkB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1D,IAAI,CAAC,GAAG,EAAE;AACR,QAAA,MAAM,IAAI,KAAK,CAAC,sBAAsB,KAAK,CAAA,2CAAA,CAA6C,CAAC;IAC3F;AACA,IAAA,OAAO,GAAG;AACZ;;ACrBA;;;;AAIG;AACI,MAAM,4BAA4B,GAAsB;AAC7D,IAAA,eAAe,EAAE,CAAC;CACnB;AAED,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,GAAG,cAAc,CAC/C,qBAAqB,EACrB,4BAA4B,CAC7B;AAED;AACO,MAAM,mBAAmB,GAAG;AAEnC;;;;AAIG;AACG,SAAU,wBAAwB,CAAC,QAAA,GAAuC,EAAE,EAAA;AAChF,IAAA,OAAO,eAAe,CAAC,QAAQ,CAAC;AAClC;;AC3BA;;;;;;;;;;;;;;;;;;;;AAoBG;MASU,SAAS,CAAA;AACX,IAAA,SAAS,GAAG,MAAM,CAAC,mBAAmB,CAAC;AAEhD;;;;;;;AAOG;AACM,IAAA,eAAe,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,eAAe,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,iBAAA,EAAA,8BAAA,EAAA,CAAA,EAC7D,SAAS,EAAE,CAAC,KAAc,KAAI;AAC5B,YAAA,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC;AACrC,YAAA,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,eAAe;AAC1E,QAAA,CAAC,GACD;IAEO,OAAO,GAAG,MAAM,CAAkB,MAAM;gFAAC;AACzC,IAAA,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE;;;;;IAMlC,aAAa,GAAG,YAAY,CAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,eAAA,EAAA,8BAAA,EAAA,CAAA,EACnC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;AACzE,QAAA,WAAW,EAAE,MAAM,KAAK,GACxB;IACF,MAAM,GAAyC,IAAI;IAC1C,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AAEnD,IAAA,kBAAkB,GAAG,QAAQ,CAAC,MAAK;AAC1C,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE;QAC7B,IAAI,MAAM,KAAK,QAAQ;AAAE,YAAA,OAAO,KAAK;QACrC,IAAI,MAAM,KAAK,OAAO;AAAE,YAAA,OAAO,IAAI;;;AAGnC,QAAA,IAAI,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC;AAAE,YAAA,OAAO,IAAI;AAC5C,QAAA,OAAO,IAAI,CAAC,aAAa,EAAE;IAC7B,CAAC;2FAAC;AAEF;;;AAGG;AACH,IAAA,YAAY,CAAC,MAAuB,EAAA;AAClC,QAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;IAC1B;AAEA,IAAA,WAAA,GAAA;;;;;;;QAOE,MAAM,CAAC,MAAK;AACV,YAAA,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE;AAC7B,YAAA,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,EAAE;YACpC,IAAI,CAAC,YAAY,EAAE;AACnB,YAAA,IAAI,IAAI,CAAC,UAAU,KAAK,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,SAAS,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE;AAC/E,gBAAA,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,MAAK;AAC5B,oBAAA,IAAI,CAAC,MAAM,GAAG,IAAI;AAClB,oBAAA,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC;gBAC9B,CAAC,EAAE,KAAK,CAAC;YACX;AACF,QAAA,CAAC,CAAC;AAEF,QAAA,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;IACzD;IAEA,YAAY,GAAA;AACV,QAAA,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,EAAE;AACxB,YAAA,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC;AACzB,YAAA,IAAI,CAAC,MAAM,GAAG,IAAI;QACpB;IACF;uGA7EW,SAAS,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;2FAAT,SAAS,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,aAAA,EAAA,MAAA,EAAA,EAAA,eAAA,EAAA,EAAA,iBAAA,EAAA,iBAAA,EAAA,UAAA,EAAA,iBAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,IAAA,EAAA,EAAA,UAAA,EAAA,EAAA,kBAAA,EAAA,UAAA,EAAA,EAAA,EAAA,SAAA,EAFT,CAAC,EAAE,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC,EAAA,QAAA,EAAA,CAAA,WAAA,CAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA;;2FAEzD,SAAS,EAAA,UAAA,EAAA,CAAA;kBARrB,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,aAAa;AACvB,oBAAA,QAAQ,EAAE,WAAW;AACrB,oBAAA,IAAI,EAAE;AACJ,wBAAA,oBAAoB,EAAE,UAAU;AACjC,qBAAA;oBACD,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAA,SAAW,EAAE,CAAC;AACrE,iBAAA;;;AChCD;;;;;;;;AAQG;MAUU,cAAc,CAAA;AAChB,IAAA,KAAK,GAAG,MAAM,CAA+B,UAAU,CAAC,CAAC,aAAa;IACtE,OAAO,GAAG,MAAM,CAAC,kBAAkB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAEjE;;;;AAIG;IACM,gBAAgB,GAAG,MAAM,EAAmB;AAErD,IAAA,WAAA,GAAA;AACE,QAAA,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;AACjB,YAAA,MAAM,IAAI,KAAK,CACb,8EAA8E,CAC/E;QACH;AACA,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO;AAC3B,QAAA,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK;QAEvB,IAAI,SAAS,GAAG,KAAK;AACrB,QAAA,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,OAAO,SAAS,GAAG,IAAI,CAAC,CAAC;QAEtD,eAAe,CAAC,MAAK;YACnB,IAAI,CAAC,aAAa,EAAE;AACtB,QAAA,CAAC,CAAC;AAEF,QAAA,IAAI,iBAAiB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,OAAO,gBAAgB,KAAK,WAAW,EAAE;AACrF,YAAA,MAAM,QAAQ,GAAG,IAAI,gBAAgB,CAAC,MAAK;gBACzC,IAAI,CAAC,aAAa,EAAE;AACtB,YAAA,CAAC,CAAC;AACF,YAAA,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;AACtE,YAAA,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,MAAM,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC3D;;;;;;QAOA,IAAI,IAAI,GAA2B,IAAI;AACvC,QAAA,IAAI,iBAAiB,GAAG,CAAC,CAAC;AAC1B,QAAA,MAAM,IAAI,GAAG,CAAC,MAAuB,KAAU;AAC7C,YAAA,IAAI,SAAS;gBAAE;YACf,IAAI,IAAI,KAAK,MAAM,IAAI,iBAAiB,KAAK,IAAI,CAAC,aAAa;gBAAE;YACjE,IAAI,GAAG,MAAM;AACb,YAAA,iBAAiB,GAAG,IAAI,CAAC,aAAa;AACtC,YAAA,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC;AAC3B,YAAA,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC;AACpC,QAAA,CAAC;AACD,QAAA,IAAI,CAAC,KAAK,GAAG,IAAI;IACnB;IAEU,MAAM,GAAA;AACd,QAAA,OAAO,IAAI,CAAC,OAAQ,CAAC,MAAM,EAAE;IAC/B;IAEU,MAAM,GAAA;;;AAGd,QAAA,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE;YAAE;AAC/B,QAAA,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ;YAAE;AAC1B,QAAA,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;IACtB;IAEU,OAAO,GAAA;AACf,QAAA,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE;YAAE;AAC/B,QAAA,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ;YAAE;AAC1B,QAAA,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;IACrB;AAEA;;;;;;;AAOG;IACH,iBAAiB,GAAA;AACf,QAAA,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,WAAW;IAC5D;IAEA,aAAa,GAAA;QACX,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC;QAC1C,IAAI,CAAC,aAAa,EAAE;AACpB,QAAA,IAAI,CAAC,WAAW,GAAG,GAAG;QACtB,IAAI,CAAC,GAAG,EAAE;AACR,YAAA,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;YAClB;QACF;AACA,QAAA,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE;;YAEvB,IAAI,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,CAAC,EAAE;AAC/B,gBAAA,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;gBACpB;YACF;;;;;;AAMA,YAAA,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC;AACrB,YAAA,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC;YAC/B;QACF;AACA,QAAA,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC;IACvB;AAEA,IAAA,qBAAqB,CAAC,GAAW,EAAA;AAC/B,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAClD,IAAI,CAAC,MAAM,EAAE;;AAEX,YAAA,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;YACnB;QACF;AACA,QAAA,MAAM,EAAE,CAAC,IAAI,CACX,MAAK;YACH,IAAI,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE;AAC1C,gBAAA,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;YACtB;QACF,CAAC,EACD,MAAK;YACH,IAAI,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE;AAC1C,gBAAA,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;YACrB;AACF,QAAA,CAAC,CACF;IACH;;AAGA,IAAA,KAAK;;IAGL,aAAa,GAAG,CAAC;;IAEjB,WAAW,GAAkB,IAAI;uGAxItB,cAAc,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;2FAAd,cAAc,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,qBAAA,EAAA,OAAA,EAAA,EAAA,gBAAA,EAAA,kBAAA,EAAA,EAAA,IAAA,EAAA,EAAA,SAAA,EAAA,EAAA,MAAA,EAAA,UAAA,EAAA,OAAA,EAAA,WAAA,EAAA,EAAA,UAAA,EAAA,EAAA,kBAAA,EAAA,UAAA,EAAA,EAAA,EAAA,QAAA,EAAA,CAAA,gBAAA,CAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA;;2FAAd,cAAc,EAAA,UAAA,EAAA,CAAA;kBAT1B,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,qBAAqB;AAC/B,oBAAA,QAAQ,EAAE,gBAAgB;AAC1B,oBAAA,IAAI,EAAE;AACJ,wBAAA,oBAAoB,EAAE,UAAU;AAChC,wBAAA,QAAQ,EAAE,UAAU;AACpB,wBAAA,SAAS,EAAE,WAAW;AACvB,qBAAA;AACF,iBAAA;;;AC1BD;;;;;;;AAOG;MAQU,iBAAiB,CAAA;AACT,IAAA,MAAM,GAAG,mBAAmB,CAAC,mBAAmB,CAAC;uGADzD,iBAAiB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;2FAAjB,iBAAiB,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,qBAAA,EAAA,IAAA,EAAA,EAAA,UAAA,EAAA,EAAA,kBAAA,EAAA,iBAAA,EAAA,EAAA,EAAA,QAAA,EAAA,CAAA,mBAAA,CAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA;;2FAAjB,iBAAiB,EAAA,UAAA,EAAA,CAAA;kBAP7B,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,qBAAqB;AAC/B,oBAAA,QAAQ,EAAE,mBAAmB;AAC7B,oBAAA,IAAI,EAAE;AACJ,wBAAA,oBAAoB,EAAE,iBAAiB;AACxC,qBAAA;AACF,iBAAA;;;AClBD;;AAEG;;;;"}
|
|
@@ -14,10 +14,15 @@ const forBreakpointsTailwind = {
|
|
|
14
14
|
xl: 1280,
|
|
15
15
|
'2xl': 1536,
|
|
16
16
|
};
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Library fallback for breakpoints defaults, read at the root injector when no
|
|
19
|
+
* consumer has called `provideForBreakpointsDefaults`. Exported for the shared
|
|
20
|
+
* defaults contract spec; not re-exported from the primitive's public entry.
|
|
21
|
+
*/
|
|
22
|
+
const FOR_BREAKPOINTS_FALLBACK_DEFAULTS = {
|
|
18
23
|
breakpoints: forBreakpointsTailwind,
|
|
19
24
|
};
|
|
20
|
-
const { token, provideDefaults } = createDefaults('FOR_BREAKPOINTS_DEFAULTS',
|
|
25
|
+
const { token, provideDefaults } = createDefaults('FOR_BREAKPOINTS_DEFAULTS', FOR_BREAKPOINTS_FALLBACK_DEFAULTS);
|
|
21
26
|
/**
|
|
22
27
|
* Token holding the resolved breakpoint map for the current injector scope.
|
|
23
28
|
* The library always provides a fully-populated value (the Tailwind fallback
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forty-cdk-breakpoints.mjs","sources":["../../../projects/forty-cdk/breakpoints/src/breakpoints-defaults.ts","../../../projects/forty-cdk/breakpoints/src/breakpoints.ts","../../../projects/forty-cdk/breakpoints/src/forty-cdk-breakpoints.ts"],"sourcesContent":["import { type Provider } from '@angular/core';\n\nimport { createDefaults } from 'forty-cdk/core';\n\n/**\n * A breakpoint map: each named breakpoint paired with its `min-width`\n * threshold in CSS pixels. Names are arbitrary — `injectBreakpoints` derives\n * its query methods (`up`, `down`, `between`, `only`) from whatever keys this\n * map declares, sorted ascending by threshold.\n */\nexport type BreakpointMap = Record<string, number>;\n\n/**\n * The default breakpoint map, mirroring Tailwind CSS's `sm`–`2xl` scale. Used\n * as the library fallback when no `provideForBreakpointsDefaults` is in scope,\n * and the source of the typed breakpoint names when the consumer has not\n * augmented {@link BreakpointRegistry}.\n */\nexport const forBreakpointsTailwind = {\n sm: 640,\n md: 768,\n lg: 1024,\n xl: 1280,\n '2xl': 1536,\n} as const satisfies BreakpointMap;\n\n/** The breakpoint names from the default Tailwind scale. */\nexport type TailwindBreakpointName = keyof typeof forBreakpointsTailwind;\n\n/** Resolved shape stored against {@link FOR_BREAKPOINTS_DEFAULTS}. */\nexport interface ForBreakpointsDefaults {\n /** The active breakpoint map for the current injector scope. */\n breakpoints: BreakpointMap;\n}\n\nconst FALLBACK: ForBreakpointsDefaults = {\n breakpoints: forBreakpointsTailwind,\n};\n\nconst { token, provideDefaults } = createDefaults<ForBreakpointsDefaults>(\n 'FOR_BREAKPOINTS_DEFAULTS',\n FALLBACK,\n);\n\n/**\n * Token holding the resolved breakpoint map for the current injector scope.\n * The library always provides a fully-populated value (the Tailwind fallback\n * at the root, or the map from the nearest `provideForBreakpointsDefaults`).\n */\nexport const FOR_BREAKPOINTS_DEFAULTS = token;\n\n/**\n * Configures the breakpoint map for this injector scope so `injectBreakpoints`\n * can be called with no arguments. Provide it once in your application config:\n *\n * ```ts\n * providers: [\n * provideForBreakpointsDefaults({ mobile: 0, tablet: 640, laptop: 1024, desktop: 1280 }),\n * ]\n * ```\n *\n * Providing it again on a component injector replaces the map for that subtree\n * only (the nearest scope wins; the map is replaced wholesale, never merged\n * key-by-key). Omit it entirely to use {@link forBreakpointsTailwind}.\n */\nexport function provideForBreakpointsDefaults(breakpoints: BreakpointMap): Provider[] {\n return provideDefaults({ breakpoints });\n}\n","import { Injector, computed, inject, runInInjectionContext, type Signal } from '@angular/core';\n\nimport { injectMediaQuery } from 'forty-cdk/core';\nimport { FOR_BREAKPOINTS_DEFAULTS, type TailwindBreakpointName } from './breakpoints-defaults';\n\n/**\n * Extension point for typing custom breakpoint names. Augment it via module\n * declaration merging so `injectBreakpoints` autocompletes your own names\n * instead of the default Tailwind scale:\n *\n * ```ts\n * import type { appBreakpoints } from './breakpoints';\n *\n * declare module 'forty-cdk' {\n * interface BreakpointRegistry extends Record<keyof typeof appBreakpoints, true> {}\n * }\n * ```\n *\n * When left empty (no augmentation), {@link BreakpointName} falls back to the\n * keys of {@link forBreakpointsTailwind}.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BreakpointRegistry {}\n\n/**\n * The set of valid breakpoint names. Resolves to the augmented\n * {@link BreakpointRegistry} keys when present, otherwise the Tailwind scale\n * (`'sm' | 'md' | 'lg' | 'xl' | '2xl'`).\n */\nexport type BreakpointName = [keyof BreakpointRegistry] extends [never]\n ? TailwindBreakpointName\n : keyof BreakpointRegistry & string;\n\n/** Reactive handle returned by {@link injectBreakpoints}. */\nexport interface ForBreakpoints<K extends string = BreakpointName> {\n /**\n * Matches the named breakpoint and wider — `(min-width: <threshold>px)`.\n * @param name A breakpoint declared in the active map.\n */\n up(name: K): Signal<boolean>;\n /**\n * Matches narrower than the named breakpoint —\n * `(max-width: <threshold - 0.02>px)`. The 0.02px back-off keeps `up(name)`\n * and `down(name)` from both matching at the exact threshold.\n * @param name A breakpoint declared in the active map.\n */\n down(name: K): Signal<boolean>;\n /**\n * Matches from `min` (inclusive) up to but not including `max`.\n * @param min Lower breakpoint, inclusive.\n * @param max Upper breakpoint, exclusive.\n */\n between(min: K, max: K): Signal<boolean>;\n /**\n * Matches only the named breakpoint's own band — from its threshold up to\n * but not including the next-larger breakpoint (open-ended for the largest).\n * @param name A breakpoint declared in the active map.\n */\n only(name: K): Signal<boolean>;\n /**\n * The largest breakpoint whose `min-width` currently matches, or `null` when\n * the viewport is narrower than the smallest breakpoint (and on the server).\n */\n readonly active: Signal<K | null>;\n /**\n * Escape hatch for an arbitrary media query string (orientation, pointer,\n * `prefers-color-scheme`, …) that the named helpers don't cover.\n * @param query Any valid media query.\n */\n matches(query: string): Signal<boolean>;\n}\n\n/**\n * A signal-first, zoneless, SSR-safe viewport breakpoint observer. Reads the\n * breakpoint map from the ambient {@link FOR_BREAKPOINTS_DEFAULTS} token\n * (configured with `provideForBreakpointsDefaults`, or the Tailwind fallback),\n * so call sites never repeat the breakpoint set:\n *\n * ```ts\n * private bp = injectBreakpoints();\n * protected isDesktop = this.bp.up('desktop');\n * protected active = this.bp.active;\n * ```\n *\n * Each query method returns a `Signal<boolean>` backed by a cached\n * `MediaQueryList`. The returned handle captures the calling injection\n * context, so its methods can be invoked lazily from `computed()` or a\n * template — not only during construction. Listeners are torn down with the\n * injector. `active`'s per-breakpoint queries are materialized eagerly at call\n * time, so reading `active` never attaches a listener from inside a reactive\n * computation or after teardown. On the server (or where `matchMedia` is\n * unavailable) every signal reads `false` and `active` reads `null`.\n *\n * Must be called from an injection context.\n *\n * @returns A {@link ForBreakpoints} handle of reactive query methods.\n */\nexport function injectBreakpoints(): ForBreakpoints {\n const injector = inject(Injector);\n const map = inject(FOR_BREAKPOINTS_DEFAULTS).breakpoints;\n const names = Object.keys(map).sort((a, b) => map[a]! - map[b]!);\n const cache = new Map<string, Signal<boolean>>();\n\n const observe = (query: string): Signal<boolean> => {\n let result = cache.get(query);\n if (result === undefined) {\n result = runInInjectionContext(injector, () => injectMediaQuery(query));\n cache.set(query, result);\n }\n return result;\n };\n\n const thresholdOf = (name: string): number => {\n const value = map[name];\n if (value === undefined) {\n throw new Error(\n `[forty-cdk/breakpoints] Unknown breakpoint \"${name}\". Defined breakpoints: ${\n names.join(', ') || '(none)'\n }.`,\n );\n }\n return value;\n };\n\n const up = (name: string): Signal<boolean> => observe(`(min-width: ${thresholdOf(name)}px)`);\n\n const down = (name: string): Signal<boolean> =>\n observe(`(max-width: ${thresholdOf(name) - 0.02}px)`);\n\n const between = (min: string, max: string): Signal<boolean> =>\n observe(`(min-width: ${thresholdOf(min)}px) and (max-width: ${thresholdOf(max) - 0.02}px)`);\n\n const only = (name: string): Signal<boolean> => {\n const lower = thresholdOf(name);\n const index = names.indexOf(name);\n const next = index < names.length - 1 ? thresholdOf(names[index + 1]!) : null;\n return next === null\n ? observe(`(min-width: ${lower}px)`)\n : observe(`(min-width: ${lower}px) and (max-width: ${next - 0.02}px)`);\n };\n\n const activeCandidates = names.map((name) => ({\n name,\n matches: observe(`(min-width: ${map[name]}px)`),\n }));\n\n const active = computed<BreakpointName | null>(() => {\n let current: string | null = null;\n for (const candidate of activeCandidates) {\n if (candidate.matches()) {\n current = candidate.name;\n }\n }\n return current as BreakpointName | null;\n });\n\n return { up, down, between, only, active, matches: (query) => observe(query) };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"forty-cdk-breakpoints.mjs","sources":["../../../projects/forty-cdk/breakpoints/src/breakpoints-defaults.ts","../../../projects/forty-cdk/breakpoints/src/breakpoints.ts","../../../projects/forty-cdk/breakpoints/src/forty-cdk-breakpoints.ts"],"sourcesContent":["import { type Provider } from '@angular/core';\n\nimport { createDefaults } from 'forty-cdk/core';\n\n/**\n * A breakpoint map: each named breakpoint paired with its `min-width`\n * threshold in CSS pixels. Names are arbitrary — `injectBreakpoints` derives\n * its query methods (`up`, `down`, `between`, `only`) from whatever keys this\n * map declares, sorted ascending by threshold.\n */\nexport type BreakpointMap = Record<string, number>;\n\n/**\n * The default breakpoint map, mirroring Tailwind CSS's `sm`–`2xl` scale. Used\n * as the library fallback when no `provideForBreakpointsDefaults` is in scope,\n * and the source of the typed breakpoint names when the consumer has not\n * augmented {@link BreakpointRegistry}.\n */\nexport const forBreakpointsTailwind = {\n sm: 640,\n md: 768,\n lg: 1024,\n xl: 1280,\n '2xl': 1536,\n} as const satisfies BreakpointMap;\n\n/** The breakpoint names from the default Tailwind scale. */\nexport type TailwindBreakpointName = keyof typeof forBreakpointsTailwind;\n\n/** Resolved shape stored against {@link FOR_BREAKPOINTS_DEFAULTS}. */\nexport interface ForBreakpointsDefaults {\n /** The active breakpoint map for the current injector scope. */\n breakpoints: BreakpointMap;\n}\n\n/**\n * Library fallback for breakpoints defaults, read at the root injector when no\n * consumer has called `provideForBreakpointsDefaults`. Exported for the shared\n * defaults contract spec; not re-exported from the primitive's public entry.\n */\nexport const FOR_BREAKPOINTS_FALLBACK_DEFAULTS: ForBreakpointsDefaults = {\n breakpoints: forBreakpointsTailwind,\n};\n\nconst { token, provideDefaults } = createDefaults<ForBreakpointsDefaults>(\n 'FOR_BREAKPOINTS_DEFAULTS',\n FOR_BREAKPOINTS_FALLBACK_DEFAULTS,\n);\n\n/**\n * Token holding the resolved breakpoint map for the current injector scope.\n * The library always provides a fully-populated value (the Tailwind fallback\n * at the root, or the map from the nearest `provideForBreakpointsDefaults`).\n */\nexport const FOR_BREAKPOINTS_DEFAULTS = token;\n\n/**\n * Configures the breakpoint map for this injector scope so `injectBreakpoints`\n * can be called with no arguments. Provide it once in your application config:\n *\n * ```ts\n * providers: [\n * provideForBreakpointsDefaults({ mobile: 0, tablet: 640, laptop: 1024, desktop: 1280 }),\n * ]\n * ```\n *\n * Providing it again on a component injector replaces the map for that subtree\n * only (the nearest scope wins; the map is replaced wholesale, never merged\n * key-by-key). Omit it entirely to use {@link forBreakpointsTailwind}.\n */\nexport function provideForBreakpointsDefaults(breakpoints: BreakpointMap): Provider[] {\n return provideDefaults({ breakpoints });\n}\n","import { Injector, computed, inject, runInInjectionContext, type Signal } from '@angular/core';\n\nimport { injectMediaQuery } from 'forty-cdk/core';\nimport { FOR_BREAKPOINTS_DEFAULTS, type TailwindBreakpointName } from './breakpoints-defaults';\n\n/**\n * Extension point for typing custom breakpoint names. Augment it via module\n * declaration merging so `injectBreakpoints` autocompletes your own names\n * instead of the default Tailwind scale:\n *\n * ```ts\n * import type { appBreakpoints } from './breakpoints';\n *\n * declare module 'forty-cdk' {\n * interface BreakpointRegistry extends Record<keyof typeof appBreakpoints, true> {}\n * }\n * ```\n *\n * When left empty (no augmentation), {@link BreakpointName} falls back to the\n * keys of {@link forBreakpointsTailwind}.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface BreakpointRegistry {}\n\n/**\n * The set of valid breakpoint names. Resolves to the augmented\n * {@link BreakpointRegistry} keys when present, otherwise the Tailwind scale\n * (`'sm' | 'md' | 'lg' | 'xl' | '2xl'`).\n */\nexport type BreakpointName = [keyof BreakpointRegistry] extends [never]\n ? TailwindBreakpointName\n : keyof BreakpointRegistry & string;\n\n/** Reactive handle returned by {@link injectBreakpoints}. */\nexport interface ForBreakpoints<K extends string = BreakpointName> {\n /**\n * Matches the named breakpoint and wider — `(min-width: <threshold>px)`.\n * @param name A breakpoint declared in the active map.\n */\n up(name: K): Signal<boolean>;\n /**\n * Matches narrower than the named breakpoint —\n * `(max-width: <threshold - 0.02>px)`. The 0.02px back-off keeps `up(name)`\n * and `down(name)` from both matching at the exact threshold.\n * @param name A breakpoint declared in the active map.\n */\n down(name: K): Signal<boolean>;\n /**\n * Matches from `min` (inclusive) up to but not including `max`.\n * @param min Lower breakpoint, inclusive.\n * @param max Upper breakpoint, exclusive.\n */\n between(min: K, max: K): Signal<boolean>;\n /**\n * Matches only the named breakpoint's own band — from its threshold up to\n * but not including the next-larger breakpoint (open-ended for the largest).\n * @param name A breakpoint declared in the active map.\n */\n only(name: K): Signal<boolean>;\n /**\n * The largest breakpoint whose `min-width` currently matches, or `null` when\n * the viewport is narrower than the smallest breakpoint (and on the server).\n */\n readonly active: Signal<K | null>;\n /**\n * Escape hatch for an arbitrary media query string (orientation, pointer,\n * `prefers-color-scheme`, …) that the named helpers don't cover.\n * @param query Any valid media query.\n */\n matches(query: string): Signal<boolean>;\n}\n\n/**\n * A signal-first, zoneless, SSR-safe viewport breakpoint observer. Reads the\n * breakpoint map from the ambient {@link FOR_BREAKPOINTS_DEFAULTS} token\n * (configured with `provideForBreakpointsDefaults`, or the Tailwind fallback),\n * so call sites never repeat the breakpoint set:\n *\n * ```ts\n * private bp = injectBreakpoints();\n * protected isDesktop = this.bp.up('desktop');\n * protected active = this.bp.active;\n * ```\n *\n * Each query method returns a `Signal<boolean>` backed by a cached\n * `MediaQueryList`. The returned handle captures the calling injection\n * context, so its methods can be invoked lazily from `computed()` or a\n * template — not only during construction. Listeners are torn down with the\n * injector. `active`'s per-breakpoint queries are materialized eagerly at call\n * time, so reading `active` never attaches a listener from inside a reactive\n * computation or after teardown. On the server (or where `matchMedia` is\n * unavailable) every signal reads `false` and `active` reads `null`.\n *\n * Must be called from an injection context.\n *\n * @returns A {@link ForBreakpoints} handle of reactive query methods.\n */\nexport function injectBreakpoints(): ForBreakpoints {\n const injector = inject(Injector);\n const map = inject(FOR_BREAKPOINTS_DEFAULTS).breakpoints;\n const names = Object.keys(map).sort((a, b) => map[a]! - map[b]!);\n const cache = new Map<string, Signal<boolean>>();\n\n const observe = (query: string): Signal<boolean> => {\n let result = cache.get(query);\n if (result === undefined) {\n result = runInInjectionContext(injector, () => injectMediaQuery(query));\n cache.set(query, result);\n }\n return result;\n };\n\n const thresholdOf = (name: string): number => {\n const value = map[name];\n if (value === undefined) {\n throw new Error(\n `[forty-cdk/breakpoints] Unknown breakpoint \"${name}\". Defined breakpoints: ${\n names.join(', ') || '(none)'\n }.`,\n );\n }\n return value;\n };\n\n const up = (name: string): Signal<boolean> => observe(`(min-width: ${thresholdOf(name)}px)`);\n\n const down = (name: string): Signal<boolean> =>\n observe(`(max-width: ${thresholdOf(name) - 0.02}px)`);\n\n const between = (min: string, max: string): Signal<boolean> =>\n observe(`(min-width: ${thresholdOf(min)}px) and (max-width: ${thresholdOf(max) - 0.02}px)`);\n\n const only = (name: string): Signal<boolean> => {\n const lower = thresholdOf(name);\n const index = names.indexOf(name);\n const next = index < names.length - 1 ? thresholdOf(names[index + 1]!) : null;\n return next === null\n ? observe(`(min-width: ${lower}px)`)\n : observe(`(min-width: ${lower}px) and (max-width: ${next - 0.02}px)`);\n };\n\n const activeCandidates = names.map((name) => ({\n name,\n matches: observe(`(min-width: ${map[name]}px)`),\n }));\n\n const active = computed<BreakpointName | null>(() => {\n let current: string | null = null;\n for (const candidate of activeCandidates) {\n if (candidate.matches()) {\n current = candidate.name;\n }\n }\n return current as BreakpointName | null;\n });\n\n return { up, down, between, only, active, matches: (query) => observe(query) };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -24,7 +24,8 @@ import { isPlatformBrowser } from '@angular/common';
|
|
|
24
24
|
* - Bootstrap-safety: `TestBed.resetTestingModule()`, micro-frontend reloads,
|
|
25
25
|
* and anything else that destroys `ApplicationRef` must not leave stale
|
|
26
26
|
* `document` listeners behind. The listeners are registered in the
|
|
27
|
-
* constructor and
|
|
27
|
+
* constructor against a single `AbortController` and dropped by one
|
|
28
|
+
* `abort()` from `DestroyRef`.
|
|
28
29
|
* - SSR safety: `document` is inaccessible on the server. The service is a
|
|
29
30
|
* no-op when `PLATFORM_ID` is not the browser; `keyboard` stays `false` and
|
|
30
31
|
* no listener is installed.
|
|
@@ -58,12 +59,11 @@ class InputModality {
|
|
|
58
59
|
if (!this.#isBrowser) {
|
|
59
60
|
return;
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
});
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const options = { capture: true, signal: controller.signal };
|
|
64
|
+
this.#document.addEventListener('keydown', this.#onKeyDown, options);
|
|
65
|
+
this.#document.addEventListener('pointerdown', this.#onPointerDown, options);
|
|
66
|
+
inject(DestroyRef).onDestroy(() => controller.abort());
|
|
67
67
|
}
|
|
68
68
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: InputModality, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
69
69
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: InputModality, providedIn: 'root' });
|
|
@@ -139,12 +139,11 @@ function injectHovered(opts) {
|
|
|
139
139
|
const onPointerLeave = () => {
|
|
140
140
|
hovered.set(false);
|
|
141
141
|
};
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
});
|
|
142
|
+
const controller = new AbortController();
|
|
143
|
+
const options = { signal: controller.signal };
|
|
144
|
+
el.addEventListener('pointerenter', onPointerEnter, options);
|
|
145
|
+
el.addEventListener('pointerleave', onPointerLeave, options);
|
|
146
|
+
inject(DestroyRef).onDestroy(() => controller.abort());
|
|
148
147
|
return computed(() => (disabled?.() ? false : hovered()));
|
|
149
148
|
}
|
|
150
149
|
|
|
@@ -205,20 +204,15 @@ function injectPressed(opts) {
|
|
|
205
204
|
const onBlur = () => {
|
|
206
205
|
pressed.set(false);
|
|
207
206
|
};
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
el.addEventListener('
|
|
211
|
-
el.addEventListener('
|
|
212
|
-
el.addEventListener('
|
|
213
|
-
el.addEventListener('
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
el.removeEventListener('pointerleave', onPointerEnd);
|
|
218
|
-
el.removeEventListener('keydown', onKeyDown);
|
|
219
|
-
el.removeEventListener('keyup', onKeyUp);
|
|
220
|
-
el.removeEventListener('blur', onBlur);
|
|
221
|
-
});
|
|
207
|
+
const controller = new AbortController();
|
|
208
|
+
const options = { signal: controller.signal };
|
|
209
|
+
el.addEventListener('pointerdown', onPointerDown, options);
|
|
210
|
+
el.addEventListener('pointerup', onPointerEnd, options);
|
|
211
|
+
el.addEventListener('pointerleave', onPointerEnd, options);
|
|
212
|
+
el.addEventListener('keydown', onKeyDown, options);
|
|
213
|
+
el.addEventListener('keyup', onKeyUp, options);
|
|
214
|
+
el.addEventListener('blur', onBlur, options);
|
|
215
|
+
inject(DestroyRef).onDestroy(() => controller.abort());
|
|
222
216
|
return computed(() => (disabled?.() ? false : pressed()));
|
|
223
217
|
}
|
|
224
218
|
|