@fusion.dev/helix-headless 0.23.0 → 0.24.1

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 (124) hide show
  1. package/custom-elements.json +50579 -26954
  2. package/dist/announcement/root/announcement-root.d.ts +19 -5
  3. package/dist/area-chart/index.d.ts +1 -0
  4. package/dist/area-chart/index.js +1 -0
  5. package/dist/area-chart/root/area-chart-root.d.ts +182 -0
  6. package/dist/area-chart/root/area-chart-root.js +205 -0
  7. package/dist/avatar/group/avatar-group.d.ts +1 -0
  8. package/dist/avatar/group/avatar-group.js +5 -1
  9. package/dist/avatar/root/avatar-root.d.ts +1 -0
  10. package/dist/avatar/root/avatar-root.js +11 -0
  11. package/dist/carousel/root/carousel-root.d.ts +3 -0
  12. package/dist/carousel/root/carousel-root.js +23 -4
  13. package/dist/chat/index.d.ts +1 -1
  14. package/dist/chat/message/chat-message.d.ts +7 -0
  15. package/dist/chat/message/chat-message.js +2 -1
  16. package/dist/chat/root/chat-root.d.ts +195 -4
  17. package/dist/chat/root/chat-root.js +316 -23
  18. package/dist/checkbox-group/root/checkbox-group-root.d.ts +2 -0
  19. package/dist/checkbox-group/root/checkbox-group-root.js +12 -2
  20. package/dist/code-block/index.d.ts +1 -0
  21. package/dist/code-block/index.js +1 -0
  22. package/dist/code-block/patch.d.ts +62 -0
  23. package/dist/code-block/patch.js +93 -0
  24. package/dist/code-block/root/code-block-root.d.ts +82 -17
  25. package/dist/code-block/root/code-block-root.js +83 -6
  26. package/dist/color-picker/root/color-picker-root.d.ts +4 -3
  27. package/dist/color-picker/root/color-picker-root.js +4 -1
  28. package/dist/combobox/root/combobox-root.d.ts +1 -0
  29. package/dist/combobox/root/combobox-root.js +6 -1
  30. package/dist/command/root/command-root.d.ts +7 -0
  31. package/dist/command/root/command-root.js +10 -0
  32. package/dist/data-table/root/data-table-root.d.ts +195 -23
  33. package/dist/data-table/root/data-table-root.js +308 -27
  34. package/dist/description-list/index.d.ts +2 -0
  35. package/dist/description-list/index.js +2 -0
  36. package/dist/description-list/item/description-list-item.d.ts +39 -0
  37. package/dist/description-list/item/description-list-item.js +26 -0
  38. package/dist/description-list/root/description-list-root.d.ts +56 -0
  39. package/dist/description-list/root/description-list-root.js +21 -0
  40. package/dist/element-events.generated.js +27 -6
  41. package/dist/filter-input/chip/filter-chip.d.ts +114 -0
  42. package/dist/filter-input/chip/filter-chip.js +61 -0
  43. package/dist/filter-input/field/filter-field.d.ts +128 -0
  44. package/dist/filter-input/field/filter-field.js +70 -0
  45. package/dist/filter-input/grammar.d.ts +96 -0
  46. package/dist/filter-input/grammar.js +133 -0
  47. package/dist/filter-input/index.d.ts +5 -0
  48. package/dist/filter-input/index.js +5 -0
  49. package/dist/filter-input/menu/filter-menu.d.ts +153 -0
  50. package/dist/filter-input/menu/filter-menu.js +164 -0
  51. package/dist/filter-input/operator/filter-operator.d.ts +105 -0
  52. package/dist/filter-input/operator/filter-operator.js +39 -0
  53. package/dist/filter-input/root/filter-input-root.d.ts +118 -9
  54. package/dist/filter-input/root/filter-input-root.js +96 -2
  55. package/dist/format-date/root/format-date-root.js +5 -2
  56. package/dist/image/root/image-root.d.ts +9 -0
  57. package/dist/image/root/image-root.js +16 -3
  58. package/dist/index.d.ts +5 -0
  59. package/dist/index.js +5 -0
  60. package/dist/kbd/root/kbd-root.d.ts +9 -2
  61. package/dist/kbd/root/kbd-root.js +7 -2
  62. package/dist/line-chart/root/line-chart-root.d.ts +9 -2
  63. package/dist/listbox/root/listbox-root.d.ts +8 -7
  64. package/dist/listbox/root/listbox-root.js +7 -2
  65. package/dist/menu/item/menu-item.d.ts +31 -6
  66. package/dist/menu/item/menu-item.js +18 -5
  67. package/dist/menu/root/menu-root.js +7 -2
  68. package/dist/notifications/group/notification-group.d.ts +129 -0
  69. package/dist/notifications/group/notification-group.js +106 -0
  70. package/dist/notifications/index.d.ts +1 -0
  71. package/dist/notifications/index.js +1 -0
  72. package/dist/notifications/notification/notification.d.ts +39 -13
  73. package/dist/notifications/notification/notification.js +11 -8
  74. package/dist/notifications/root/notifications-root.d.ts +57 -1
  75. package/dist/notifications/root/notifications-root.js +35 -4
  76. package/dist/number-input/root/number-input-root.d.ts +21 -1
  77. package/dist/number-input/root/number-input-root.js +18 -5
  78. package/dist/object-inspector/index.d.ts +1 -0
  79. package/dist/object-inspector/index.js +1 -0
  80. package/dist/object-inspector/root/object-inspector-root.d.ts +183 -0
  81. package/dist/object-inspector/root/object-inspector-root.js +348 -0
  82. package/dist/option/root/option-root.d.ts +17 -0
  83. package/dist/option/root/option-root.js +4 -0
  84. package/dist/outline/root/outline-root.d.ts +16 -1
  85. package/dist/outline/root/outline-root.js +27 -9
  86. package/dist/qr-code/root/qr-code-root.d.ts +3 -1
  87. package/dist/qr-code/root/qr-code-root.js +11 -1
  88. package/dist/radio/group/radio-group.d.ts +1 -0
  89. package/dist/rating/root/rating-root.d.ts +3 -0
  90. package/dist/rating/root/rating-root.js +9 -0
  91. package/dist/scatter-chart/index.d.ts +1 -0
  92. package/dist/scatter-chart/index.js +1 -0
  93. package/dist/scatter-chart/root/scatter-chart-root.d.ts +144 -0
  94. package/dist/scatter-chart/root/scatter-chart-root.js +205 -0
  95. package/dist/select/root/select-root.d.ts +8 -0
  96. package/dist/select/root/select-root.js +8 -6
  97. package/dist/skip-link/index.d.ts +1 -0
  98. package/dist/skip-link/index.js +1 -0
  99. package/dist/skip-link/root/skip-link-root.d.ts +81 -0
  100. package/dist/skip-link/root/skip-link-root.js +58 -0
  101. package/dist/speed-dial/root/speed-dial-root.d.ts +1 -0
  102. package/dist/speed-dial/root/speed-dial-root.js +6 -2
  103. package/dist/steps/root/steps-root.d.ts +1 -0
  104. package/dist/steps/root/steps-root.js +5 -2
  105. package/dist/table/column/table-column.d.ts +79 -0
  106. package/dist/table/column/table-column.js +125 -5
  107. package/dist/table/root/table-root.d.ts +2 -0
  108. package/dist/table/root/table-root.js +12 -3
  109. package/dist/time-picker/root/time-picker-root.d.ts +9 -0
  110. package/dist/time-picker/root/time-picker-root.js +5 -0
  111. package/dist/toast/root/toast-root.d.ts +2 -0
  112. package/dist/tooltip/root/tooltip-root.js +1 -1
  113. package/dist/tour/root/tour-root.d.ts +14 -0
  114. package/dist/tour/root/tour-root.js +43 -18
  115. package/dist/tree/item/tree-item.d.ts +7 -4
  116. package/dist/tree/item/tree-item.js +4 -1
  117. package/dist/video/root/video-root.d.ts +23 -1
  118. package/dist/video/root/video-root.js +46 -23
  119. package/dist/virtual-list/root/virtual-list-root.d.ts +10 -6
  120. package/dist/virtual-list/root/virtual-list-root.js +10 -4
  121. package/package.json +5 -5
  122. package/types/jsx.d.ts +4536 -346
  123. package/types/svelte.d.ts +11 -0
  124. package/types/vue.d.ts +738 -39
@@ -12,8 +12,9 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
12
12
  *
13
13
  * **The role follows the shape.** A row that can be ticked is
14
14
  * `menuitemcheckbox` and carries `aria-checked`, because a reader is told "one
15
- * of these is on" rather than "here is a button". A row that opens a page keeps
16
- * `menuitem` and says `aria-haspopup`, since the page it opens is a menu.
15
+ * of these is on" rather than "here is a button"; `exclusive` makes it
16
+ * `menuitemradio`, for a set where only one can be. A row that opens a page
17
+ * keeps `menuitem` and says `aria-haspopup`, since the page it opens is a menu.
17
18
  *
18
19
  * Whether it is on the page showing is set by the menu as a property, never as
19
20
  * an attribute: an attribute written onto a consumer's element after it
@@ -64,6 +65,19 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
64
65
  * about who may name a file on somebody's disk rather than a gap here.
65
66
  */
66
67
  download: boolean;
68
+ /**
69
+ * Whether only one row of the set it belongs to can be on.
70
+ *
71
+ * Announcement and nothing else, the way `checkable` is: this element applies
72
+ * no choice and polices no group, so what it does is tell a reader which kind
73
+ * of set they are in. `menuitemcheckbox` says every row here is independent,
74
+ * which for a run of comparisons, or the values of a filter that compares
75
+ * against one, is the opposite of true.
76
+ *
77
+ * Meaningless without `checkable`, since a row that cannot be ticked is not
78
+ * one of a set of anything.
79
+ */
80
+ exclusive: boolean;
67
81
  /**
68
82
  * Where the row goes, which makes it a real link.
69
83
  *
@@ -98,13 +112,14 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
98
112
  */
99
113
  rel: string;
100
114
  /**
101
- * Whether this row's submenu is the page showing.
115
+ * Whether the page showing is this row's submenu, or one below it.
102
116
  *
103
117
  * Separate from `onPage`, and it has to be. A row that opened a page is not
104
118
  * itself on that page, so both are false for it, and rendering nothing on
105
119
  * that basis takes its `slot[name="submenu"]` with it: the rows inside have
106
- * nowhere to be assigned and are never laid out. The element that holds the
107
- * page has to keep the slot even when it shows no row of its own.
120
+ * nowhere to be assigned and are never laid out. Every row on the way down to
121
+ * the open page has to keep its slot, not only the one directly above it.
122
+ * Three levels deep, the topmost row holds the slot the middle one lives in.
108
123
  */
109
124
  showingSubmenu: boolean;
110
125
  /**
@@ -131,7 +146,7 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
131
146
  */
132
147
  get hasSubmenu(): boolean;
133
148
  /**
134
- * Whether anything is slotted into `submenu`. Read after first render.
149
+ * Whether anything is slotted into `submenu`.
135
150
  *
136
151
  * State rather than a getter over `children`. A getter is read during
137
152
  * `render()` with nothing to re-read it, so a submenu arriving after the
@@ -158,6 +173,16 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
158
173
  * exactly the thing that is.
159
174
  */
160
175
  private start;
176
+ /**
177
+ * Both answers, from the children, before each render.
178
+ *
179
+ * The slots exist only once a render has made them: read through them, the
180
+ * first render drew no caret and no leading box and a second render had to
181
+ * follow it. `slotchange` still reads them, for a child that arrives later.
182
+ * Not on the server, where `children` is undefined, nor while hydrating what
183
+ * one sent.
184
+ */
185
+ protected willUpdate(changed: PropertyValues): void;
161
186
  protected firstUpdated(changed: PropertyValues): void;
162
187
  /**
163
188
  * Re-reads whether anything is before the label.
@@ -16,6 +16,7 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
16
16
  this.checked = false;
17
17
  this.disabled = false;
18
18
  this.download = false;
19
+ this.exclusive = false;
19
20
  this.href = '';
20
21
  this.onPage = true;
21
22
  this.rel = '';
@@ -55,10 +56,18 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
55
56
  get hasSubmenu() {
56
57
  return this.submenu;
57
58
  }
59
+ willUpdate(changed) {
60
+ super.willUpdate(changed);
61
+ if (this.children === undefined || this.hydrating) {
62
+ return;
63
+ }
64
+ const named = (name) => [...this.children].some((child) => child.slot === name);
65
+ this.submenu = named('submenu');
66
+ this.start = named('start');
67
+ }
58
68
  firstUpdated(changed) {
59
69
  super.firstUpdated(changed);
60
- this.readSubmenu();
61
- this.readStart();
70
+ this.finishHydration();
62
71
  }
63
72
  focus() {
64
73
  this.renderRoot.querySelector('[part="item"]')?.focus();
@@ -87,8 +96,9 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
87
96
  }
88
97
  const submenu = this.hasSubmenu;
89
98
  const linked = this.href !== '' && !this.disabled;
90
- const role = this.checkable || this.checked ? 'menuitemcheckbox' : 'menuitem';
91
- const checkedState = this.checkable || this.checked ? (this.checked ? 'true' : 'false') : nothing;
99
+ const ticks = this.checkable || this.checked;
100
+ const role = ticks ? (this.exclusive ? 'menuitemradio' : 'menuitemcheckbox') : 'menuitem';
101
+ const checkedState = ticks ? (this.checked ? 'true' : 'false') : nothing;
92
102
  const inside = html `<span part="start" ?data-empty=${!this.start}
93
103
  ><slot name="start" @slotchange=${this.readStart}></slot
94
104
  ></span>
@@ -138,7 +148,7 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
138
148
  @click=${this.handleClick}
139
149
  @keydown=${this.handleKeydown}
140
150
  >${inside}</div>`}
141
- <slot name="submenu" @slotchange=${this.readSubmenu}></slot>`;
151
+ <div hidden><slot name="submenu" @slotchange=${this.readSubmenu}></slot></div>`;
142
152
  }
143
153
  };
144
154
  __decorate([
@@ -153,6 +163,9 @@ __decorate([
153
163
  __decorate([
154
164
  property({ type: Boolean })
155
165
  ], HelixHeadlessMenuItem.prototype, "download", void 0);
166
+ __decorate([
167
+ property({ reflect: true, type: Boolean })
168
+ ], HelixHeadlessMenuItem.prototype, "exclusive", void 0);
156
169
  __decorate([
157
170
  property()
158
171
  ], HelixHeadlessMenuItem.prototype, "href", void 0);
@@ -45,7 +45,11 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
45
45
  this.enter(chosen);
46
46
  return;
47
47
  }
48
- this.hide();
48
+ queueMicrotask(() => {
49
+ if (!event.defaultPrevented) {
50
+ this.hide();
51
+ }
52
+ });
49
53
  };
50
54
  this.handleFocusIn = (event) => {
51
55
  const item = this.items.find((row) => row.contains(event.target));
@@ -167,8 +171,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
167
171
  this.toggleState('mark', rows.some((row) => row.checkable || row.checked || this.drilldown.hasPage(row)));
168
172
  }
169
173
  mark(item, onPage) {
174
+ const page = this.drilldown.page;
170
175
  item.onPage = onPage;
171
- item.showingSubmenu = item === this.drilldown.page;
176
+ item.showingSubmenu = page !== null && item.contains(page);
172
177
  for (const child of item.children) {
173
178
  const nested = child;
174
179
  if (nested.isMenuItem === true && child.slot === 'submenu') {
@@ -0,0 +1,129 @@
1
+ import { HelixElement } from '@fusion.dev/helix-core';
2
+ import { type PropertyValues, type TemplateResult } from 'lit';
3
+ import type { HelixHeadlessNotification } from '../notification/notification.js';
4
+ declare const HelixHeadlessNotificationGroup_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & typeof HelixElement;
5
+ /**
6
+ * @summary A named set of notifications, which can be collapsed to the first of
7
+ * them.
8
+ *
9
+ * @description
10
+ * **What groups with what is the consumer's, and this never guesses.** A feed
11
+ * can be cut by day, by the thing being talked about, by the person doing the
12
+ * talking, or by nothing at all, and which of those is right is a question about
13
+ * the product. So a group is written by hand around the rows that belong
14
+ * together and told what to call itself, exactly as `FilterField` is told what
15
+ * its operators are.
16
+ *
17
+ * **Collapsed shows the first row and assigns the rest nowhere.** Not `hidden`,
18
+ * not `inert`, not a stylesheet: the slot is assigned imperatively, so a
19
+ * collapsed group's later rows are absent from the layout and from the
20
+ * accessibility tree with no CSS at all, which is what the headless tier claims
21
+ * for itself. `Notifications` already assigns its list this way and this borrows
22
+ * the mechanism.
23
+ *
24
+ * The count is separate from how many rows are written, because it usually is: a
25
+ * group saying five with one row under it and a link to the rest is the common
26
+ * shape, and computing the number from the children would make that unsayable.
27
+ * Left off, it is the number of rows.
28
+ *
29
+ * @since 0.24.0
30
+ * @status experimental
31
+ * @category feedback
32
+ * @aliases NotificationSection, FeedGroup
33
+ * @documentation https://helix.fusion.dev/components/notifications
34
+ *
35
+ * @slot - The notifications in this group.
36
+ * @slot action - Beside the count, for a link to the rest of them.
37
+ * @slot label - Replaces the group's name, for a heading that needs a mark in it.
38
+ *
39
+ * @event {HelixBeforeCloseEvent} helix-before-close - Fired before the group folds, and cancelable.
40
+ * @event {HelixBeforeOpenEvent} helix-before-open - Fired before the group unfolds. Cancelable: refusing it takes the decision and nothing moves.
41
+ *
42
+ * @csspart action - The trailing region of the header.
43
+ * @csspart count - How many are in the group.
44
+ * @csspart header - The label, the count and the action together.
45
+ * @csspart items - The region holding the notifications.
46
+ * @csspart label - What the group is called.
47
+ * @csspart trigger - The control that collapses and expands, where collapsible.
48
+ */
49
+ export declare class HelixHeadlessNotificationGroup extends HelixHeadlessNotificationGroup_base {
50
+ /**
51
+ * Manual, which is what lets a fold remove rows rather than hide them.
52
+ *
53
+ * Under automatic assignment every unnamed child lands in the default slot and
54
+ * the only ways to fold are `hidden`, `inert` or a stylesheet: the first two
55
+ * leave the rows in the layout or in the tree, and the third puts the state
56
+ * somewhere a headless consumer has to re-implement. Assigning by hand, a
57
+ * folded group's later rows are assigned nowhere and render nothing at all.
58
+ *
59
+ * `Notifications` takes the same option for a neighboring reason, and its
60
+ * note records the other half of it: the rows are ordinary children a consumer
61
+ * writes with no `slot`, and wrapping them any other way means editing their
62
+ * markup.
63
+ */
64
+ static shadowRootOptions: ShadowRootInit;
65
+ /**
66
+ * Whether the group can be collapsed to its first row.
67
+ *
68
+ * Off by default, because a group is a heading before it is a control: a feed
69
+ * cut by day wants its days named and rarely wants them folded, and a control
70
+ * that does nothing anybody asked for is still a tab stop.
71
+ */
72
+ collapsible: boolean;
73
+ /**
74
+ * How many notifications this group stands for.
75
+ *
76
+ * **Not the number of rows, and that is the point.** A group saying five with
77
+ * one row under it and a link to the rest is the ordinary shape of a feed that
78
+ * has been trimmed, and deriving this from the children would make it
79
+ * unsayable. Left off, it is the number of rows.
80
+ */
81
+ count?: number;
82
+ /**
83
+ * What the group is called: a day, a subject, a person.
84
+ *
85
+ * The slot's fallback rather than its replacement, so a consumer who wants a
86
+ * mark beside the name slots `label` and this goes unused.
87
+ */
88
+ label: string;
89
+ /**
90
+ * What this is, for the feed above it to recognize without importing it.
91
+ *
92
+ * The same seam `Notification` uses: a root reads a child's markers rather
93
+ * than its constructor, so the two tiers never have to agree about which class
94
+ * a consumer happened to instantiate.
95
+ */
96
+ readonly isNotificationGroup = true;
97
+ /** The notifications written into this group, in the order they were given. */
98
+ get items(): HelixHeadlessNotification[];
99
+ /** How many the group says it stands for, which is the count or the rows. */
100
+ get total(): number;
101
+ protected willUpdate(changed: PropertyValues): void;
102
+ protected firstUpdated(changed: PropertyValues): void;
103
+ protected updated(changed: PropertyValues): void;
104
+ /**
105
+ * Puts the rows the group is showing into the slot, and the rest nowhere.
106
+ *
107
+ * **Assignment rather than hiding, which is what makes this work with no
108
+ * stylesheet.** A row that is not assigned to a slot renders nothing at all:
109
+ * it is out of the layout, out of the tab order and out of the accessibility
110
+ * tree, in both tiers, without a rule anywhere. `hidden` would do the first
111
+ * three and `inert` only the last two, and either would put the collapse in a
112
+ * place a headless consumer has to re-implement.
113
+ *
114
+ * Collapsed keeps the first row rather than none, because a group folded to
115
+ * nothing is a heading with a number after it and no reason to believe there
116
+ * is anything behind it. The one row is the evidence.
117
+ */
118
+ private assign;
119
+ private readonly handleToggle;
120
+ /** How many rows the collapse is hiding, for the control to say so. */
121
+ private get folded();
122
+ protected render(): TemplateResult;
123
+ }
124
+ declare global {
125
+ interface HTMLElementTagNameMap {
126
+ 'helix-headless-notification-group': HelixHeadlessNotificationGroup;
127
+ }
128
+ }
129
+ export {};
@@ -0,0 +1,106 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { customElement, HelixElement, hostAria, MixinControllerOpen } from '@fusion.dev/helix-core';
8
+ import { html, nothing } from 'lit';
9
+ import { property } from 'lit/decorators.js';
10
+ const TRIGGER_ID = 'trigger';
11
+ const ITEMS_ID = 'items';
12
+ let HelixHeadlessNotificationGroup = class HelixHeadlessNotificationGroup extends MixinControllerOpen(HelixElement) {
13
+ constructor() {
14
+ super(...arguments);
15
+ this.collapsible = false;
16
+ this.label = '';
17
+ this.isNotificationGroup = true;
18
+ this.handleToggle = () => {
19
+ this.toggleOpen();
20
+ };
21
+ }
22
+ static { this.shadowRootOptions = {
23
+ ...HelixElement.shadowRootOptions,
24
+ slotAssignment: 'manual',
25
+ }; }
26
+ get items() {
27
+ return this.knownChildren.filter((child) => child.isNotification === true);
28
+ }
29
+ get total() {
30
+ return this.count ?? this.items.length;
31
+ }
32
+ willUpdate(changed) {
33
+ super.willUpdate(changed);
34
+ hostAria(this.internals, this.label === '' ? { role: 'group' } : { label: this.label, role: 'group' });
35
+ }
36
+ firstUpdated(changed) {
37
+ super.firstUpdated(changed);
38
+ this.finishHydration();
39
+ }
40
+ updated(changed) {
41
+ super.updated(changed);
42
+ this.assign();
43
+ }
44
+ assign() {
45
+ const slot = this.renderRoot?.querySelector?.('slot:not([name])');
46
+ if (!slot?.assign) {
47
+ return;
48
+ }
49
+ const items = this.items;
50
+ const showing = this.collapsible && !this.open ? items.slice(0, 1) : items;
51
+ if (this.assignsByName) {
52
+ for (const item of items) {
53
+ this.route(item, showing.includes(item) ? undefined : 'folded');
54
+ }
55
+ return;
56
+ }
57
+ slot.assign(...showing);
58
+ const action = this.renderRoot?.querySelector?.('slot[name="action"]');
59
+ action?.assign?.(...[...this.children].filter((child) => child.getAttribute('slot') === 'action'));
60
+ const label = this.renderRoot?.querySelector?.('slot[name="label"]');
61
+ label?.assign?.(...[...this.children].filter((child) => child.getAttribute('slot') === 'label'));
62
+ }
63
+ get folded() {
64
+ return Math.max(this.items.length - 1, 0);
65
+ }
66
+ render() {
67
+ const collapsed = this.collapsible && !this.open;
68
+ return html `<div part="header">
69
+ ${this.collapsible
70
+ ? html `<button
71
+ id=${TRIGGER_ID}
72
+ aria-controls=${ITEMS_ID}
73
+ aria-expanded=${this.open ? 'true' : 'false'}
74
+ part="trigger"
75
+ type="button"
76
+ @click=${this.handleToggle}
77
+ >
78
+ <span part="label"><slot name="label">${this.label}</slot></span>
79
+ <span part="count">${this.total}</span>
80
+ </button>`
81
+ : html `<span part="label"><slot name="label">${this.label}</slot></span>
82
+ <span part="count">${this.total}</span>`}
83
+ <span part="action"><slot name="action"></slot></span>
84
+ </div>
85
+ <div
86
+ id=${ITEMS_ID}
87
+ part="items"
88
+ aria-label=${collapsed && this.folded > 0 ? `${this.folded} more` : nothing}
89
+ >
90
+ <slot></slot>
91
+ </div>`;
92
+ }
93
+ };
94
+ __decorate([
95
+ property({ reflect: true, type: Boolean })
96
+ ], HelixHeadlessNotificationGroup.prototype, "collapsible", void 0);
97
+ __decorate([
98
+ property({ type: Number })
99
+ ], HelixHeadlessNotificationGroup.prototype, "count", void 0);
100
+ __decorate([
101
+ property()
102
+ ], HelixHeadlessNotificationGroup.prototype, "label", void 0);
103
+ HelixHeadlessNotificationGroup = __decorate([
104
+ customElement('helix-headless-notification-group')
105
+ ], HelixHeadlessNotificationGroup);
106
+ export { HelixHeadlessNotificationGroup };
@@ -1,2 +1,3 @@
1
+ export { HelixHeadlessNotificationGroup } from './group/notification-group.js';
1
2
  export { HelixHeadlessNotification } from './notification/notification.js';
2
3
  export { HelixHeadlessNotifications } from './root/notifications-root.js';
@@ -1,2 +1,3 @@
1
+ export { HelixHeadlessNotificationGroup } from './group/notification-group.js';
1
2
  export { HelixHeadlessNotification } from './notification/notification.js';
2
3
  export { HelixHeadlessNotifications } from './root/notifications-root.js';
@@ -36,10 +36,14 @@ import '../../format-date/index.js';
36
36
  * @documentation https://helix.fusion.dev/components/notifications
37
37
  *
38
38
  * @slot - What happened, in the consumer's own markup.
39
- * @slot actions - What a reader can do about it.
39
+ * @slot actions - What a reader can do about it, under the words it is about. Drawn always
40
+ * rather than on hover, because an action is the point of the row and not chrome around it.
40
41
  * @slot caption - A second line qualifying it, quieter than the first. A bare
41
42
  * slot rather than a box, so an item without one pays nothing for it; style it
42
43
  * through `::slotted()`.
44
+ * @slot detail - A block belonging to the row rather than a line about it: a quoted
45
+ * comment, a status moving from one value to another, a preview. Unlike `caption` nothing
46
+ * here sets a size or an ink, because whatever fills it usually brings its own.
43
47
  * @slot media - An avatar or icon identifying who or what it came from.
44
48
  *
45
49
  * @event {HelixDismissEvent} helix-dismiss - Fired when a reader asks to remove it, and cancelable.
@@ -121,14 +125,30 @@ export declare class HelixHeadlessNotification extends HelixElement {
121
125
  /**
122
126
  * Says whether there is anything for the time to make way for.
123
127
  *
124
- * The trailing column holds the time and the controls in one place and shows
128
+ * The trailing column holds the time and the dismiss in one place and shows
125
129
  * one of them, so a row with nothing to do about it would hide its timestamp
126
- * on hover and put nothing there. `dismissible` this element knows on the
127
- * server; whether a consumer filled the actions slot it can only find out from
128
- * a DOM, so this runs after the render and the resting answer, no controls, is
129
- * the one the server writes.
130
+ * on hover and put nothing there.
131
+ *
132
+ * **`dismissible` alone, where this once also counted a filled actions slot.**
133
+ * An action is not chrome. A row offering "Respond" is a row whose whole point
134
+ * is that button, and one nobody sees until a pointer arrives is one most
135
+ * readers never find: on a coarse pointer it was drawn and on a desktop it was
136
+ * not, so the two disagreed about whether the row had a button at all. Actions
137
+ * now sit in the body under the words they are about, and the swap is left to
138
+ * the one control that really is chrome.
139
+ *
140
+ * This element knows `dismissible` on the server, so unlike the answer it
141
+ * replaces this one needs no DOM and is right from the first render.
130
142
  */
131
143
  private markControls;
144
+ /**
145
+ * Says whether a consumer put anything in the actions slot.
146
+ *
147
+ * Only so the stylesheet can space them from the words above: a row with no
148
+ * actions pays nothing for the room they would have taken. Answered from a
149
+ * DOM, so the resting answer is the one the server writes.
150
+ */
151
+ private markActions;
132
152
  /**
133
153
  * The control that removes the item, with the mark a consumer can replace.
134
154
  *
@@ -148,13 +168,19 @@ export declare class HelixHeadlessNotification extends HelixElement {
148
168
  * on its own viewBox, so it lands where the box says and takes the icon step
149
169
  * the row is already set in.
150
170
  *
151
- * **A trash and not the `x` the Tag draws, because the two do not mean the
152
- * same thing.** A tag's cross takes a token off a field the reader is still
153
- * filling in, and putting it back is retyping a word. A notification is the
154
- * only copy of something that happened, and this removes it from the feed for
155
- * good, so the mark is the one every other interface uses for that and the
156
- * ink is `danger`. It is only ever on screen while the row is hovered or
157
- * focused, so a feed of twenty is not a column of red.
171
+ * **A cross, and not the trash this used to draw.** The argument for the trash
172
+ * was that a notification is the only copy of something that happened, so
173
+ * removing it is destruction and should be marked as such. That reads the row
174
+ * as the event. It is not: the deploy still deployed and the build still
175
+ * failed, and the feed this sits in has `See everything` under it, which says
176
+ * plainly that the row is a view of a longer list. What dismissing ends is the
177
+ * notice.
178
+ *
179
+ * The component cannot know better than that either. `helix-dismiss` is a
180
+ * request the consumer answers, so whether anything is destroyed is theirs to
181
+ * decide, and a default that shouts danger asserts a permanence this element
182
+ * does not control. `--notifications-dismiss-color` is still there for a
183
+ * consumer whose control really does destroy something.
158
184
  */
159
185
  protected renderDismiss(): TemplateResult | typeof nothing;
160
186
  /**
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { customElement, HelixDismissEvent, HelixElement, HelixReadEvent, hostAria, requestChange, slotFilled, } from '@fusion.dev/helix-core';
8
- import { trash } from '@fusion.dev/helix-icons';
8
+ import { x } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
@@ -62,10 +62,14 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends HelixEle
62
62
  updated(changed) {
63
63
  super.updated(changed);
64
64
  this.markControls();
65
+ this.markActions();
65
66
  }
66
67
  markControls() {
68
+ this.toggleState('controls', this.dismissible);
69
+ }
70
+ markActions() {
67
71
  const slot = this.renderRoot?.querySelector('slot[name="actions"]');
68
- this.toggleState('controls', this.dismissible || slotFilled(slot));
72
+ this.toggleState('actions', slotFilled(slot));
69
73
  }
70
74
  renderDismiss() {
71
75
  if (!this.dismissible) {
@@ -83,9 +87,9 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends HelixEle
83
87
  data-glyph
84
88
  fill="currentColor"
85
89
  part="dismiss-mark"
86
- viewBox=${trash.viewBox}
90
+ viewBox=${x.viewBox}
87
91
  >
88
- ${unsafeSVG(trash.content)}
92
+ ${unsafeSVG(x.content)}
89
93
  </svg>
90
94
  </slot>
91
95
  </button>`;
@@ -105,13 +109,12 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends HelixEle
105
109
  <div part="body">
106
110
  <div part="label"><slot></slot></div>
107
111
  <slot name="caption"></slot>
112
+ <slot name="detail"></slot>
113
+ <span part="actions"><slot name="actions" @slotchange=${this.handleActions}></slot></span>
108
114
  </div>
109
115
  <div part="trailing">
110
116
  ${this.renderTime()}
111
- <div part="controls">
112
- <span part="actions"><slot name="actions" @slotchange=${this.handleActions}></slot></span>
113
- ${this.renderDismiss()}
114
- </div>
117
+ <div part="controls">${this.renderDismiss()}</div>
115
118
  </div>
116
119
  </div>`;
117
120
  }
@@ -1,5 +1,6 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
+ import type { HelixHeadlessNotificationGroup } from '../group/notification-group.js';
3
4
  import type { HelixHeadlessNotification } from '../notification/notification.js';
4
5
  declare const HelixHeadlessNotifications_base: (new (...args: any[]) => import("@fusion.dev/helix-core").CollectionElement) & typeof HelixElement;
5
6
  /**
@@ -33,6 +34,7 @@ declare const HelixHeadlessNotifications_base: (new (...args: any[]) => import("
33
34
  * @slot footer - Below the list, for a link to everything.
34
35
  * @slot header - Above the list, for a title and the mark-all control.
35
36
  *
37
+ * @event {HelixDismissEvent} helix-dismiss - A row asking to be removed, dispatched by the row and heard here because the feed is what a consumer holds. Cancelable: refusing it takes the decision and the row stays.
36
38
  * @event {HelixReadEvent} helix-read - Fired by an item, and by this element when everything is marked read. Cancelable.
37
39
  *
38
40
  * @csspart empty - Shown when there is nothing in the feed.
@@ -79,8 +81,62 @@ export declare class HelixHeadlessNotifications extends HelixHeadlessNotificatio
79
81
  * attribute showed a count for an item the reader had just opened.
80
82
  */
81
83
  get unread(): number;
82
- /** The notifications declared as children, in the order they are written. */
84
+ /**
85
+ * The notifications in the feed, in the order they are written.
86
+ *
87
+ * **Through a group as well as beside one**, because a feed may be cut into
88
+ * named piles and the rows inside them are still the feed's rows: `unread`
89
+ * counts them and `markEveryoneRead` moves them. A group that renders only its
90
+ * first row while folded is still holding the rest, and a badge that dropped
91
+ * to what was on screen would fall every time somebody folded something.
92
+ *
93
+ * One level, not a walk. A group inside a group is not a thing this family
94
+ * has, and a recursive read here would invent one.
95
+ *
96
+ * `?? []` is the server. Lit's element shim carries attributes and not a tree,
97
+ * so `children` is undefined there, and this getter is reached from `unread`
98
+ * during the first update.
99
+ */
83
100
  get items(): HelixHeadlessNotification[];
101
+ /**
102
+ * What the list slot is given: the rows and the groups, as written.
103
+ *
104
+ * Not `items`, which reaches through a group to the rows inside it. Assigning
105
+ * those would lift them out of the group that is arranging them and hand the
106
+ * feed a flat list again.
107
+ */
108
+ get rows(): Element[];
109
+ /**
110
+ * The groups written into the feed, in the order they are written.
111
+ *
112
+ * Only the ones that can fold answer to `foldEveryGroup`, but this is every
113
+ * group: a consumer counting their sections should not have to know which of
114
+ * them happen to be collapsible.
115
+ */
116
+ get groups(): HelixHeadlessNotificationGroup[];
117
+ /**
118
+ * Folds every group that can fold, and reports how many actually moved.
119
+ *
120
+ * **A command, not a second place the state lives.** A group owns whether it
121
+ * is folded; this asks all of them at once. The alternative, a property on the
122
+ * feed that groups read, gives two answers to one question the moment a reader
123
+ * unfolds one of them, and then neither is wrong and neither is right.
124
+ *
125
+ * **Through each group's own request**, so one refusal refuses one group: a
126
+ * `helix-before-open` or `helix-before-close` prevented on a group leaves that
127
+ * group where it was and the rest still move. `markEveryoneRead` takes the
128
+ * same route for the same reason.
129
+ *
130
+ * **And unlike that one, this announces nothing of its own.** Marking read is
131
+ * usually a round trip, so a feed-level `helix-read` lets a consumer refuse the
132
+ * whole operation before any of it starts. Folding is not: it is where the
133
+ * reader is looking, it reaches no server, and an event nobody can meaningfully
134
+ * refuse is a channel that exists to be ignored.
135
+ *
136
+ * A group that is not `collapsible` is left alone rather than counted as
137
+ * refusing, because it was never asked.
138
+ */
139
+ foldEveryGroup(folded?: boolean): number;
84
140
  /**
85
141
  * Marks every unread item read, and reports how many actually moved.
86
142
  *