forty-cdk 0.25.2 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/accordion/README.md +86 -38
  2. package/aspect-ratio/README.md +17 -34
  3. package/avatar/README.md +35 -37
  4. package/breadcrumbs/README.md +54 -13
  5. package/breakpoints/README.md +80 -24
  6. package/button/README.md +42 -15
  7. package/calendar/README.md +171 -49
  8. package/carousel/README.md +200 -56
  9. package/checkbox/README.md +73 -36
  10. package/combobox/README.md +245 -107
  11. package/context-menu/README.md +105 -99
  12. package/date-field/README.md +57 -29
  13. package/date-picker/README.md +118 -68
  14. package/dialog/README.md +247 -198
  15. package/disclosure/README.md +37 -32
  16. package/drag-drop/README.md +157 -59
  17. package/drawer/README.md +183 -63
  18. package/dropdown-menu/README.md +113 -115
  19. package/fesm2022/forty-cdk-checkbox.mjs.map +1 -1
  20. package/fesm2022/forty-cdk-combobox.mjs +29 -6
  21. package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
  22. package/fesm2022/forty-cdk-core-overlay.mjs +86 -46
  23. package/fesm2022/forty-cdk-core-overlay.mjs.map +1 -1
  24. package/fesm2022/forty-cdk-core.mjs +86 -10
  25. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  26. package/fesm2022/forty-cdk-date-picker.mjs +7 -5
  27. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
  28. package/fesm2022/forty-cdk-dialog.mjs +7 -1
  29. package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
  30. package/fesm2022/forty-cdk-drag-drop.mjs +14 -6
  31. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
  32. package/fesm2022/forty-cdk-field.mjs +61 -25
  33. package/fesm2022/forty-cdk-field.mjs.map +1 -1
  34. package/fesm2022/forty-cdk-fieldset.mjs +6 -1
  35. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -1
  36. package/fesm2022/forty-cdk-listbox.mjs +23 -12
  37. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  38. package/fesm2022/forty-cdk-menu.mjs +4 -2
  39. package/fesm2022/forty-cdk-menu.mjs.map +1 -1
  40. package/fesm2022/forty-cdk-menubar.mjs +18 -4
  41. package/fesm2022/forty-cdk-menubar.mjs.map +1 -1
  42. package/fesm2022/forty-cdk-popover.mjs +5 -3
  43. package/fesm2022/forty-cdk-popover.mjs.map +1 -1
  44. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -1
  45. package/fesm2022/forty-cdk-select.mjs +14 -15
  46. package/fesm2022/forty-cdk-select.mjs.map +1 -1
  47. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  48. package/fesm2022/forty-cdk-time-picker.mjs +3 -3
  49. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
  50. package/fesm2022/forty-cdk-tooltip.mjs +1 -0
  51. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -1
  52. package/fesm2022/forty-cdk-tree.mjs +208 -59
  53. package/fesm2022/forty-cdk-tree.mjs.map +1 -1
  54. package/field/README.md +53 -43
  55. package/fieldset/README.md +39 -34
  56. package/file-upload/README.md +80 -32
  57. package/hover-card/README.md +79 -54
  58. package/input/README.md +93 -85
  59. package/internationalized-date/README.md +5 -3
  60. package/listbox/README.md +112 -57
  61. package/menu/README.md +234 -62
  62. package/menubar/README.md +84 -64
  63. package/meter/README.md +37 -39
  64. package/navigation-menu/README.md +138 -45
  65. package/number-input/README.md +32 -31
  66. package/otp-input/README.md +74 -70
  67. package/package.json +1 -1
  68. package/pagination/README.md +75 -11
  69. package/pane-resizer/README.md +46 -56
  70. package/popover/README.md +90 -63
  71. package/progress/README.md +28 -44
  72. package/radio-group/README.md +77 -30
  73. package/scroll-area/README.md +92 -117
  74. package/search/README.md +113 -46
  75. package/select/README.md +258 -104
  76. package/separator/README.md +25 -26
  77. package/shared/README.md +30 -24
  78. package/slider/README.md +64 -38
  79. package/stepper/README.md +191 -112
  80. package/switch/README.md +36 -30
  81. package/table/README.md +381 -186
  82. package/table-virtualization/README.md +29 -27
  83. package/tabs/README.md +79 -40
  84. package/time-field/README.md +53 -20
  85. package/time-picker/README.md +104 -43
  86. package/toast/README.md +174 -98
  87. package/toggle/README.md +79 -70
  88. package/toolbar/README.md +85 -26
  89. package/tooltip/README.md +90 -70
  90. package/tree/README.md +244 -131
  91. package/types/forty-cdk-checkbox.d.ts +1 -1
  92. package/types/forty-cdk-combobox.d.ts +11 -3
  93. package/types/forty-cdk-core-overlay.d.ts +35 -13
  94. package/types/forty-cdk-core.d.ts +89 -18
  95. package/types/forty-cdk-dialog.d.ts +2 -1
  96. package/types/forty-cdk-drag-drop.d.ts +2 -2
  97. package/types/forty-cdk-field.d.ts +26 -9
  98. package/types/forty-cdk-listbox.d.ts +8 -1
  99. package/types/forty-cdk-menu.d.ts +12 -10
  100. package/types/forty-cdk-menubar.d.ts +21 -3
  101. package/types/forty-cdk-radio-group.d.ts +1 -1
  102. package/types/forty-cdk-select.d.ts +1 -1
  103. package/types/forty-cdk-table.d.ts +1 -1
  104. package/types/forty-cdk-tooltip.d.ts +6 -5
  105. package/types/forty-cdk-tree.d.ts +66 -10
  106. package/virtual-reorder/README.md +25 -23
  107. package/virtualization/README.md +132 -43
  108. package/visually-hidden/README.md +69 -32
@@ -9,6 +9,12 @@ apgUrl: https://www.w3.org/WAI/ARIA/apg/patterns/accordion/
9
9
 
10
10
  A stack of collapsible sections, optionally allowing multiple panels open at once.
11
11
 
12
+ ## When to choose
13
+
14
+ - **Accordion**: a group of collapsible items under one root. `[(value)]` holds which are open, `multiple` decides whether more than one may be, and ArrowUp / ArrowDown / Home / End move focus across the triggers.
15
+ - **[Disclosure](../disclosure/README.md)**: a single trigger and its region, with no shared state and no arrow-key navigation. Stacking several of them is not an accordion, and that is the right shape when the panels are unrelated.
16
+ - **[Tabs](../tabs/README.md)**: when exactly one panel is ever visible and the panels are alternatives rather than sections the reader may open together.
17
+
12
18
  ## Anatomy
13
19
 
14
20
  ```html
@@ -25,8 +31,10 @@ A stack of collapsible sections, optionally allowing multiple panels open at onc
25
31
 
26
32
  ## Examples
27
33
 
34
+ Open a panel with the pointer or `Enter`, move between headers with the arrow keys, and watch `data-state` flip on the item, its trigger and its content together.
35
+
28
36
  ```ts
29
- import { Component, signal } from '@angular/core';
37
+ import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
30
38
  import {
31
39
  ForAccordion,
32
40
  ForAccordionContent,
@@ -34,43 +42,83 @@ import {
34
42
  ForAccordionTrigger,
35
43
  } from 'forty-cdk/accordion';
36
44
 
45
+ interface AccordionEntry {
46
+ readonly value: string;
47
+ readonly title: string;
48
+ readonly body: string;
49
+ }
50
+
37
51
  @Component({
38
- selector: 'demo-faq',
52
+ selector: 'app-accordion-default-example',
53
+ changeDetection: ChangeDetectionStrategy.OnPush,
39
54
  imports: [ForAccordion, ForAccordionItem, ForAccordionTrigger, ForAccordionContent],
40
55
  template: `
41
- <div forAccordion [(value)]="open" collapsible>
42
- <div forAccordionItem value="shipping">
43
- <h3>
44
- <button type="button" forAccordionTrigger class="accordion-trigger">Shipping</button>
45
- </h3>
46
- <section forAccordionContent>Ships in 24h.</section>
47
- </div>
48
- <div forAccordionItem value="returns">
49
- <h3>
50
- <button type="button" forAccordionTrigger class="accordion-trigger">Returns</button>
51
- </h3>
52
- <section forAccordionContent>Free 30-day returns.</section>
53
- </div>
56
+ <div forAccordion class="acc-root" [(value)]="value" collapsible>
57
+ @for (item of items; track item.value) {
58
+ <div forAccordionItem class="acc-item" [value]="item.value">
59
+ <h3 class="acc-heading">
60
+ <button type="button" forAccordionTrigger class="acc-trigger">
61
+ <span>{{ item.title }}</span>
62
+ <span class="chevron" aria-hidden="true"></span>
63
+ </button>
64
+ </h3>
65
+ <section forAccordionContent class="acc-content">
66
+ <div class="acc-inner">
67
+ <p>{{ item.body }}</p>
68
+ </div>
69
+ </section>
70
+ </div>
71
+ }
54
72
  </div>
55
73
  `,
56
74
  })
57
- export class DemoFaq {
58
- readonly open = signal<readonly string[]>([]);
75
+ export class AccordionDefaultExample {
76
+ protected readonly items: readonly AccordionEntry[] = [
77
+ {
78
+ value: 'a',
79
+ title: 'What is forty-cdk?',
80
+ body: 'A library of headless UI primitives with built-in WAI-ARIA accessibility.',
81
+ },
82
+ {
83
+ value: 'b',
84
+ title: 'Does it ship styles?',
85
+ body: 'No. It exposes state, behavior, focus and ARIA; you apply the styles yourself.',
86
+ },
87
+ {
88
+ value: 'c',
89
+ title: 'Does it work without Zone.js?',
90
+ body: 'Yes, it is designed to run under provideZonelessChangeDetection().',
91
+ },
92
+ ];
93
+
94
+ protected readonly value = signal<readonly string[]>(['a']);
59
95
  }
60
96
  ```
61
97
 
98
+ ### Multiple
99
+
100
+ `multiple` lets several sections stay open at once, so `value` holds an array of every open item.
101
+
102
+ ### Horizontal
103
+
104
+ `orientation='horizontal'` lays the sections out in a row and switches roving navigation to `ArrowLeft` / `ArrowRight`. It is reflected as `data-orientation` for styling.
105
+
106
+ ### Disabled item
107
+
108
+ A disabled item cannot be toggled and is skipped by the arrow keys, while staying in the DOM for screen readers.
109
+
62
110
  ## API
63
111
 
64
112
  ### `ForAccordion`
65
113
 
66
- | Property | Type | Description |
67
- | ------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
68
- | `value` | `model<readonly string[]>` | Currently open item values. In single mode the array has 0 or 1 element.<br>**Default:** — |
69
- | `multiple` | `input<boolean>` | When true, multiple items can be open simultaneously.<br>**Default:** `false` |
70
- | `collapsible` | `input<boolean>` | Single mode only: when true, the open item can be collapsed by clicking it. Otherwise once any item is open, exactly one stays open.<br>**Default:** `false` |
71
- | `disabled` | `input<boolean>` | When true, disables every item — each trigger reflects the native `disabled` attribute and cannot toggle. Composes with a per-item `[disabled]`.<br>**Default:** `false` |
72
- | `orientation` | `input<'horizontal' \| 'vertical'>` | Layout direction of the trigger list. In horizontal mode ArrowLeft/Right replace ArrowUp/Down.<br>**Default:** `'vertical'` |
73
- | `dir` | `input<'ltr' \| 'rtl'>` | Writing direction. Only relevant in horizontal mode — swaps the meaning of Left/Right arrows.<br>**Default:** — |
114
+ | Property | Type | Description |
115
+ | ------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
116
+ | `value` | `model<readonly string[]>` | Currently open item values. In single mode the array has 0 or 1 element.<br>**Default:** — |
117
+ | `multiple` | `input<boolean>` | When true, multiple items can be open simultaneously.<br>**Default:** `false` |
118
+ | `collapsible` | `input<boolean>` | Single mode only: when true, the open item can be collapsed by clicking it. Otherwise once any item is open, exactly one stays open.<br>**Default:** `false` |
119
+ | `disabled` | `input<boolean>` | When true, disables every item: each trigger reflects the native `disabled` attribute and cannot toggle. Composes with a per-item `[disabled]`.<br>**Default:** `false` |
120
+ | `orientation` | `input<'horizontal' \| 'vertical'>` | Layout direction of the trigger list. In horizontal mode ArrowLeft/Right replace ArrowUp/Down.<br>**Default:** `'vertical'` |
121
+ | `dir` | `input<'ltr' \| 'rtl'>` | Writing direction. Only relevant in horizontal mode, where it swaps the meaning of Left/Right arrows.<br>**Default:** — |
74
122
 
75
123
  | Data attribute | Values |
76
124
  | ------------------ | -------------------------- |
@@ -106,35 +154,35 @@ export class DemoFaq {
106
154
 
107
155
  ## Keyboard
108
156
 
109
- | Key | Action |
110
- | -------------------------------------------- | -------------------------------------------------------------------------------------------------- |
111
- | <kbd>Enter</kbd> / <kbd>Space</kbd> | Toggle the focused trigger (native button). |
112
- | <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd> | Move focus between triggers (vertical, default). Wrap-around, skips disabled. |
113
- | <kbd>ArrowLeft</kbd> / <kbd>ArrowRight</kbd> | Move focus between triggers (horizontal — flipped under `dir='rtl'`). Wrap-around, skips disabled. |
114
- | <kbd>Home</kbd> | Jump to the first trigger. |
115
- | <kbd>End</kbd> | Jump to the last trigger. |
157
+ | Key | Action |
158
+ | -------------------------------------------- | ------------------------------------------------------------------------------------------------- |
159
+ | <kbd>Enter</kbd> / <kbd>Space</kbd> | Toggle the focused trigger (native button). |
160
+ | <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd> | Move focus between triggers (vertical, default). Wrap-around, skips disabled. |
161
+ | <kbd>ArrowLeft</kbd> / <kbd>ArrowRight</kbd> | Move focus between triggers (horizontal, flipped under `dir='rtl'`). Wrap-around, skips disabled. |
162
+ | <kbd>Home</kbd> | Jump to the first trigger. |
163
+ | <kbd>End</kbd> | Jump to the last trigger. |
116
164
 
117
165
  ## Accessibility
118
166
 
119
- - **Heading wrapper is your job.** The library does not render a heading around the trigger — wrap it in the heading level (`<h2>`–`<h6>`) appropriate to your document outline. Without it, screen-reader landmark navigation is broken.
167
+ - **Heading wrapper is your job.** The library does not render a heading around the trigger, so wrap it in the heading level (`<h2>`–`<h6>`) appropriate to your document outline. Without it, screen-reader landmark navigation is broken.
120
168
  - **Use a real `<button type="button">` for the trigger.** Native Enter / Space activation and focus come for free; the directive does not synthesize them.
121
169
  - **`role="region"`** is added to every panel automatically. APG recommends suppressing it on accordions with 6+ panels to avoid landmark proliferation; there is currently no opt-out.
122
170
  - **Closed panels leave the accessibility tree.** While closed, `ForAccordionContent` sets `aria-hidden="true"` and `inert` on the panel, removing it from both the accessibility tree and the focus order. The directive does **not** apply `[hidden]`, so pick how to hide it visually:
123
- - **Mount / unmount with `@if (item.expanded())`** — the panel is absent from the DOM while closed; the cleanest path for `animate.enter` / `animate.leave`. The trigger emits `aria-controls` only while expanded, so the reference never dangles at an unmounted panel.
124
- - **Leave it mounted** — preserve internal state or run CSS-only transitions off `data-state`. Add `display: none` (or your own collapse animation) keyed on `[data-state="closed"]` to also hide it visually.
171
+ - **Mount / unmount with `@if (item.expanded())`**: the panel is absent from the DOM while closed; the cleanest path for `animate.enter` / `animate.leave`. The trigger emits `aria-controls` only while expanded, so the reference never dangles at an unmounted panel.
172
+ - **Leave it mounted**: preserve internal state or run CSS-only transitions off `data-state`. Add `display: none` (or your own collapse animation) keyed on `[data-state="closed"]` to also hide it visually.
125
173
  - **`aria-disabled`** is applied to the open trigger only when single mode is active and `collapsible=false`, indicating the user cannot collapse it from this trigger.
126
174
  - **A truly disabled item (`[disabled]` on `[forAccordionItem]`) uses the native `disabled` attribute on the trigger, by design.** The trigger is a real single-purpose `<button>`, not a roving-tabindex collection item (each trigger stays independently in the Tab order; arrow-key navigation is the APG-optional enhancement on top). The disabled trigger leaves the Tab order and the arrow-key navigation (which already skips it), but stays in the accessibility tree so screen readers announce it as unavailable in browse mode. The [APG Accordion pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/) does not require disabled headers to remain focusable.
127
175
 
128
176
  ## Styling
129
177
 
130
- forty-cdk ships no styles. Add your own class to each piece — the `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../docs/styling.md)). Key your CSS off the reflected `data-*` attributes listed per piece in the [API](#api) section.
178
+ forty-cdk ships no styles. Add your own class to each piece. The `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../docs/styling.md)). Key your CSS off the reflected `data-*` attributes listed per piece in the [API](#api) section.
131
179
 
132
180
  ```css
133
- .trigger-chevron {
181
+ .chevron {
134
182
  transition: transform 150ms ease;
135
183
  }
136
184
 
137
- .accordion-trigger[data-state='open'] .trigger-chevron {
185
+ .acc-trigger[data-state='open'] .chevron {
138
186
  transform: rotate(180deg);
139
187
  }
140
188
  ```
@@ -8,21 +8,21 @@ archetype: [composable-ui]
8
8
 
9
9
  A container that keeps its content at a fixed width-to-height ratio.
10
10
 
11
- Pure visual utility — it locks an element's box via the native CSS `aspect-ratio` property, with no ARIA semantics. Reach for it to reserve space for media before it loads (preventing layout shift), keep cards on a grid uniform, or wrap responsive iframes.
11
+ It is a pure visual utility that locks an element's box via the native CSS `aspect-ratio` property, with no ARIA semantics. Reach for it to reserve space for media before it loads (preventing layout shift), keep cards on a grid uniform, or wrap responsive iframes.
12
12
 
13
13
  ## Why this exists
14
14
 
15
- A fixed, never-changing ratio is one line of CSS — you don't need this primitive for that:
15
+ You don't need this primitive for a fixed, never-changing ratio, which is one line of CSS:
16
16
 
17
17
  ```css
18
- .card-cover {
18
+ .box {
19
19
  aspect-ratio: 16 / 9;
20
20
  }
21
21
  ```
22
22
 
23
23
  `[forAspectRatio]` earns its place when the ratio is **dynamic or must be validated**. It is more than the static declaration:
24
24
 
25
- - **Reactive `ratio` input.** Bind `[ratio]="ratio()"` and the host style recomputes as the value changes — no manual style writes.
25
+ - **Reactive `ratio` input.** Bind `[ratio]="ratio()"` and the host style recomputes as the value changes, with no manual style writes.
26
26
  - **Invalid-value guarding.** `0`, negative, and non-finite ratios fall back to `1`, so a bad computed value never emits invalid CSS.
27
27
  - **SSR-safe.** The `aspect-ratio` style is bound declaratively (never touched imperatively), so it renders identically on the server and hydrates cleanly.
28
28
  - **Consistent headless API.** Same shape as the other primitives, so it composes the same way.
@@ -39,46 +39,29 @@ If your ratio is a literal constant, prefer the CSS property directly and keep t
39
39
 
40
40
  ## Examples
41
41
 
42
+ Resize the preview and watch the frame hold its 16 / 9 ratio. The primitive writes the ratio and nothing else, so every border, colour and inset below is your own CSS.
43
+
42
44
  ```ts
43
- import { Component } from '@angular/core';
45
+ import { ChangeDetectionStrategy, Component } from '@angular/core';
44
46
  import { ForAspectRatio } from 'forty-cdk/aspect-ratio';
45
47
 
46
48
  @Component({
47
- selector: 'demo-aspect-ratio',
49
+ selector: 'app-aspect-ratio-default-example',
50
+ changeDetection: ChangeDetectionStrategy.OnPush,
48
51
  imports: [ForAspectRatio],
49
52
  template: `
50
- <div forAspectRatio [ratio]="16 / 9" class="card-cover">
51
- <img src="cover.jpg" alt="" />
52
- </div>
53
-
54
- <div forAspectRatio ratio="1" class="avatar">
55
- <img src="me.jpg" alt="Me" />
56
- </div>
57
-
58
- <div forAspectRatio [ratio]="21 / 9" class="hero">
59
- <video src="hero.mp4" autoplay loop muted></video>
53
+ <div forAspectRatio class="box" [ratio]="16 / 9">
54
+ <span class="label">16 / 9</span>
60
55
  </div>
61
56
  `,
62
- styles: [
63
- `
64
- .card-cover,
65
- .avatar,
66
- .hero {
67
- width: 100%;
68
- }
69
- .card-cover img,
70
- .avatar img,
71
- .hero video {
72
- width: 100%;
73
- height: 100%;
74
- object-fit: cover;
75
- }
76
- `,
77
- ],
78
57
  })
79
- export class DemoAspectRatio {}
58
+ export class AspectRatioDefaultExample {}
80
59
  ```
81
60
 
61
+ ### Square (1 / 1)
62
+
63
+ Set `ratio` to `1` to keep a box perfectly square at any width. That is handy for avatars, thumbnails, or uniform grid cards.
64
+
82
65
  ## API
83
66
 
84
67
  ### `ForAspectRatio`
@@ -93,7 +76,7 @@ export class DemoAspectRatio {}
93
76
 
94
77
  ## Styling
95
78
 
96
- forty-cdk ships no styles. Add your own class to each piece — the `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../docs/styling.md)). This primitive is purely structural: its only host effect is the native `aspect-ratio` style, so it reflects no `data-*` attributes and writes no CSS custom properties. Style the host through your own class on `[forAspectRatio]`.
79
+ forty-cdk ships no styles. Add your own class to each piece. The `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../docs/styling.md)). This primitive is purely structural: its only host effect is the native `aspect-ratio` style, so it reflects no `data-*` attributes and writes no CSS custom properties. Style the host through your own class on `[forAspectRatio]`.
97
80
 
98
81
  ## Behavior notes
99
82
 
package/avatar/README.md CHANGED
@@ -8,7 +8,7 @@ archetype: [composable-ui]
8
8
 
9
9
  A user image with a graceful fallback across its loading lifecycle.
10
10
 
11
- Headless and presentational — it tracks the load lifecycle of an `<img>` and lets you choose what to show while loading or after an error. There is no WAI-ARIA pattern for avatars, so the directive imposes no `role` of its own.
11
+ It is headless and presentational: it tracks the load lifecycle of an `<img>` and lets you choose what to show while loading or after an error. There is no WAI-ARIA pattern for avatars, so the directive imposes no `role` of its own.
12
12
 
13
13
  ## Anatomy
14
14
 
@@ -22,52 +22,50 @@ Headless and presentational — it tracks the load lifecycle of an `<img>` and l
22
22
 
23
23
  ## Examples
24
24
 
25
+ Let the image load, then break its URL: `data-status` moves between `loading`, `loaded` and `error`, and the fallback only appears once the delay has passed without an image.
26
+
25
27
  ```ts
26
- import { Component, signal } from '@angular/core';
28
+ import { ChangeDetectionStrategy, Component } from '@angular/core';
27
29
  import { ForAvatar, ForAvatarFallback, ForAvatarImage } from 'forty-cdk/avatar';
28
30
 
31
+ const AVATAR_SRC =
32
+ 'data:image/svg+xml;utf8,' +
33
+ encodeURIComponent(
34
+ `<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 72 72">
35
+ <defs>
36
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
37
+ <stop offset="0" stop-color="#6366f1" />
38
+ <stop offset="1" stop-color="#ec4899" />
39
+ </linearGradient>
40
+ </defs>
41
+ <rect width="72" height="72" fill="url(#g)" />
42
+ <circle cx="36" cy="28" r="14" fill="#fff" opacity="0.92" />
43
+ <path d="M14 64c0-12 9.8-20 22-20s22 8 22 20Z" fill="#fff" opacity="0.92" />
44
+ </svg>`,
45
+ );
46
+
29
47
  @Component({
30
- selector: 'demo-avatar',
48
+ selector: 'app-avatar-default-example',
49
+ changeDetection: ChangeDetectionStrategy.OnPush,
31
50
  imports: [ForAvatar, ForAvatarImage, ForAvatarFallback],
32
51
  template: `
33
- <span forAvatar #a="forAvatar" class="avatar" fallbackDelayMs="500">
34
- <img forAvatarImage class="avatar-image" [src]="user.avatarUrl" [alt]="user.name" />
35
- @if (a.shouldShowFallback()) {
36
- <span forAvatarFallback class="avatar-fallback">{{ initials() }}</span>
52
+ <span forAvatar #avatar="forAvatar" class="avatar" [fallbackDelayMs]="500">
53
+ <img forAvatarImage class="avatar-image" [src]="src" alt="Ada Lovelace" />
54
+ @if (avatar.shouldShowFallback()) {
55
+ <span forAvatarFallback class="avatar-fallback">AL</span>
37
56
  }
38
57
  </span>
39
58
  `,
40
- styles: [
41
- `
42
- .avatar {
43
- display: inline-flex;
44
- width: 40px;
45
- height: 40px;
46
- border-radius: 999px;
47
- overflow: hidden;
48
- background: #eee;
49
- font: 600 14px/40px system-ui;
50
- align-items: center;
51
- justify-content: center;
52
- }
53
- .avatar-image {
54
- width: 100%;
55
- height: 100%;
56
- object-fit: cover;
57
- }
58
- .avatar-image[data-status='loading'],
59
- .avatar-image[data-status='error'] {
60
- display: none;
61
- }
62
- `,
63
- ],
64
59
  })
65
- export class DemoAvatar {
66
- readonly user = { name: 'Ada Lovelace', avatarUrl: '/api/avatar/ada.jpg' };
67
- readonly initials = signal('AL');
60
+ export class AvatarDefaultExample {
61
+ protected readonly src = AVATAR_SRC;
68
62
  }
69
63
  ```
70
64
 
65
+ ### Failed load
66
+
67
+ When the image errors, the directive flips `shouldShowFallback()` and the initials render in its place. An error shows the fallback at once, skipping the `fallbackDelayMs` wait.
68
+
71
69
  ## API
72
70
 
73
71
  ### `ForAvatar`
@@ -104,7 +102,7 @@ The directive does not impose a `role`. Pair the avatar with visible name text o
104
102
 
105
103
  ## Styling
106
104
 
107
- forty-cdk ships no styles. Add your own class to each piece — the `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../docs/styling.md)). Key your CSS off the reflected `data-*` attributes listed per piece in the [API](#api) section.
105
+ forty-cdk ships no styles. Add your own class to each piece. The `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../docs/styling.md)). Key your CSS off the reflected `data-*` attributes listed per piece in the [API](#api) section.
108
106
 
109
107
  ```css
110
108
  .avatar-image:not([data-status='loaded']) {
@@ -117,9 +115,9 @@ forty-cdk ships no styles. Add your own class to each piece — the `for*` selec
117
115
 
118
116
  ## Behavior notes
119
117
 
120
- - **Cached images are detected on first render.** If the browser already has the image cached, `load`/`error` may not fire — the directive checks `<img>.complete` and `naturalWidth` after the first render and reports `loaded` / `error` accordingly. A cached image that is `complete` but has zero intrinsic width (e.g. an SVG without explicit dimensions) is ambiguous, so the directive stays `loading` and confirms validity with `img.decode()` rather than pessimistically flagging `error`.
118
+ - **Cached images are detected on first render.** If the browser already has the image cached, `load`/`error` may not fire. The directive checks `<img>.complete` and `naturalWidth` after the first render and reports `loaded` / `error` accordingly. A cached image that is `complete` but has zero intrinsic width (e.g. an SVG without explicit dimensions) is ambiguous, so the directive stays `loading` and confirms validity with `img.decode()` rather than pessimistically flagging `error`.
121
119
  - **Multiple images per avatar are not supported.** Each `[forAvatar]` expects exactly one `[forAvatarImage]`. If you need cascading sources (CDN → fallback URL → fallback content), swap `src` on a single image.
122
- - **`alt` is consumer territory.** Because `<img>` is the host element, the consumer keeps full control of `alt` — set `""` for purely decorative avatars next to a name, or describe the person if the avatar stands alone.
120
+ - **`alt` is consumer territory.** Because `<img>` is the host element, the consumer keeps full control of `alt`. Set `""` for purely decorative avatars next to a name, or describe the person if the avatar stands alone.
123
121
  - **The image stays in the DOM.** Hide it via CSS `[data-status="loading"], [data-status="error"] { display: none }` if your consumer-side styling needs it gone. The fallback uses `@if`, so it only mounts when needed.
124
122
 
125
123
  ## Wrapping in a design system
@@ -23,34 +23,75 @@ A labelled navigation landmark for a breadcrumb trail: links with aria-current='
23
23
 
24
24
  ## Examples
25
25
 
26
+ Walk the trail with `Tab`: the last crumb is the page you are on, so it carries `aria-current="page"` and is not a link back to itself.
27
+
26
28
  ```ts
27
- import { Component } from '@angular/core';
29
+ import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
28
30
  import { ForBreadcrumbItem, ForBreadcrumbSeparator, ForBreadcrumbs } from 'forty-cdk/breadcrumbs';
29
31
 
32
+ interface Crumb {
33
+ readonly label: string;
34
+ readonly href: string;
35
+ }
36
+
30
37
  @Component({
31
- selector: 'demo-breadcrumbs',
38
+ selector: 'app-breadcrumbs-default-example',
39
+ changeDetection: ChangeDetectionStrategy.OnPush,
32
40
  imports: [ForBreadcrumbs, ForBreadcrumbItem, ForBreadcrumbSeparator],
33
41
  template: `
34
- <nav forBreadcrumbs>
35
- <ol>
36
- <li><a forBreadcrumbItem href="/">Home</a></li>
37
- <li forBreadcrumbSeparator>/</li>
38
- <li><a forBreadcrumbItem href="/library">Library</a></li>
39
- <li forBreadcrumbSeparator>/</li>
40
- <li><a forBreadcrumbItem href="/library/data" current>Data</a></li>
42
+ <nav forBreadcrumbs class="bc">
43
+ <ol class="bc-list">
44
+ @for (crumb of crumbs(); track crumb.href; let last = $last) {
45
+ <li class="bc-li">
46
+ <a
47
+ forBreadcrumbItem
48
+ class="bc-link"
49
+ [href]="crumb.href"
50
+ [current]="last"
51
+ (click)="$event.preventDefault()"
52
+ >
53
+ {{ crumb.label }}
54
+ </a>
55
+ </li>
56
+ @if (!last) {
57
+ <li forBreadcrumbSeparator class="bc-sep">
58
+ <svg viewBox="0 0 24 24" aria-hidden="true">
59
+ <path
60
+ d="m8.25 4.5 7.5 7.5-7.5 7.5"
61
+ fill="none"
62
+ stroke="currentColor"
63
+ stroke-width="1.75"
64
+ stroke-linecap="round"
65
+ stroke-linejoin="round"
66
+ />
67
+ </svg>
68
+ </li>
69
+ }
70
+ }
41
71
  </ol>
42
72
  </nav>
43
73
  `,
44
74
  })
45
- export class DemoBreadcrumbs {}
75
+ export class BreadcrumbsDefaultExample {
76
+ protected readonly crumbs = signal<readonly Crumb[]>([
77
+ { label: 'Home', href: '/' },
78
+ { label: 'Components', href: '/components' },
79
+ { label: 'Navigation', href: '/components/navigation' },
80
+ { label: 'Breadcrumbs', href: '/components/navigation/breadcrumbs' },
81
+ ]);
82
+ }
46
83
  ```
47
84
 
48
- The root defaults its label to `Breadcrumb`. Override it with `ariaLabel="…"` (or point a native `aria-labelledby` at a visible heading) when a page hosts more than one breadcrumb trail.
85
+ ### Collapsing a long trail
86
+
87
+ The primitive renders whatever items you give it, so collapsing a deep path is a consumer decision. Here the middle is folded into an expandable ellipsis button that reveals the hidden crumbs. The trail stays a single accessible navigation landmark either way.
49
88
 
50
- ### Localizing the label
89
+ ## Localizing the label
51
90
 
52
91
  `Breadcrumb` is verbalized by screen readers, so translate it per injector scope with `provideForBreadcrumbsDefaults`. Configure it at the application root, or in any component's `providers` to scope the translation to a subtree. A per-instance `[ariaLabel]` still wins over the scope default.
53
92
 
93
+ <!-- snippet: fragment -->
94
+
54
95
  ```ts
55
96
  import { provideForBreadcrumbsDefaults } from 'forty-cdk/breadcrumbs';
56
97
 
@@ -77,7 +118,7 @@ bootstrapApplication(App, {
77
118
 
78
119
  Implements the [WAI-ARIA Breadcrumb pattern](https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/).
79
120
 
80
- - **Navigation landmark.** `[forBreadcrumbs]` applies `role="navigation"` and labels it `aria-label="Breadcrumb"` by default, creating a named landmark that screen-reader users can jump to directly.
121
+ - **Navigation landmark.** `[forBreadcrumbs]` applies `role="navigation"` and labels it `aria-label="Breadcrumb"` by default, creating a named landmark that screen-reader users can jump to directly. When a page hosts more than one trail, name each with `[ariaLabel]`, or point a native `aria-labelledby` at a visible heading.
81
122
  - **Current page.** Set `current` on `[forBreadcrumbItem]` for the active page; the directive reflects `aria-current="page"` so assistive technology announces the user's location in the trail.
82
123
  - **Decorative separators.** `[forBreadcrumbSeparator]` reflects `aria-hidden="true"` so the visual divider (e.g. `/`) is skipped by screen readers.
83
124