@fusion.dev/helix-headless 0.1.0 → 0.3.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,12 +7,6 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, HelixShortcutEvent } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * Whether a node is something a reader types into.
12
- *
13
- * The input types are the ones that take text. A checkbox and a range are
14
- * `<input>` and are not typing, so a chord still reaches them.
15
- */
16
10
  const typing = (node) => {
17
11
  if (node instanceof HTMLTextAreaElement) {
18
12
  return true;
@@ -22,7 +16,6 @@ const typing = (node) => {
22
16
  }
23
17
  return node instanceof HTMLInputElement && !UNTYPED.has(node.type);
24
18
  };
25
- /** `<input>` types that hold no text, so a bare chord is not competing with one. */
26
19
  const UNTYPED = new Set([
27
20
  'button',
28
21
  'checkbox',
@@ -35,14 +28,6 @@ const UNTYPED = new Set([
35
28
  'reset',
36
29
  'submit',
37
30
  ]);
38
- /**
39
- * How each name in a chord is drawn, per platform.
40
- *
41
- * Apple keyboards label their modifiers with glyphs and print no separator
42
- * between them, so `⌘K` is the whole of it. Everywhere else the words are the
43
- * label and the plus is part of the convention rather than prose, which is why
44
- * it is the one separator this component supplies.
45
- */
46
31
  const APPLE = {
47
32
  alt: '⌥',
48
33
  backspace: '⌫',
@@ -81,109 +66,22 @@ const ELSEWHERE = {
81
66
  tab: 'Tab',
82
67
  up: '↑',
83
68
  };
84
- /** Whether the keyboard in front of the reader labels its modifiers with glyphs. */
85
69
  const onApple = () => typeof navigator !== 'undefined' && /mac|iphone|ipad|ipod/i.test(navigator.userAgent);
86
- /**
87
- * @summary A keyboard key or chord, set inline with text.
88
- *
89
- * @description
90
- * Renders a real `<kbd>`, so the passage still means "press this" to anything
91
- * reading the markup rather than only to a reader looking at it.
92
- *
93
- * **One key per element, with `keys` as the stated exception.** A *sequence* is
94
- * several of these with the separator written between them as ordinary text,
95
- * because that separator is prose: it is "then" in a sequence and "plus" in a
96
- * simultaneous press, and a component that inserted one would have to guess
97
- * which. `keys` cannot be guessing, since a chord written `mod+k` has said
98
- * which it is.
99
- *
100
- * **`keys` exists because the glyphs were being typed by hand and were wrong.**
101
- * A literal `⌘K` in a template tells a reader on Windows to press a key their
102
- * keyboard does not have, and nothing catches it, a string being a string.
103
- * Given `mod` this renders `⌘K` on Apple and `Ctrl+K` everywhere else.
104
- *
105
- * **`within` is what binds it, and it is absent by default.** Without it this
106
- * draws a key and listens for nothing, which is what every existing use of this
107
- * component wants. The two values are the two honest lifetimes:
108
- *
109
- * - `control` listens within the element's own parent, which is the case this
110
- * is mostly for: a hint sitting inside the control it belongs to, where the
111
- * shortcut should work while that control has focus and stop existing when
112
- * the control does.
113
- * - `document` listens on the document, for a chord that works from anywhere.
114
- *
115
- * **A hidden `kbd` is a dead shortcut, and that is the point rather than a
116
- * trap.** Binding here ties the key to its own advertisement, so a hint that is
117
- * not rendered is a shortcut that is not offered. A chord that must outlive its
118
- * label is a chord whose owner is not a label: give it to whatever owns the
119
- * action and let this element only describe it.
120
- *
121
- * Nothing binds until this upgrades, so a shortcut is not available in the
122
- * moments before hydration.
123
- *
124
- * @since 0.1.0
125
- * @status stable
126
- * @category typography
127
- * @aliases Key, Shortcut, Hotkey
128
- * @documentation https://helix.fusion.dev/components/kbd
129
- *
130
- * @slot - The key's label, where `keys` is not set.
131
- *
132
- * @event {HelixShortcutEvent} helix-shortcut - Fired when the chord is pressed, and cancelable.
133
- *
134
- * @csspart kbd - The key.
135
- */
136
70
  let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
137
71
  constructor() {
138
72
  super(...arguments);
139
- /**
140
- * The chord, written plainly: `mod+k`, `shift+?`, `/`.
141
- *
142
- * `mod` is the platform's own command modifier, which is the reason to write
143
- * a chord here rather than a glyph: it is the one token whose right answer
144
- * differs per reader.
145
- */
146
73
  this.keys = '';
147
74
  this.handleKeydown = (event) => {
148
75
  const press = event;
149
- /* Mid-composition is not a chord. An IME sends keystrokes that belong to
150
- the text being composed, and taking one is how a shortcut eats a
151
- character the reader was trying to type. */
152
76
  if (press.isComposing) {
153
77
  return;
154
78
  }
155
- /* Neither is a bare character typed into a field, which is the same fault
156
- the IME guard above already refuses: the reader meant the key to reach
157
- what they are writing in.
158
-
159
- Only a bare printable chord. `mod+k` is a shortcut a reader expects to
160
- work from inside a search field, and `escape` is how they leave it, so a
161
- guard over every chord would break the two conventions this is for. It is
162
- `/` and `?` that are ambiguous, being both the most conventional
163
- shortcuts on the web and characters somebody types.
164
-
165
- `composedPath()[0]` rather than `target`, which retargets to the host at
166
- document level and would need a list of every tag name that holds a field.
167
- The path's first entry is the real one, through open shadow roots and
168
- through `delegatesFocus`.
169
-
170
- **`document` only, because `control` has already answered the question.**
171
- That binding listens on `this.parentElement`, so it fires only with focus
172
- inside the control the badge sits in, and the guard would make a bare
173
- printable chord unreachable there by construction: the two are mutually
174
- exclusive. `.examples/events.html` is exactly that shape, a `keys="/"`
175
- slotted into a `helix-input` telling the reader to put the cursor in the
176
- field and press slash, and it would have shipped to the docs site doing
177
- nothing. What is ambiguous is `/` competing with every field on the page,
178
- which is the `document` case and the only one this refuses. */
179
79
  if (this.within === 'document' && this.printable && typing(press.composedPath()[0])) {
180
80
  return;
181
81
  }
182
82
  if (!this.matches_(press)) {
183
83
  return;
184
84
  }
185
- /* Announced before it is consumed, so a consumer can refuse it: the key
186
- goes on to whatever would have had it wherever this is prevented. */
187
85
  const shortcut = new HelixShortcutEvent({ keys: this.keys });
188
86
  this.dispatchEvent(shortcut);
189
87
  if (!shortcut.defaultPrevented) {
@@ -201,15 +99,11 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
201
99
  }
202
100
  updated(changed) {
203
101
  super.updated(changed);
204
- /* The target changes with `within` and the chord changes with `keys`, and
205
- the second matters as much as the first: a listener holding the old chord
206
- answers to a key the element no longer advertises. */
207
102
  if (changed.has('within') || changed.has('keys')) {
208
103
  this.deafen();
209
104
  this.listen();
210
105
  }
211
106
  }
212
- /** The chord as this reader's keyboard labels it. */
213
107
  get label() {
214
108
  const names = onApple() ? APPLE : ELSEWHERE;
215
109
  const parts = this.keys
@@ -234,13 +128,6 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
234
128
  this.listening?.removeEventListener('keydown', this.handleKeydown);
235
129
  this.listening = undefined;
236
130
  }
237
- /**
238
- * Whether this chord is one bare character, and so indistinguishable from
239
- * typing.
240
- *
241
- * A single part means no modifier, and one code point means a character
242
- * rather than a name: `/` and `?` are printable, `escape` and `enter` are not.
243
- */
244
131
  get printable() {
245
132
  const parts = this.keys
246
133
  .split('+')
@@ -248,7 +135,6 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
248
135
  .filter((part) => part !== '');
249
136
  return parts.length === 1 && [...(parts[0] ?? '')].length === 1;
250
137
  }
251
- /** Whether a press is this chord, modifiers and all. */
252
138
  matches_(press) {
253
139
  const parts = this.keys
254
140
  .split('+')
@@ -260,9 +146,6 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
260
146
  const wanted = parts[parts.length - 1] ?? '';
261
147
  const modifiers = new Set(parts.slice(0, -1));
262
148
  const mod = onApple() ? press.metaKey : press.ctrlKey;
263
- /* Every modifier is checked in both directions, so `mod+k` is not answered
264
- by `shift+mod+k`: a chord a reader did not press is a chord that should
265
- go to whatever did want it. */
266
149
  return (press.key.toLowerCase() === wanted &&
267
150
  (modifiers.has('mod') ? mod : true) &&
268
151
  (modifiers.has('shift') ? press.shiftKey : !press.shiftKey) &&
@@ -7,67 +7,11 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, SourceController } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /** How many colors the series palette has, and so where it starts over. */
11
10
  const PALETTE = 24;
12
- /**
13
- * @summary The names a chart draws, each beside the color it is drawn in.
14
- *
15
- * @description
16
- * A legend is a list, so it is a list: not part of a chart, and shared by
17
- * whichever of them needs one. It takes its entries from the same table the
18
- * chart reads, through `for`, so the two cannot disagree about how many series
19
- * there are or what they are called.
20
- *
21
- * `entries` picks which way to read that table, and the two ways are the two
22
- * shapes a chart comes in. `series` is one entry per data column, named by the
23
- * header row, which is a line or a bar chart. `categories` is one entry per
24
- * body row, named by its row header, which is a donut: its slices are the rows,
25
- * and it has no axis to write their names along.
26
- *
27
- * **The table is resolved on the client and there is no server pass.** `for` is
28
- * an id and resolving it wants a document, which the server render does not
29
- * have, so a legend ships empty and fills in on hydration. The table itself is
30
- * ordinary markup and is served whole, so nothing is missing from the page
31
- * before then, only from this list.
32
- *
33
- * The table is watched, because a legend that goes stale when its data changes
34
- * is a legend that has to be told, and nothing else in the page would know to
35
- * tell it.
36
- *
37
- * **Not hidden from a screen reader, though the mapping it draws is a color
38
- * one.** The argument for hiding it is real: the colors mean nothing to a
39
- * reader who cannot see them, and the names are already in the table. It stays
40
- * because the cost of being wrong runs one way. A duplicated list of three
41
- * words is noise; a consumer who took the table out of the accessibility tree
42
- * and then found the legend hidden too has a chart nobody can read, and neither
43
- * decision announces itself.
44
- *
45
- * @since 0.1.0
46
- * @status experimental
47
- * @category charts
48
- * @aliases Key, Series Key
49
- * @documentation https://helix.fusion.dev/components/legend
50
- *
51
- * @csspart entry - One name and its swatch.
52
- * @csspart list - The list itself.
53
- * @csspart name - One series name.
54
- * @csspart swatch - The color a series is drawn in.
55
- */
56
11
  let HelixHeadlessLegend = class HelixHeadlessLegend extends HelixElement {
57
12
  constructor() {
58
13
  super(...arguments);
59
- /**
60
- * The id of the table to take the names from.
61
- *
62
- * Resolved through `getRootNode()`, so a legend inside another component's
63
- * shadow root names a table in that root rather than in the document.
64
- *
65
- * **In React this arrives as a property and not as an attribute**, the way
66
- * `Tooltip.for` does, so it does not reflect and nothing styles on it.
67
- */
68
14
  this.for = '';
69
- /* One controller rather than the resolve-and-watch each chart would write
70
- out for itself, which drifts into two names for the same lookup. */
71
15
  this.source = new SourceController(this);
72
16
  }
73
17
  willUpdate(changed) {
@@ -75,15 +19,6 @@ let HelixHeadlessLegend = class HelixHeadlessLegend extends HelixElement {
75
19
  this.source.watch();
76
20
  }
77
21
  }
78
- /**
79
- * The names, in the order the chart will draw them.
80
- *
81
- * `protected` rather than private because the styled tier has to match a
82
- * per-series treatment against them, the way `BarChart` matches `outlined` and
83
- * `subtle` against the headings its table gives. A legend keys a chart, so the
84
- * two answer the same question about the same series, and the tier that paints
85
- * cannot do that without knowing which entry is which.
86
- */
87
22
  names() {
88
23
  const series = this.source.series;
89
24
  return this.entries === 'categories'
@@ -91,14 +26,6 @@ let HelixHeadlessLegend = class HelixHeadlessLegend extends HelixElement {
91
26
  : series.map((one) => one.name);
92
27
  }
93
28
  render() {
94
- /* The index rather than the color, because a color is a token and the
95
- headless tier holds none. The styled tier maps one to the other, and it
96
- starts over after twenty-four.
97
- *
98
- * The palette is eight hues in three tones, so a legend of nine repeats the
99
- * first hue in a brighter tone rather than repeating a color outright. It
100
- * is still a hue collision and a chart with nine series has a worse problem
101
- * than the palette does; twenty-four is where it stops being able to help. */
102
29
  return html `<ul part="list" role="list">
103
30
  ${this.names().map((name, index) => html `<li
104
31
  data-series=${(index % PALETTE) + 1}
@@ -8,110 +8,28 @@ import { customElement, drawTooltip, extent, HelixElement, linear, linePath, Mix
8
8
  import { html, nothing, svg } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import '../../tooltip/index.js';
11
- /** The drawing's own coordinates. */
12
11
  const WIDTH = 100;
13
12
  const HEIGHT = 100;
14
- /**
15
- * @summary A series over time as a line, read from a table.
16
- *
17
- * @description
18
- * The chart for a shape rather than for a quantity, which is what decides the
19
- * one thing a bar chart is not allowed to do.
20
- *
21
- * **It measures from its own range and not from zero, deliberately.** `BarChart`
22
- * refuses that switch because a bar's length *is* its value, so a baseline
23
- * anywhere else makes the lengths proportional to nothing. A line claims no
24
- * such thing: it says where the readings went, and forcing zero into the range
25
- * of a series that moves between 980 and 1020 flattens the entire shape into a
26
- * rule across the top of the box. The two rules are opposite because the two
27
- * charts answer opposite questions, and `zero` is here for the case where the
28
- * distance from nothing is the point.
29
- *
30
- * **One scale across every series**, the same as the bar chart, because two
31
- * series drawn to different scales are two charts sharing a picture.
32
- *
33
- * **The labels are HTML beside the drawing rather than `<text>` inside it**, for
34
- * the reason `BarChart` records: an `<svg>` that fills its box scales its type
35
- * with everything else, the same `font-size` arriving at 10px in a narrow chart
36
- * and 43px in a wide one, and there is no non-scaling-font to answer it the way
37
- * `vector-effect` answers strokes.
38
- *
39
- * The table is the accessible representation and this drawing is `aria-hidden`.
40
- * A reader who wants the numbers gets the numbers, not a described picture.
41
- *
42
- * @since 0.1.0
43
- * @status experimental
44
- * @category charts
45
- * @aliases Time Series, Trend Chart, Multi-line Chart
46
- * @documentation https://helix.fusion.dev/components/line-chart
47
- *
48
- * @csspart area - The wash under one line, where `area` is set.
49
- * @csspart band - The slot the pointer is resting in.
50
- * @csspart baseline - The rule at zero, where zero is inside the range.
51
- * @csspart gridline - One rule across the plot, under `gridlines`.
52
- * @csspart guide - A rule down the reading a pointer is resting on.
53
- * @csspart label - One reading's name.
54
- * @csspart labels - The row of reading names.
55
- * @csspart line - One series.
56
- * @csspart mark - The invisible point the tooltip is anchored to.
57
- * @csspart plot - The drawing.
58
- * @csspart svg - The drawing surface the lines and areas sit on.
59
- * @csspart terminus - The mark on one series' last reading.
60
- * @csspart tooltip - The bubble, forwarded from `Tooltip`.
61
- * @csspart tooltip-anchor - The `Tooltip` element itself.
62
- * @csspart tooltip-cell - One column's name and value inside the tooltip.
63
- * @csspart tooltip-label - The reading's own name inside the tooltip.
64
- * @csspart tooltip-name - A column's name inside the tooltip.
65
- * @csspart tooltip-value - A column's value inside the tooltip.
66
- */
67
13
  let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSource(HelixElement) {
68
14
  constructor() {
69
15
  super(...arguments);
70
- /**
71
- * Whether a wash is drawn under each line.
72
- *
73
- * Absent is lines alone, which is what suits more than one series: washes
74
- * stack and the lower ones are read through the upper. One series over a
75
- * ground it should feel like a quantity against is the case for this.
76
- */
77
16
  this.area = false;
78
- /**
79
- * Whether zero is forced into the range.
80
- *
81
- * Absent measures from the data's own extent, which is what a line is for.
82
- * Set it where the distance from nothing is the point rather than the shape,
83
- * and accept that a series which never approaches zero becomes a flat rule
84
- * near the top of the box.
85
- */
86
17
  this.zero = false;
87
18
  }
88
- /** Whether the levels are ruled, which is every value but `category`. */
89
19
  get rulesValue() {
90
20
  return this.gridlines !== undefined && this.gridlines !== 'category';
91
21
  }
92
- /** Whether each reading is ruled, which only two values ask for. */
93
22
  get rulesCategory() {
94
23
  return this.gridlines === 'both' || this.gridlines === 'category';
95
24
  }
96
- /** The domain every series is drawn against, which the tooltip also needs. */
97
25
  range(series) {
98
26
  const values = series.flatMap((one) => one.points.map((point) => point.value));
99
27
  const [low, high] = extent(values.length > 0 ? values : [0]);
100
28
  return this.zero ? [Math.min(0, low), Math.max(0, high)] : [low, high];
101
29
  }
102
- /**
103
- * Where each series ends, as percentages of the drawing's own box.
104
- *
105
- * Percentages rather than user units, for the reason the mark is a `<span>`
106
- * rather than a `<circle>`: `preserveAspectRatio="none"` stretches the
107
- * coordinate space, so a length in there means two different things depending
108
- * on how wide the consumer made the chart. The host's box stretches nothing.
109
- */
110
30
  ends(series, x, y) {
111
31
  return series.flatMap((one, depth) => {
112
32
  const last = one.points.at(-1);
113
- /* A single reading is a point rather than a line, and a mark on the end of
114
- something with no length is a mark on nothing. */
115
33
  if (!last || one.points.length < 2) {
116
34
  return [];
117
35
  }
@@ -132,40 +50,13 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
132
50
  }
133
51
  const domain = this.range(series);
134
52
  const y = linear(domain, [HEIGHT, 0]);
135
- /* **A reading sits in the middle of its own slot, not at the edge of the
136
- box**, which is what makes the names underneath line up with it.
137
- *
138
- * Spanning 0 to 100 is the obvious version and is wrong. The labels are a
139
- * grid of equal columns, the same row the bar chart uses, so each name is
140
- * centered in its share of the width while the readings sit at the
141
- * gridlines between those shares: measured, the first name centered at a
142
- * twelfth of the width with its point at zero. Every label is off by half
143
- * a slot and the error grows toward the middle.
144
- *
145
- * Insetting by half a slot also means a line chart and a bar chart drawn
146
- * from the same table stack, which is the arrangement anybody comparing a
147
- * total to its parts wants. The cost is that the line stops short of both
148
- * edges, and that is the smaller thing to give up. */
149
53
  const slot = WIDTH / readings.length;
150
54
  const x = linear([0, Math.max(readings.length - 1, 1)], [slot / 2, WIDTH - slot / 2]);
151
55
  return html `${this.renderPlot(series, x, y, domain)} ${this.renderLabels(readings)}`;
152
56
  }
153
- /**
154
- * The lines themselves, behind a seam the styled tier can move.
155
- *
156
- * `aria-hidden`, because the table it was read from is the readable version
157
- * and a screen reader hearing both hears the same numbers twice.
158
- */
159
57
  renderPlot(series, x, y, domain) {
160
- /* Drawn only where zero is inside the range. A rule at the foot of a chart
161
- whose lowest reading is 980 is not the zero line, it is the bottom of the
162
- box wearing the zero line's clothes, and a reader takes it for one. */
163
58
  const floor = domain[0] <= 0 && domain[1] >= 0 ? y(0) : undefined;
164
59
  const ends = this.terminus === 'none' ? [] : this.ends(series, x, y);
165
- /* Built here rather than in the sheet, because a stylesheet cannot count
166
- series and a mask needs one layer per hole. Each layer is transparent
167
- inside its own ring and opaque everywhere else, so intersecting them
168
- removes the union of the holes and nothing besides. */
169
60
  const holes = this.terminus === 'ring' && ends.length > 0
170
61
  ? ends
171
62
  .map((end) => `radial-gradient(circle at ${end.x}% ${end.y}%, #0000 calc(var(--_chart-terminus-inner) - 0.5px), #000 var(--_chart-terminus-inner))`)
@@ -194,22 +85,10 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
194
85
  ${this.renderTooltip()}
195
86
  </div>`;
196
87
  }
197
- /**
198
- * The grid, behind everything, or nothing at all.
199
- *
200
- * Drawn first so a series is never cut by a rule, and before the baseline so
201
- * that where the two coincide the baseline wins: zero is a fact about the
202
- * data and a gridline is a ruler, and the reader should see the fact.
203
- */
204
88
  renderGridlines(x, y, domain, count) {
205
89
  if (this.gridlines === undefined) {
206
90
  return nothing;
207
91
  }
208
- /* Through each reading rather than between them. A rule on the boundary
209
- divides two catchments, which is a fact about the pointer; a rule on the
210
- reading is the line a reader traces down to the label, which is what a
211
- category grid is for. The readings sit at slot centers, so the two are
212
- half a slot apart and only one of them lands under a point. */
213
92
  const down = this.rulesCategory
214
93
  ? Array.from({ length: count }, (_, index) => svg `<line part="gridline" x1=${x(index)} x2=${x(index)} y1="0" y2=${HEIGHT} />`)
215
94
  : [];
@@ -218,19 +97,6 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
218
97
  : [];
219
98
  return svg `${down}${across}`;
220
99
  }
221
- /**
222
- * The slot the pointer is resting in, drawn behind the drawing.
223
- *
224
- * A band and not a second rule. The guide says exactly where the reading is
225
- * and the band says which share of the width belongs to it, which is what a
226
- * reader is actually pointing at: the catchment is a slot wide, so a pointer
227
- * a few pixels off the line is still asking about that reading, and without
228
- * the band nothing on screen says so.
229
- *
230
- * The same slot arithmetic the pointer controller resolves with, so the lit
231
- * region and the region that answers are the same region by construction
232
- * rather than by two files agreeing.
233
- */
234
100
  renderBand(count) {
235
101
  const at = this.pointer.at;
236
102
  if (at < 0 || count === 0) {
@@ -239,17 +105,6 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
239
105
  const slot = WIDTH / count;
240
106
  return svg `<rect height=${HEIGHT} part="band" width=${slot} x=${at * slot} y="0" />`;
241
107
  }
242
- /**
243
- * A rule down the reading the tooltip is about.
244
- *
245
- * Without it the tooltip is a box of numbers floating over a drawing, and a
246
- * reader has to match its heading against the row of names to find out which
247
- * column it means. The rule is the whole of what connects the two, and it
248
- * matters most on a line, where there is no marker under the pointer to
249
- * stand in for one.
250
- *
251
- * Drawn before the series so a line is never cut by it.
252
- */
253
108
  renderGuide(x) {
254
109
  const at = this.pointer.at;
255
110
  if (at < 0) {
@@ -257,12 +112,6 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
257
112
  }
258
113
  return svg `<line part="guide" x1=${x(at)} x2=${x(at)} y1="0" y2=${HEIGHT} />`;
259
114
  }
260
- /**
261
- * The marks, as elements beside the drawing rather than shapes inside it.
262
- *
263
- * A seam of its own, so a tier or a consumer's subclass can draw them
264
- * differently without reimplementing the plot.
265
- */
266
115
  renderTermini(ends) {
267
116
  if (ends.length === 0) {
268
117
  return nothing;
@@ -273,7 +122,6 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
273
122
  style=${`--chart-series: var(--chart-series-${end.index}); --chart-terminus-x: ${end.x}%; --chart-terminus-y: ${end.y}%`}
274
123
  ></span>`)}`;
275
124
  }
276
- /** One series, its wash first so the line is drawn over its own fill. */
277
125
  renderSeries(one, depth, x, y) {
278
126
  const points = one.points.map((point, index) => [x(index), y(point.value)]);
279
127
  const path = linePath(points, this.curve ?? 'smooth');
@@ -281,10 +129,6 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
281
129
  return html `${nothing}`;
282
130
  }
283
131
  const index = seriesIndex(depth);
284
- /* Closed along the floor of the box and back, which is what makes it
285
- fillable: a `<path>` is filled between its own ends, so an open line
286
- fills the region between the stroke and the straight line joining its
287
- first point to its last. */
288
132
  const first = points[0];
289
133
  const last = points.at(-1);
290
134
  const wash = `${path}L${last[0]},${HEIGHT}L${first[0]},${HEIGHT}Z`;
@@ -303,33 +147,17 @@ let HelixHeadlessLineChart = class HelixHeadlessLineChart extends MixinChartSour
303
147
  style="--chart-series: var(--chart-series-${index})"
304
148
  />`;
305
149
  }
306
- /**
307
- * The reading names, as HTML beside the drawing rather than inside it.
308
- *
309
- * Inside, they would scale with the box, for the reason the class doc gives.
310
- */
311
150
  renderLabels(readings) {
312
151
  return html `<div aria-hidden="true" part="labels">
313
152
  ${readings.map((one) => html `<span part="label">${one}</span>`)}
314
153
  </div>`;
315
154
  }
316
- /**
317
- * What a pointer resting on a reading shows.
318
- *
319
- * A seam over `drawTooltip`, which holds the markup every chart shares. The
320
- * usual reason to override is arrangement: every cell of the row is already
321
- * there, including the columns nothing was plotted from.
322
- */
323
155
  renderTooltip() {
324
156
  const at = this.pointer.at;
325
157
  const series = this.series;
326
158
  if (at < 0 || series.length === 0) {
327
159
  return drawTooltip(this.sourceRows, at, this.tag('tooltip'));
328
160
  }
329
- /* Above the topmost series at that reading, so the box never covers the
330
- line it is describing. `y` counts down in the drawing, so the smallest is
331
- the highest, and no readings at all would make `Math.min` return
332
- Infinity. */
333
161
  const domain = this.range(series);
334
162
  const scale = linear(domain, [HEIGHT, 0]);
335
163
  const tops = series