@fusion.dev/helix-headless 0.2.0 → 0.4.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 (127) hide show
  1. package/custom-elements.json +234 -234
  2. package/dist/accordion/item/accordion-item.js +0 -108
  3. package/dist/accordion/root/accordion-root.js +0 -33
  4. package/dist/announcement/root/announcement-root.js +1 -76
  5. package/dist/app-header/root/app-header-root.js +0 -84
  6. package/dist/avatar/group/avatar-group.js +0 -77
  7. package/dist/avatar/root/avatar-root.js +0 -72
  8. package/dist/badge/root/badge-root.js +0 -67
  9. package/dist/bar-chart/root/bar-chart-root.js +3 -263
  10. package/dist/blockquote/root/blockquote-root.js +0 -46
  11. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
  12. package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
  13. package/dist/button/root/button-root.js +0 -215
  14. package/dist/button-group/root/button-group-root.js +0 -30
  15. package/dist/callout/root/callout-root.js +0 -52
  16. package/dist/card/root/card-root.js +0 -41
  17. package/dist/carousel/root/carousel-root.d.ts +2 -2
  18. package/dist/carousel/root/carousel-root.js +2 -362
  19. package/dist/chat/composer/chat-composer.js +0 -131
  20. package/dist/chat/message/chat-message.js +0 -137
  21. package/dist/chat/root/chat-root.js +0 -193
  22. package/dist/checkbox/root/checkbox-root.js +0 -113
  23. package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
  24. package/dist/code/root/code-root.js +0 -26
  25. package/dist/code-block/root/code-block-root.js +0 -100
  26. package/dist/color-input/root/color-input-root.js +0 -217
  27. package/dist/color-picker/color.js +0 -69
  28. package/dist/color-picker/root/color-picker-root.js +0 -272
  29. package/dist/combobox/root/combobox-root.js +0 -428
  30. package/dist/command/root/command-root.js +0 -177
  31. package/dist/copy-button/root/copy-button-root.js +0 -134
  32. package/dist/data-table/root/data-table-root.js +0 -336
  33. package/dist/data-table/root/order.js +0 -100
  34. package/dist/date-input/root/date-input-root.js +1 -398
  35. package/dist/date-picker/root/date-picker-root.js +0 -358
  36. package/dist/dialog/root/dialog-root.js +0 -189
  37. package/dist/divider/root/divider-root.js +0 -71
  38. package/dist/donut-chart/root/donut-chart-root.js +0 -214
  39. package/dist/drawer/root/drawer-root.js +0 -116
  40. package/dist/drilldown/item/drilldown-item.js +0 -82
  41. package/dist/drilldown/root/drilldown-root.js +0 -91
  42. package/dist/element-events.generated.js +0 -5
  43. package/dist/empty/root/empty-root.js +0 -35
  44. package/dist/file-input/root/file-input-root.js +0 -232
  45. package/dist/format-date/root/format-date-root.js +0 -95
  46. package/dist/heatmap/root/buckets.js +0 -57
  47. package/dist/heatmap/root/heatmap-root.js +0 -313
  48. package/dist/icon/root/icon-root.js +0 -48
  49. package/dist/icon-button/root/icon-button-root.js +0 -30
  50. package/dist/image/root/image-root.js +0 -174
  51. package/dist/index.js +0 -9
  52. package/dist/input/root/input-root.js +0 -228
  53. package/dist/kbd/root/kbd-root.js +0 -117
  54. package/dist/legend/root/legend-root.js +0 -73
  55. package/dist/line-chart/root/line-chart-root.js +0 -172
  56. package/dist/list/item/list-item.js +0 -92
  57. package/dist/list/root/list-root.js +0 -26
  58. package/dist/listbox/root/listbox-root.js +2 -429
  59. package/dist/loader/root/loader-root.js +0 -40
  60. package/dist/megamenu/root/megamenu-root.js +0 -284
  61. package/dist/menu/item/menu-item.js +1 -221
  62. package/dist/menu/root/menu-root.js +1 -171
  63. package/dist/meter/root/meter-root.js +1 -152
  64. package/dist/navigation/root/navigation-root.js +0 -56
  65. package/dist/notifications/notification/notification.js +0 -159
  66. package/dist/notifications/root/notifications-root.js +0 -148
  67. package/dist/number-input/root/number-input-root.js +1 -263
  68. package/dist/option/group/option-group.js +0 -26
  69. package/dist/option/root/option-root.js +0 -157
  70. package/dist/outline/root/outline-root.js +0 -292
  71. package/dist/pagination/root/pagination-root.js +0 -361
  72. package/dist/pagination/size/pagination-size.js +0 -104
  73. package/dist/pagination/summary/pagination-summary.js +0 -90
  74. package/dist/pin-input/root/pin-input-root.js +0 -276
  75. package/dist/popover/root/popover-root.js +0 -103
  76. package/dist/progress/root/progress-root.js +1 -133
  77. package/dist/qr-code/root/qr-code-root.js +0 -96
  78. package/dist/quote/root/quote-root.js +0 -25
  79. package/dist/radio/group/radio-group.js +0 -81
  80. package/dist/radio/root/radio-root.js +0 -85
  81. package/dist/rating/root/rating-root.js +0 -220
  82. package/dist/scroll-area/root/scroll-area-root.js +0 -100
  83. package/dist/select/root/select-root.js +2 -218
  84. package/dist/skeleton/root/skeleton-root.js +0 -20
  85. package/dist/slider/root/slider-root.js +1 -267
  86. package/dist/sparkline/root/sparkline-root.js +3 -164
  87. package/dist/speed-dial/item/speed-dial-item.js +0 -52
  88. package/dist/speed-dial/root/speed-dial-root.js +1 -116
  89. package/dist/splitter/root/splitter-root.js +0 -489
  90. package/dist/stat/group/stat-group.js +0 -18
  91. package/dist/stat/root/stat-root.js +0 -23
  92. package/dist/steps/root/steps-root.js +0 -62
  93. package/dist/steps/step/step.js +0 -131
  94. package/dist/switch/root/switch-root.js +0 -80
  95. package/dist/table/cell/table-cell.js +0 -23
  96. package/dist/table/column/table-column.js +0 -158
  97. package/dist/table/root/table-root.js +0 -82
  98. package/dist/table/row/table-row.js +0 -21
  99. package/dist/tabs/panel/tab-panel.js +0 -49
  100. package/dist/tabs/root/tabs-root.js +0 -98
  101. package/dist/tabs/tab/tab.js +0 -59
  102. package/dist/tag/root/tag-root.js +0 -31
  103. package/dist/textarea/root/textarea-root.js +0 -162
  104. package/dist/time-input/root/time-input-root.js +0 -283
  105. package/dist/time-picker/root/time-picker-root.js +0 -326
  106. package/dist/timeline/item/timeline-item.js +0 -66
  107. package/dist/timeline/root/timeline-root.js +0 -41
  108. package/dist/toast/item/toast-item.js +0 -165
  109. package/dist/toast/root/toast-root.js +0 -51
  110. package/dist/toggle/group/toggle-group.js +0 -150
  111. package/dist/toggle/root/toggle-root.js +0 -78
  112. package/dist/toolbar/root/toolbar-root.js +0 -116
  113. package/dist/tooltip/root/tooltip-root.js +0 -427
  114. package/dist/tour/root/tour-root.js +0 -130
  115. package/dist/tour/stop/tour-stop.js +1 -127
  116. package/dist/tree/item/tree-item.js +0 -198
  117. package/dist/tree/root/tree-root.js +0 -97
  118. package/dist/trend/root/trend-root.js +0 -53
  119. package/dist/video/root/video-root.d.ts +8 -12
  120. package/dist/video/root/video-root.js +5 -756
  121. package/dist/video-playlist/item/video-playlist-item.js +1 -90
  122. package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
  123. package/dist/video-playlist/root/video-playlist-root.js +0 -113
  124. package/dist/virtual-list/root/virtual-list-root.js +0 -105
  125. package/jsx.d.ts +1408 -2576
  126. package/package.json +6 -6
  127. package/vue.d.ts +1 -1
@@ -7,145 +7,39 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, svg } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary A measurement in a known range, and whether it is where you want it.
12
- *
13
- * @description
14
- * **Not `Progress`, and the difference is the whole component.** A progress bar
15
- * reports how far along a task is: it starts empty, ends full, and full is
16
- * always the good end. A meter reports a level in a range that is not going
17
- * anywhere, and which end is good is a fact about the thing being measured.
18
- * Disk usage, a battery, a score, a rate limit. So there is no `indeterminate`
19
- * here: a measurement whose value is unknown is not a measurement.
20
- *
21
- * **`low`, `high` and `optimum` are the reason it exists**, and they follow the
22
- * HTML specification's own rule. The three thresholds cut the range into a low
23
- * band, a middle and a high band, and `optimum` says which of the three is the
24
- * one to be in. The band the value falls in is then the good one, the one next
25
- * to it is fair, and the one two away is poor.
26
- *
27
- * **Without thresholds there are no bands, and this is where it parts company
28
- * with `<meter>`.** The platform defaults `optimum` to the midpoint and both
29
- * thresholds to the ends, so a bare `<meter value="20" max="100">` computes as
30
- * optimum and every browser paints it green. A gauge with nothing said about it
31
- * is not good news; it is a reading. Declared thresholds turn the color on.
32
- *
33
- * **And the bands are drawn as well as colored.** A tick sits at `low` and at
34
- * `high`, so the reading can be placed against them without seeing color at
35
- * all, which is the same call the carousel's current dot makes by lengthening
36
- * rather than only changing color.
37
- *
38
- * The properties below are written by this component and read by whoever draws
39
- * it. A consumer sets none of them: they carry a number this element computed
40
- * and CSS cannot, which is what lets a stylesheet on this tier draw the thing
41
- * without measuring anything itself.
42
- *
43
- * @since 0.1.0
44
- * @status stable
45
- * @category feedback
46
- * @aliases Gauge, Level, Capacity
47
- * @documentation https://helix.fusion.dev/components/meter
48
- *
49
- * @csspart groove - The unfilled circle, in `ring` and `arc`.
50
- * @csspart indicator - The part of it the value fills.
51
- * @csspart tick - A mark at `low` or at `high`.
52
- * @csspart track - The whole range, or the drawing in `ring` and `arc`.
53
- * @csspart value - The reading, under `show-value`.
54
- *
55
- * @cssproperty --meter-along - Where a bound sits on the arc, as a length along it.
56
- * @cssproperty --meter-at - Where a bound sits on a linear track, as a percentage.
57
- * @cssproperty --meter-fraction - The value as a fraction of the range, 0 to 1.
58
- * @cssproperty --meter-sweep - How much of the circle the arc covers, as a path length.
59
- */
60
10
  let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
61
11
  constructor() {
62
12
  super(...arguments);
63
- /**
64
- * Accessible name for the reading.
65
- */
66
13
  this.label = 'Meter';
67
- /**
68
- * Top of the range.
69
- *
70
- * A hundred rather than the platform's one, which is what `Progress` already
71
- * takes and what a consumer writing `value="72"` means. A range of zero to one
72
- * is a fraction, and a component that reads `value="72"` as seventy-two times
73
- * over its maximum is a trap that costs nothing to avoid.
74
- */
75
14
  this.max = 100;
76
- /**
77
- * Bottom of the range.
78
- */
79
15
  this.min = 0;
80
- /**
81
- * Whether the reading is shown beside the bar.
82
- */
83
16
  this.showValue = false;
84
- /**
85
- * Where the reading sits between `min` and `max`.
86
- *
87
- * Outside them, it clamps.
88
- */
89
17
  this.value = 0;
90
- /**
91
- * What the reading says, where a number is not the whole story.
92
- *
93
- * `value-text="18 GB of 32 GB"` is announced and shown instead of `56%`. The
94
- * same property `Progress` carries, for the same reason: a percentage is a
95
- * fair summary of a task and a poor one of a quantity.
96
- */
97
18
  this.valueText = '';
98
19
  }
99
- /** The range, clamped so `max` is never at or below `min`. */
100
20
  get span() {
101
21
  const min = Number.isFinite(this.min) ? this.min : 0;
102
22
  const max = Number.isFinite(this.max) && this.max > min ? this.max : min + 1;
103
23
  return { max, min };
104
24
  }
105
- /** The reading, pulled into the range. */
106
25
  get reading() {
107
26
  const { max, min } = this.span;
108
27
  return Math.min(max, Math.max(min, Number.isFinite(this.value) ? this.value : min));
109
28
  }
110
- /**
111
- * How much of the track is filled, from zero to one.
112
- *
113
- * Clamped, because a bar cannot draw past its own end. What the reading *says*
114
- * is not clamped: see `ratio`.
115
- */
116
29
  get fraction() {
117
30
  const { max, min } = this.span;
118
31
  return (this.reading - min) / (max - min);
119
32
  }
120
- /**
121
- * Where the value really sits, which is what the words report.
122
- *
123
- * **Unclamped, so a value past the end is not swallowed.** Clamped as well as
124
- * the bar is, 110 and 250 read as exactly full, and a meter that cannot say
125
- * "you are over" is worse than no meter for the one case anybody checks it in.
126
- *
127
- * The bar still stops at its end, because that is what an end is. The words
128
- * and the accessible reading carry the truth, and `data-beyond` is on the fill
129
- * so a consumer can say more about it.
130
- */
131
33
  get ratio() {
132
34
  const { max, min } = this.span;
133
35
  const raw = Number.isFinite(this.value) ? this.value : min;
134
36
  return (raw - min) / (max - min);
135
37
  }
136
- /** Whether the value is past an end, which the bar itself cannot show. */
137
38
  get beyond() {
138
39
  const { max, min } = this.span;
139
40
  const raw = Number.isFinite(this.value) ? this.value : min;
140
41
  return raw > max ? 'over' : raw < min ? 'under' : undefined;
141
42
  }
142
- /**
143
- * The thresholds, in order, or nothing where the consumer declared none.
144
- *
145
- * Both are clamped into the range and to each other, because a `low` above a
146
- * `high` describes no bands at all and the spec's own answer is to raise the
147
- * one that is out of order rather than to give up.
148
- */
149
43
  get thresholds() {
150
44
  const { max, min } = this.span;
151
45
  if (this.low === undefined && this.high === undefined) {
@@ -155,14 +49,6 @@ let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
155
49
  const high = Math.min(max, Math.max(low, this.high ?? max));
156
50
  return { high, low };
157
51
  }
158
- /**
159
- * Which band the reading is in, by the specification's rule.
160
- *
161
- * `optimum` names a point, and the band containing it is the good one. A
162
- * reading in that band is good, one in a band beside it is fair, and one two
163
- * bands away is poor. Undefined where nothing was declared, which is what
164
- * keeps an unbanded meter from being painted as good news.
165
- */
166
52
  get computedBand() {
167
53
  if (this.band) {
168
54
  return this.band;
@@ -177,47 +63,19 @@ let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
177
63
  const distance = Math.abs(which(this.reading) - which(optimum));
178
64
  return distance === 0 ? 'good' : distance === 1 ? 'fair' : 'poor';
179
65
  }
180
- /** What the reading says, which is the consumer's words or a percentage. */
181
66
  get text() {
182
67
  return this.valueText || `${Math.round(this.ratio * 100)}%`;
183
68
  }
184
69
  willUpdate(changed) {
185
70
  super.willUpdate(changed);
186
71
  const { max, min } = this.span;
187
- /* On the host through internals, so the reading is the element itself and
188
- an author's own `aria-label` beats it without anything being written onto
189
- markup their server never had. `meter` is the ARIA role for exactly this
190
- and is not `progressbar`: a progress bar promises it is going somewhere. */
191
72
  hostAria(this.internals, { label: this.label, role: 'meter' });
192
73
  this.internals.ariaValueMin = String(min);
193
74
  this.internals.ariaValueMax = String(max);
194
75
  this.internals.ariaValueNow = String(this.reading);
195
- /* The consumer's words where they gave any, and otherwise nothing at all
196
- *unless* the value is past an end. `ariaValueNow` has to sit inside the
197
- range, so it is the only place a reader can be told that 250 is not 100. */
198
76
  this.internals.ariaValueText =
199
77
  this.valueText || (this.beyond ? `${Math.round(this.ratio * 100)}%` : null);
200
78
  }
201
- /**
202
- * The same reading, drawn round.
203
- *
204
- * `pathLength="100"` is what makes every number here a percentage: the path is
205
- * declared a hundred units long, so nothing computes a circumference, a radius
206
- * or pi, and a consumer changing the thickness moves the circle that carries
207
- * it without this element recalculating anything. Progress does the same and
208
- * for the same reason.
209
- *
210
- * The sweep is what separates the two shapes. A ring is the whole circle and
211
- * an arc is three quarters of it, open at the bottom, which is where a gauge
212
- * has always had its gap: the two ends need somewhere to be that is not each
213
- * other, and the opening is where a reading of the value goes.
214
- *
215
- * Ticks are segments of the same circle rather than marks placed on it. One
216
- * unit of a hundred, offset to where the threshold falls, which needs no
217
- * trigonometry and stays correct at any radius or thickness. Placing them by
218
- * hand would mean sine and cosine in a template and a second definition of
219
- * where the arc starts.
220
- */
221
79
  renderRound() {
222
80
  const bounds = this.thresholds;
223
81
  const { max, min } = this.span;
@@ -225,16 +83,7 @@ let HelixHeadlessMeter = class HelixHeadlessMeter extends HelixElement {
225
83
  const along = (value) => ((value - min) / (max - min)) * sweep;
226
84
  return html `<svg part="track" viewBox="0 0 100 100" aria-hidden="true">
227
85
  <circle part="groove" pathLength="100" style="--meter-sweep: ${sweep}" />
228
- ${
229
- /* `svg` and not `html`, which is the bug the sparkline records one
230
- file over. A nested Lit template is parsed on its own, so a
231
- `<circle>` written in one has no `<svg>` around it and lands in the
232
- XHTML namespace: it takes the part, takes the style, inherits every
233
- rule in the stylesheet, and draws nothing, because geometry belongs
234
- to the namespace and not to the tag name. Both threshold ticks were
235
- invisible in `shape="ring"` and `shape="arc"`, which is the whole of
236
- what the round example shows. */
237
- bounds
86
+ ${bounds
238
87
  ? svg `<circle
239
88
  part="tick"
240
89
  pathLength="100"
@@ -7,90 +7,34 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, IndicatorController } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary A set of destinations, with a marker on the current one.
12
- *
13
- * @description
14
- * Renders a real `<nav>`, which is the landmark a screen-reader user jumps to.
15
- * It exists as its own component rather than as a region of `AppHeader` because
16
- * a header is not inherently navigation, and navigation is not only ever in a
17
- * header: a footer and a sidebar hold the same thing.
18
- *
19
- * **It renders no links.** The consumer brings them, marked with
20
- * `aria-current="page"` and styled by the `data-navigation-item` recipe, for the same
21
- * reason nothing else here renders an anchor: a router renders its own, and
22
- * cannot render it inside this shadow root.
23
- *
24
- * What this owns is the marker that travels between them, and that is the whole
25
- * reason it is a component. A marker that moves needs something that knows
26
- * where every item is, and reading `aria-current` is how it knows which: the
27
- * thing that makes the page announce correctly is the same thing that puts the
28
- * marker in the right place.
29
- *
30
- * @since 0.1.0
31
- * @status experimental
32
- * @category navigation
33
- * @aliases Nav, NavList, Tabs
34
- * @documentation https://helix.fusion.dev/components/navigation
35
- *
36
- * @slot - The destinations, as the consumer's own links.
37
- *
38
- * @csspart indicator - The marker that travels to the current destination.
39
- * @csspart nav - The navigation landmark.
40
- */
41
10
  let HelixHeadlessNavigation = class HelixHeadlessNavigation extends HelixElement {
42
11
  constructor() {
43
12
  super(...arguments);
44
- /* The controller carries the RTL sign and border correction: a distance
45
- measured from the physical left and applied from the inline-start edge
46
- travels the wrong way in a right-to-left document. */
47
13
  this.indicator = new IndicatorController(this, {
48
14
  active: () => this.items.find((item) => item.getAttribute('aria-current') === 'page'),
49
15
  indicator: () => this.renderRoot?.querySelector('[part="indicator"]'),
50
16
  surface: () => this.renderRoot?.querySelector('[part="nav"]'),
51
17
  });
52
- /**
53
- * Accessible name for the landmark.
54
- */
55
18
  this.label = 'Main';
56
19
  }
57
- /** The destinations the consumer slotted. */
58
20
  get items() {
59
21
  const slot = this.renderRoot.querySelector('slot');
60
22
  return (slot?.assignedElements() ?? []);
61
23
  }
62
- /** Re-measures when the links change, move, or hand over the current page. */
63
24
  watch() {
64
25
  this.observer?.disconnect();
65
26
  this.current?.disconnect();
66
- /* Nothing is written onto the items, and `data-navigation-item` must not be
67
- * written here: it would land after upgrade, so a server-rendered nav would
68
- * arrive unstyled and restyle itself a frame later, and a React consumer
69
- * who wrote the attribute themselves gets `data-navigation-item="true"`
70
- * from JSX, which overwriting reports as a hydration mismatch. The base
71
- * layer selects `helix-navigation a`, so the styling is true the moment the
72
- * markup is parsed, on the server and on the client. */
73
27
  this.indicator.place();
74
28
  this.observer = new ResizeObserver(() => this.indicator.place());
75
29
  this.observer.observe(this);
76
30
  for (const item of this.items) {
77
31
  this.observer.observe(item);
78
32
  }
79
- // Client-side routing changes `aria-current` in place rather than
80
- // reloading, so nothing else would tell the marker to move.
81
33
  this.current = new MutationObserver(() => this.indicator.place());
82
34
  for (const item of this.items) {
83
35
  this.current.observe(item, { attributeFilter: ['aria-current'] });
84
36
  }
85
37
  }
86
- /* Rebuilt on reconnection, because nothing else does it.
87
- *
88
- * `firstUpdated` runs once for the life of the element and
89
- * `disconnectedCallback` runs every time it moves, so a framework reordering
90
- * a list left this permanently dead: the observer gone, nothing to rebuild
91
- * it, and the component still rendering as though it were watching. Guarded
92
- * on `hasUpdated`, because the first connection happens before the first
93
- * render and there is nothing to watch yet. */
94
38
  connectedCallback() {
95
39
  super.connectedCallback();
96
40
  if (this.hasUpdated) {
@@ -9,110 +9,18 @@ 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';
12
- /* `lit/static-html.js`, because this template interpolates a tag name and the
13
- plain `html` cannot: it renders the placeholder as the element instead, which
14
- shifts every attribute after it by one. See `renderTime`. */
15
12
  import { html } from 'lit/static-html.js';
16
- /* Side-effect import, because this template interpolates the family's tag and a
17
- custom element registers on import. Both tiers import it, the relative path
18
- resolving to each tier's own element. */
19
13
  import '../../format-date/index.js';
20
- /**
21
- * @summary One thing that happened, which a reader may not have seen yet.
22
- *
23
- * @description
24
- * A notification is a toast that did not go away. `helix-toast` is deliberately
25
- * the opposite of this: a live region whose items carry a `duration` and whose
26
- * root never creates or destroys anything. What makes this its own family is the
27
- * state each item carries rather than the arrangement it sits in: read and
28
- * unread, a timestamp that has to stay correct, and an action a reader can take
29
- * later.
30
- *
31
- * **The row is a label, a caption under it, and one trailing column that holds
32
- * either the time or the controls.** A feed is scanned rather than read, so the
33
- * thing that happened gets the first line on its own and everything qualifying
34
- * it goes under. The time and the controls occupy the same place because a
35
- * reader wants exactly one of them at a time: the time while they are scanning,
36
- * and what they can do about it once they have stopped on a row.
37
- *
38
- * **The timestamp is `helix-format-date`**, which is where the reading, the
39
- * `<time datetime>` and the shared timer now live. It was implemented here, and
40
- * a component that formats a date is not a fact about notifications: Chat wanted
41
- * the same thing and had its own copy of half of it.
42
- *
43
- * **Read state is reported, not invented.** Marking something read is usually a
44
- * round trip, and a component that flips its own state and then hears the
45
- * request failed has told the reader something untrue. `helix-read` is
46
- * cancelable, and a `read` set by the consumer makes this element defer entirely.
47
- *
48
- * @since 0.1.0
49
- * @status experimental
50
- * @category feedback
51
- * @aliases Notification item, Inbox item
52
- * @documentation https://helix.fusion.dev/components/notifications
53
- *
54
- * @slot - What happened, in the consumer's own markup.
55
- * @slot actions - What a reader can do about it.
56
- * @slot caption - A second line qualifying it, quieter than the first. A bare
57
- * slot rather than a box, so an item without one pays nothing for it; style it
58
- * through `::slotted()`.
59
- * @slot media - An avatar or icon identifying who or what it came from.
60
- *
61
- * @event {HelixDismissEvent} helix-dismiss - Fired when a reader asks to remove it, and cancelable.
62
- * @event {HelixReadEvent} helix-read - Fired when the item is marked read or unread, and cancelable.
63
- *
64
- * @csspart actions - The trailing slot's box.
65
- * @csspart body - The label and the caption together.
66
- * @csspart controls - The actions and the dismiss, which replace the time.
67
- * @csspart dismiss - The control that removes it.
68
- * @csspart dismiss-mark - The glyph on that control, where none is slotted.
69
- * @csspart item - The whole notification.
70
- * @csspart label - What happened, on the first line.
71
- * @csspart media - The leading slot's box.
72
- * @csspart time - When it happened, read the way a person reads one.
73
- * @csspart trailing - The column holding the time and the controls.
74
- */
75
14
  let HelixHeadlessNotification = class HelixHeadlessNotification extends HelixElement {
76
15
  constructor() {
77
16
  super(...arguments);
78
- /**
79
- * When it happened, as anything `Date` parses.
80
- *
81
- * An ISO string is what an API sends and what `<time datetime>` takes, so it
82
- * is the shape that needs no translation at either end. Left unset the item
83
- * shows no timestamp rather than showing the epoch, which is what parsing an
84
- * empty string would give.
85
- */
86
17
  this.at = '';
87
- /**
88
- * State to open with, where nothing declared one.
89
- */
90
18
  this.defaultRead = false;
91
- /**
92
- * Whether a reader may remove it.
93
- */
94
19
  this.dismissible = false;
95
- /**
96
- * Accessible name for the dismiss control.
97
- */
98
20
  this.dismissLabel = 'Dismiss';
99
- /**
100
- * Locale for the timestamp.
101
- *
102
- * Empty is the runtime's own.
103
- */
104
21
  this.locale = '';
105
- /**
106
- * @internal Read by the list to identify its children across the light DOM,
107
- * which is cheaper than `instanceof` and is why this is public rather than
108
- * protected. Not a consumer's to read or set.
109
- */
110
22
  this.isNotification = true;
111
23
  this.readState = undefined;
112
- /* Content arriving from the light DOM changes no property here, so nothing
113
- would schedule the re-read. Bound to the slot rather than to the host,
114
- because `slotchange` bubbles and is not composed: it reaches the shadow root
115
- that owns the slot and stops there. */
116
24
  this.handleActions = () => {
117
25
  this.markControls();
118
26
  };
@@ -123,21 +31,6 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends HelixEle
123
31
  }
124
32
  };
125
33
  }
126
- /**
127
- * Whether the reader has seen it.
128
- *
129
- * Set it and this element defers: a reader's click fires `helix-read` and
130
- * changes nothing until a new value arrives. `default-read` seeds a state
131
- * this element then owns.
132
- *
133
- * **Not reflected.** Unread is a visual state, which is why the four mixins
134
- * mirror theirs, but the mirror may not be the property's own attribute:
135
- * `<helix-notification default-read>` comes back from a server without `read`
136
- * and would gain it on the first update, which React reports as a hydration
137
- * mismatch, and which a copy of the serialized element re-parses as a
138
- * declaration. `:state(read)` is what the stylesheet reads.
139
- * `hydration.dom.spec.ts` holds it.
140
- */
141
34
  get read() {
142
35
  return this.readState ?? this.defaultRead;
143
36
  }
@@ -146,13 +39,6 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends HelixEle
146
39
  this.readState = next;
147
40
  this.requestUpdate('read', previous);
148
41
  }
149
- /**
150
- * Asks to be marked read, and reports whether the request stood.
151
- *
152
- * Public because a consumer's own control is as likely to ask as this
153
- * element's own markup is, and because a list marking everything read has to
154
- * go through the same request rather than writing the property behind it.
155
- */
156
42
  markRead(next = true) {
157
43
  if (next === this.read) {
158
44
  return false;
@@ -171,48 +57,16 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends HelixEle
171
57
  willUpdate(changed) {
172
58
  super.willUpdate(changed);
173
59
  hostAria(this.internals, { role: 'listitem' });
174
- /* The mirror the stylesheet selects on, kept off the host's attributes for
175
- the reason on `read` itself. */
176
60
  this.toggleState('read', this.read);
177
61
  }
178
62
  updated(changed) {
179
63
  super.updated(changed);
180
64
  this.markControls();
181
65
  }
182
- /**
183
- * Says whether there is anything for the time to make way for.
184
- *
185
- * The trailing column holds the time and the controls in one place and shows
186
- * one of them, so a row with nothing to do about it would hide its timestamp
187
- * on hover and put nothing there. `dismissible` this element knows on the
188
- * server; whether a consumer filled the actions slot it can only find out from
189
- * a DOM, so this runs after the render and the resting answer, no controls, is
190
- * the one the server writes.
191
- */
192
66
  markControls() {
193
67
  const slot = this.renderRoot?.querySelector('slot[name="actions"]');
194
68
  this.toggleState('controls', this.dismissible || slotFilled(slot));
195
69
  }
196
- /**
197
- * The control that removes the item, with the mark a consumer can replace.
198
- *
199
- * **A method rather than markup inside `render`, and the slot moves with
200
- * it.** A fallback answers a consumer and a seam answers the tier above, which
201
- * are different people: the styled tier wants an icon here, and with the slot
202
- * written inline the only way to change it is to reimplement the whole
203
- * template. Moving only the fallback out is not enough: the slot is the thing
204
- * a tier redraws.
205
- *
206
- * **The mark is drawn rather than typed, which is what lets it align.** A
207
- * character such as `&times;` is placed by its font rather than by its box:
208
- * measured against the ghost button beside it at 8x, the cross carried 6
209
- * pixels of ink to the label's 9.5 and sat 1.5 pixels below its center, in two
210
- * boxes whose own centers agreed to a tenth of a pixel. No stylesheet can
211
- * correct that, because the offset is inside the glyph. An `<svg>` is centered
212
- * on its own viewBox, so it lands where the box says and takes the icon step
213
- * the row is already set in. It is the `x` the Tag's dismiss draws, so two
214
- * controls that mean "take this away" are one mark.
215
- */
216
70
  renderDismiss() {
217
71
  if (!this.dismissible) {
218
72
  return nothing;
@@ -236,19 +90,6 @@ let HelixHeadlessNotification = class HelixHeadlessNotification extends HelixEle
236
90
  </slot>
237
91
  </button>`;
238
92
  }
239
- /**
240
- * When it happened, delegated whole.
241
- *
242
- * The tag is interpolated rather than written, so each tier renders its own:
243
- * the base tier may not name a styled element, and a styled item inherits this
244
- * template without having a tag of its own to remind anybody.
245
- *
246
- * `exportparts` and no `part` of its own, which is the rule about a part
247
- * already named for its role: the child publishes `time`, the role here is
248
- * `time`, and naming both would leave `::part(time)` matching two elements.
249
- * What a consumer wants to reach is the `<time>`, so that is what the name
250
- * reaches.
251
- */
252
93
  renderTime() {
253
94
  const stamp = this.tag('format-date');
254
95
  return html `<${stamp}