@fusion.dev/helix-headless 0.184.0 → 0.186.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 (48) hide show
  1. package/custom-elements.json +3796 -1105
  2. package/dist/accordion/item/accordion-item.js +1 -1
  3. package/dist/button-group/root/button-group-root.js +1 -1
  4. package/dist/canvas/root/canvas-root.js +1 -1
  5. package/dist/data-table/root/data-table-root.d.ts +25 -6
  6. package/dist/data-table/root/data-table-root.js +21 -9
  7. package/dist/dialog/root/dialog-root.js +1 -1
  8. package/dist/drawer/root/drawer-root.js +1 -1
  9. package/dist/drilldown/item/drilldown-item.js +1 -1
  10. package/dist/element-events.generated.js +7 -0
  11. package/dist/graph/root/graph-root.d.ts +6 -4
  12. package/dist/graph/root/graph-root.js +3 -3
  13. package/dist/heatmap/root/heatmap-root.d.ts +15 -15
  14. package/dist/icon/all/index.js +0 -3
  15. package/dist/icon/root/icon-root.d.ts +11 -18
  16. package/dist/icon/root/icon-root.js +4 -16
  17. package/dist/icon/set.js +2 -16
  18. package/dist/index.d.ts +2 -1
  19. package/dist/index.js +2 -1
  20. package/dist/isometric-heatmap/index.d.ts +1 -0
  21. package/dist/isometric-heatmap/index.js +1 -0
  22. package/dist/isometric-heatmap/projection.d.ts +70 -0
  23. package/dist/isometric-heatmap/projection.js +49 -0
  24. package/dist/isometric-heatmap/root/isometric-heatmap-root.d.ts +118 -0
  25. package/dist/isometric-heatmap/root/isometric-heatmap-root.js +127 -0
  26. package/dist/lightbox/root/lightbox-root.js +1 -1
  27. package/dist/list/root/list-root.js +1 -1
  28. package/dist/map/root/map-root.js +1 -1
  29. package/dist/menu/item/menu-item.js +1 -1
  30. package/dist/outline/root/outline-root.js +2 -2
  31. package/dist/popover/root/popover-root.js +1 -1
  32. package/dist/table/root/table-root.d.ts +4 -6
  33. package/dist/table/root/table-root.js +3 -29
  34. package/dist/table/tracks.d.ts +20 -0
  35. package/dist/table/tracks.js +28 -0
  36. package/dist/tabs/root/tabs-root.js +4 -5
  37. package/dist/tabs/tab/tab.js +2 -3
  38. package/dist/time-picker/root/time-picker-root.js +1 -1
  39. package/dist/toggle/root/toggle-root.js +1 -1
  40. package/dist/tour/root/tour-root.js +1 -1
  41. package/dist/tree/root/tree-root.js +1 -1
  42. package/dist/video/root/video-root.js +1 -1
  43. package/lint.json +1 -1
  44. package/package.json +6 -6
  45. package/types/jsx.d.ts +1340 -74
  46. package/types/svelte.d.ts +1 -0
  47. package/types/vue.d.ts +279 -13
  48. package/types/words.d.ts +69 -0
@@ -0,0 +1,118 @@
1
+ import { nothing, type TemplateResult } from 'lit';
2
+ import '../../tooltip/index.js';
3
+ import { HelixHeadlessHeatmap } from '../../heatmap/root/heatmap-root.js';
4
+ /**
5
+ * @summary A heatmap drawn as a relief, where each reading stands as tall as its value.
6
+ *
7
+ * @description
8
+ * The same strip as `Heatmap`, seen from above at an angle. Each reading is a
9
+ * prism: its top face takes the cell's tone, and its height repeats the value,
10
+ * measured from zero against the highest reading in the table. `rows="7"`
11
+ * makes a contribution graph, with each week a column running down to the
12
+ * right.
13
+ *
14
+ * **The height says what the tone says.** Read the exact value from the tooltip
15
+ * or the table, and compare readings by tone. A tall reading at the front can
16
+ * hide a short one behind it, so the newest column stands at the front, where
17
+ * nothing covers it.
18
+ *
19
+ * **Everything else is `Heatmap`.** The table named by `for`, the tones and
20
+ * thresholds, the keys, the tooltip, the overview, `selection-mode`, and the
21
+ * events all work as they do there. The class extends the headless heatmap,
22
+ * and its own entry point carries the drawing, so a page using only `Heatmap`
23
+ * loads none of it.
24
+ *
25
+ * **`flat` lays every prism down** at no height, which leaves the tones on a
26
+ * plane at the same angle.
27
+ *
28
+ * **`gap` and `min-cell` set the smallest cell.** The drawing scales as a
29
+ * whole, so a wider box draws larger cells and the gap grows with them. A box
30
+ * too narrow for every column at `min-cell` draws the newest that fit.
31
+ *
32
+ * **The drawing is SVG and holds still.** It scales with its box through its
33
+ * `viewBox`, needs no WebGL, and has no motion to pause.
34
+ *
35
+ * **Keyboard.** The keys are `Heatmap`'s. Down moves to the next reading in a
36
+ * column, drawn down to the left, and Right to the same reading in the next
37
+ * column, drawn down to the right.
38
+ *
39
+ * | Key | What it does |
40
+ * |---|---|
41
+ * | Tab | Moves focus to the strip and shows the first cell. |
42
+ * | Down, Up | Moves to the next or the previous cell. |
43
+ * | Right, Left | Moves a column on or back, a week with `rows="7"`. On a right-to-left page, Left moves on. |
44
+ * | Home, End | Moves to the first or the last cell. |
45
+ * | Page Down, Page Up | Moves a month on or back where the rows carry dates, and four columns otherwise. |
46
+ * | Enter, Space | Presses the cell, which sends `helix-reading-press`, and selects it or lets it go under `selection-mode`. |
47
+ * | Shift with an arrow, Home, End, Page Up or Page Down | Selects every cell from the last one pressed to where the key went, under `selection-mode="multiple"`. |
48
+ * | Escape | Puts the cell's tooltip away. |
49
+ *
50
+ * @since 0.185.0
51
+ * @status experimental
52
+ * @category charts
53
+ * @aliases Isometric chart, 3D heatmap, Relief chart, Isometric contribution graph
54
+ * @documentation https://fusion.dev/helix/components/isometric-heatmap
55
+ *
56
+ * @csspart cell - One reading: a group holding its three faces.
57
+ * @csspart drawing - The SVG every prism is drawn in. It is mirrored on a right-to-left page.
58
+ * @csspart left-face - The face of a prism toward the next row, drawn at its lower left.
59
+ * @csspart right-face - The face of a prism toward the next column, drawn at its lower right.
60
+ * @csspart row - A column of prisms, which a grid reads as a row, under `selection-mode`.
61
+ * @csspart strip - The box the drawing fills, which takes focus to be read by key. It is named by the host's `aria-labelledby` or `aria-label`, then by `label`, then by the caption of the table.
62
+ * @csspart top-face - The top of a prism, which carries the reading's tone.
63
+ *
64
+ * @cssproperty --isometric-heatmap-span - How many cells the drawing spans across: its columns and its rows together. The component writes it on the strip, and a stylesheet that caps the cell size reads it.
65
+ */
66
+ export declare class HelixHeadlessIsometricHeatmap extends HelixHeadlessHeatmap {
67
+ /**
68
+ * The key this family's words are registered under, its styled tag:
69
+ * `registerWords('de', { 'helix-isometric-heatmap': { emptyText: 'Keine Werte' } })`.
70
+ */
71
+ static words: string;
72
+ protected static markRules: string[];
73
+ /**
74
+ * Draws every prism at no height, so the tones lie on a plane.
75
+ */
76
+ flat: boolean;
77
+ /**
78
+ * How tall the highest reading stands, in cells. Every other reading stands
79
+ * at its share of it.
80
+ */
81
+ relief: number;
82
+ /** The prisms drawn on the last render, which the tooltip is placed against. */
83
+ private shown;
84
+ /**
85
+ * How many readings fit, as whole columns, newest kept.
86
+ *
87
+ * The drawing runs at 30 degrees, so a cell takes `ACROSS` of its width, and
88
+ * the rows run across too: a strip `rows` deep starts that many cells to the
89
+ * left of its first column.
90
+ */
91
+ protected get room(): number;
92
+ /** The drawn readings as prisms, standing against the highest reading in the table. */
93
+ private drawnRelief;
94
+ protected render(): TemplateResult;
95
+ /**
96
+ * One reading as a prism. The sides come first, so the top face is painted
97
+ * over the edge they share with it.
98
+ *
99
+ * Under `selection-mode` the prism is a `gridcell` named by its row in the
100
+ * table and saying whether it is selected, as the flat strip's cell is.
101
+ */
102
+ private renderPrism;
103
+ /**
104
+ * The prisms as a grid's rows, one for each column of the strip, which is
105
+ * also the order they are painted in.
106
+ */
107
+ private renderPrismRows;
108
+ /**
109
+ * The bubble over the top face of the reading the pointer or the keys rest
110
+ * on, placed as a share of the drawing, which the strip's box matches.
111
+ */
112
+ protected renderTooltip(grid?: boolean): TemplateResult | typeof nothing;
113
+ }
114
+ declare global {
115
+ interface HTMLElementTagNameMap {
116
+ 'helix-headless-isometric-heatmap': HelixHeadlessIsometricHeatmap;
117
+ }
118
+ }
@@ -0,0 +1,127 @@
1
+ import { __decorate } from "tslib";
2
+ import { CHART_PLOT, commaList, customElement, drawTooltip, MIRROR_RULE, spokenReading, writtenList, writtenNumbers, } from '@fusion.dev/helix-core';
3
+ import { html, nothing, svg } from 'lit';
4
+ import { property } from 'lit/decorators.js';
5
+ import '../../tooltip/index.js';
6
+ import { fits, toneOf } from '../../heatmap/root/buckets.js';
7
+ import { HelixHeadlessHeatmap } from '../../heatmap/root/heatmap-root.js';
8
+ import { ACROSS, heightsOf, highestOf, reliefOf } from '../projection.js';
9
+ let HelixHeadlessIsometricHeatmap = class HelixHeadlessIsometricHeatmap extends HelixHeadlessHeatmap {
10
+ constructor() {
11
+ super(...arguments);
12
+ this.flat = false;
13
+ this.relief = 2;
14
+ this.shown = undefined;
15
+ }
16
+ static { this.words = 'helix-isometric-heatmap'; }
17
+ static { this.markRules = [MIRROR_RULE]; }
18
+ get room() {
19
+ const width = this.width;
20
+ if (width === undefined) {
21
+ return this.points.length;
22
+ }
23
+ const pitch = this.cellPx + this.gapPx;
24
+ const across = width / ACROSS - this.rowCount * pitch - this.gapPx;
25
+ return fits(this.points.length, Math.max(across, Number.MIN_VALUE), this.cellPx, this.gapPx, this.rowCount);
26
+ }
27
+ drawnRelief() {
28
+ const tallest = this.flat ? 0 : Math.max(0, this.relief || 0);
29
+ const highest = highestOf(this.points.map((point) => point.value));
30
+ const heights = heightsOf(this.drawn.map((point) => point.value), highest, tallest);
31
+ return reliefOf(heights, this.rowCount, this.cellPx / (this.cellPx + this.gapPx));
32
+ }
33
+ render() {
34
+ const bounds = writtenNumbers(this.thresholds);
35
+ const palette = writtenList(this.tones);
36
+ const cells = this.drawn;
37
+ const readable = cells.length > 0;
38
+ const grid = readable && this.selectionMode !== undefined;
39
+ const keys = this.selectionMode ? this.keysOf(cells) : [];
40
+ const chosen = new Set(this.selectionMode ? commaList(this.value) : []);
41
+ const relief = this.drawnRelief();
42
+ this.shown = relief;
43
+ const at = this.pointer.at;
44
+ const prisms = relief.prisms.map((prism) => this.renderPrism(prism.index, prism.top, prism.left, prism.right, at === prism.index, chosen.has(keys[prism.index] ?? ''), grid, toneOf(cells[prism.index] ?? { value: undefined }, bounds, palette)));
45
+ const { box } = relief;
46
+ const drawing = readable
47
+ ? html `<svg
48
+ aria-hidden=${grid ? nothing : 'true'}
49
+ data-mirror
50
+ part="drawing"
51
+ role=${grid ? 'none' : nothing}
52
+ viewBox="${box.x} ${box.y} ${box.width} ${box.height}"
53
+ xmlns="http://www.w3.org/2000/svg"
54
+ >${grid ? this.renderPrismRows(prisms) : prisms}</svg>`
55
+ : nothing;
56
+ return html `<div
57
+ aria-activedescendant=${grid && at >= 0 ? `cell-${at}` : nothing}
58
+ aria-describedby=${readable ? 'helix-chart-summary' : nothing}
59
+ aria-label=${this.named || this.stripLabel}
60
+ aria-multiselectable=${grid && this.selectionMode === 'multiple' ? 'true' : nothing}
61
+ part="strip plot"
62
+ id=${CHART_PLOT}
63
+ role=${grid ? 'grid' : readable ? 'application' : 'img'}
64
+ style="--isometric-heatmap-span: ${Math.ceil(cells.length / this.rowCount) + Math.min(this.rowCount, cells.length)}"
65
+ tabindex=${readable ? '0' : nothing}
66
+ @click=${this.onClick}
67
+ @keydown=${this.onStripKeydown}
68
+ @pointerover=${this.onPointerOver}
69
+ @pointerleave=${this.onPointerLeave}
70
+ @pointerup=${this.onPointerUp}
71
+ >${drawing}${this.renderTooltip(grid)}</div>
72
+ ${cells.length === 0 ? this.renderStatus() : nothing}
73
+ <div part="footer">
74
+ <span part="start"><slot name="start"></slot></span>
75
+ <span part="summary"><slot name="summary"></slot></span>
76
+ <span part="end"><slot name="end"></slot></span>
77
+ </div>${readable ? this.renderOverview(cells) : nothing}${this.status.spoken
78
+ ? html `<span part="selection" role="status">${this.status.said}</span>`
79
+ : nothing}${this.renderTable()}`;
80
+ }
81
+ renderPrism(index, top, left, right, active, picked, grid, tone) {
82
+ const row = grid ? this.sourceRows[this.points.length - this.drawn.length + index] : undefined;
83
+ const name = row ? spokenReading(row) : '';
84
+ return svg `<g
85
+ aria-label=${grid && name ? name : nothing}
86
+ aria-selected=${grid ? (picked ? 'true' : 'false') : nothing}
87
+ data-index=${index}
88
+ data-tone=${tone ?? nothing}
89
+ id=${grid ? `cell-${index}` : nothing}
90
+ part="cell${active ? ' cell-active' : ''}${picked ? ' cell-selected' : ''}"
91
+ role=${grid ? 'gridcell' : nothing}
92
+ style=${tone === undefined ? nothing : `--heatmap-tone: var(--heatmap-tone-${tone})`}
93
+ ><path d=${left} part="left-face"></path><path d=${right} part="right-face"></path><path d=${top} part="top-face"></path></g>`;
94
+ }
95
+ renderPrismRows(prisms) {
96
+ const size = this.rowCount === 1 ? prisms.length : this.rowCount;
97
+ const rows = [];
98
+ for (let start = 0; start < prisms.length; start += size) {
99
+ rows.push(svg `<g part="row" role="row">${prisms.slice(start, start + size)}</g>`);
100
+ }
101
+ return rows;
102
+ }
103
+ renderTooltip(grid = false) {
104
+ const at = this.pointer.at;
105
+ const prism = this.shown?.prisms[at];
106
+ const box = this.shown?.box;
107
+ const reading = grid ? undefined : this.pointer;
108
+ if (at < 0 || !prism || !box || box.width <= 0 || box.height <= 0) {
109
+ return drawTooltip(this.sourceRows, -1, this.tag('tooltip'), undefined, undefined, reading);
110
+ }
111
+ return drawTooltip(this.sourceRows, this.points.length - this.drawn.length + at, this.tag('tooltip'), {
112
+ placement: 'top',
113
+ x: `${((prism.peak.x - box.x) / box.width) * 100}%`,
114
+ y: `${((prism.peak.y - box.y) / box.height) * 100}%`,
115
+ }, undefined, reading);
116
+ }
117
+ };
118
+ __decorate([
119
+ property({ type: Boolean })
120
+ ], HelixHeadlessIsometricHeatmap.prototype, "flat", void 0);
121
+ __decorate([
122
+ property({ type: Number })
123
+ ], HelixHeadlessIsometricHeatmap.prototype, "relief", void 0);
124
+ HelixHeadlessIsometricHeatmap = __decorate([
125
+ customElement('helix-headless-isometric-heatmap')
126
+ ], HelixHeadlessIsometricHeatmap);
127
+ export { HelixHeadlessIsometricHeatmap };
@@ -356,7 +356,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
356
356
  return;
357
357
  }
358
358
  if (this.open && !dialog.open) {
359
- auditName(this, () => null, 'Give it a `label`, such as "Project photos".', () => this.label !== '');
359
+ auditName(this, () => null, 'lightbox', () => this.label !== '');
360
360
  dialog.showModal();
361
361
  dialog.focus({ preventScroll: true });
362
362
  this.idle.stir();
@@ -366,7 +366,7 @@ let HelixHeadlessList = class HelixHeadlessList extends HelixElement {
366
366
  this.sync();
367
367
  }
368
368
  if (this.interactive) {
369
- auditName(this, () => null, 'Give it a `label`, or an `aria-label` that says what the rows are.', () => this.label !== '');
369
+ auditName(this, () => null, 'list', () => this.label !== '');
370
370
  }
371
371
  if (this.childrenKnown) {
372
372
  this.toggleState('empty', this.rows.length === 0);
@@ -346,7 +346,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
346
346
  firstUpdated(changed) {
347
347
  super.firstUpdated(changed);
348
348
  this.finishHydration();
349
- auditName(this, () => null, 'Give it a `label` that says what the map shows, such as "Stores in Edinburgh".', () => this.label !== '');
349
+ auditName(this, () => null, 'map', () => this.label !== '');
350
350
  }
351
351
  updated(changed) {
352
352
  super.updated(changed);
@@ -116,7 +116,7 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
116
116
  : [],
117
117
  });
118
118
  if (this.partNamed('item')) {
119
- auditName(this, () => this.partNamed('item') ?? undefined, 'Give the row words in its default slot, or a label on the icon it holds.');
119
+ auditName(this, () => this.partNamed('item') ?? undefined, 'row');
120
120
  }
121
121
  }
122
122
  renderDescription() {
@@ -1,5 +1,5 @@
1
1
  import { __decorate } from "tslib";
2
- import { customElement, elementById, HelixActivateEvent, HelixElement, IndicatorController, plainClick, reflectElements, scrollItemIntoView, scrollingAncestor, words, } from '@fusion.dev/helix-core';
2
+ import { customElement, elementById, HelixActivateEvent, HelixElement, IndicatorController, plainClick, reflectElements, scrollingAncestor, scrollWithin, words, } from '@fusion.dev/helix-core';
3
3
  import { html, nothing } from 'lit';
4
4
  import { property } from 'lit/decorators.js';
5
5
  import { levelOf, nest } from '../levels.js';
@@ -151,7 +151,7 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
151
151
  const sections = this.scroller;
152
152
  const node = scrollingAncestor(link, { axis: 'y', overflowing: true, stop: this.page });
153
153
  if (node && node !== sections && !node.contains(sections)) {
154
- scrollItemIntoView(link, { scroller: node });
154
+ scrollWithin(link, node);
155
155
  }
156
156
  }
157
157
  get atEnd() {
@@ -216,7 +216,7 @@ let HelixHeadlessPopover = class HelixHeadlessPopover extends MixinControllerOpe
216
216
  this.tellTrigger();
217
217
  this.nameByHeading();
218
218
  if (changed.has('open') && this.open) {
219
- auditName(this, () => null, 'Give it a `label`, or a heading in the panel.', () => this.label !== '' || this.namedByHeading || headingIn(this.slotted) != null);
219
+ auditName(this, () => null, 'popover', () => this.label !== '' || this.namedByHeading || headingIn(this.slotted) != null);
220
220
  }
221
221
  if (this.settled) {
222
222
  this.hideAround();
@@ -243,13 +243,11 @@ export declare class HelixHeadlessTable extends HelixHeadlessTable_base {
243
243
  * **It is also the only track list a server can send.** The shim has no tree
244
244
  * to walk, so a render there knows of no columns and this attribute is the
245
245
  * whole answer: a table given one lays its grid out in the first paint, and a
246
- * table sized only by its columns is one track wide until it upgrades.
246
+ * table sized only by its columns takes, until it upgrades, a track for each
247
+ * column at the width a column with no `width` takes (`helix.css`).
247
248
  *
248
- * **Data Table has none of this, and should not.** Its columns are its schema
249
- * rather than an option, so there is always a column to write a `width` on
250
- * and a list here would be a second way to say what that says. This table's
251
- * cells are markup, and a table written as rows of cells with no column
252
- * declared in it has nowhere else to be sized from.
249
+ * Data Table takes the same list, for the same reason: a data table that
250
+ * serves its first page as rows is laid out by it before it upgrades.
253
251
  */
254
252
  columns: string;
255
253
  /**
@@ -1,38 +1,12 @@
1
1
  import { __decorate } from "tslib";
2
- import { AriaForwardController, anchorAt, assignSlots, auditName, commaList, customElement, cycle, everyState, followLink, format, formatWords, HelixBeforeToggleEvent, HelixElement, HelixLoadMoreEvent, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, HelixToggleEvent, inRowOrder, MixinCollection, onAndroid, parse, ReorderController, rangeAdd, reflectElements, requestChange, scrollingAncestor, sort, splitValues, toggleEvery, treeArrow, VIRTUAL_RULE, VirtualizeController, whenHydrated, words, } from '@fusion.dev/helix-core';
2
+ import { AriaForwardController, anchorAt, assignSlots, auditName, commaList, customElement, cycle, everyState, followLink, format, formatWords, HelixBeforeToggleEvent, HelixElement, HelixLoadMoreEvent, HelixSelectEvent, HelixSelectedEvent, HelixSortEvent, HelixSortedEvent, HelixToggleEvent, inRowOrder, MixinCollection, onAndroid, parse, ReorderController, rangeAdd, reflectElements, requestChange, scrollingAncestor, sort, toggleEvery, treeArrow, VIRTUAL_RULE, VirtualizeController, whenHydrated, words, } from '@fusion.dev/helix-core';
3
3
  import { nothing } from 'lit';
4
4
  import { property, state } from 'lit/decorators.js';
5
5
  import { html } from 'lit/static-html.js';
6
6
  import '../../checkbox/index.js';
7
7
  import { edgeOf } from '../column/table-column.js';
8
8
  import { TableGridController } from '../grid-controller.js';
9
- const tracksOf = (list, wanted) => {
10
- const words = splitValues(list, ' ');
11
- const valid = (track) => typeof CSS === 'undefined' || CSS.supports('grid-template-columns', track);
12
- const tracks = [];
13
- let names = '';
14
- for (const one of words) {
15
- if (tracks.length >= wanted) {
16
- break;
17
- }
18
- if (one.startsWith('[')) {
19
- names += `${one} `;
20
- continue;
21
- }
22
- const repeat = /^repeat\(\s*(\d+)\s*,(.*)\)$/is.exec(one);
23
- if (repeat) {
24
- const inner = tracksOf(repeat[2] ?? '', wanted);
25
- for (let time = 0; time < Number(repeat[1]) && tracks.length < wanted; time += 1) {
26
- tracks.push(...inner.map((track, index) => (time === 0 && index === 0 ? names + track : track)));
27
- }
28
- }
29
- else if (!/^repeat\(/i.test(one)) {
30
- tracks.push(valid(names + one) ? names + one : '');
31
- }
32
- names = '';
33
- }
34
- return tracks.slice(0, wanted);
35
- };
9
+ import { tracksOf } from '../tracks.js';
36
10
  const SHORT = 32;
37
11
  const FLOOR = '14ch';
38
12
  const SAMPLE = 200;
@@ -812,7 +786,7 @@ let HelixHeadlessTable = class HelixHeadlessTable extends MixinCollection(HelixE
812
786
  this.markSorted();
813
787
  this.markSelected();
814
788
  this.nameByHeading();
815
- auditName(this, () => null, 'Give it a `label`, or put a heading in its `heading` slot, that says what the rows are.', () => this.label !== '' || this.headings.length > 0);
789
+ auditName(this, () => null, 'table', () => this.label !== '' || this.headings.length > 0);
816
790
  if (this.childrenKnown) {
817
791
  this.gridKeys.mark();
818
792
  this.gridKeys.place();
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The first `wanted` tracks of a track list, one entry each, so every declared
3
+ * column can take the one in its place.
4
+ *
5
+ * Split on the spaces between tracks and never inside a function, so
6
+ * `minmax(0, 1fr)` stays one track. A `repeat()` with a count is that many
7
+ * tracks, written out; an automatic one names no count and yields none. Line
8
+ * names go with the track after them.
9
+ *
10
+ * Everything above the split is a fact about a grid track list rather than
11
+ * about a value list, which is why only the split itself is shared.
12
+ *
13
+ * **A track the grid would refuse yields nothing either**, where the platform
14
+ * can say. One invalid track makes the whole `grid-template-columns` invalid,
15
+ * which a grid ignores in silence and draws as one column. That is the failure
16
+ * that once had `columns` deprecated, a list of headings handed over as widths,
17
+ * and dealt out a track at a time it costs the one column its width and no
18
+ * more.
19
+ */
20
+ export declare const tracksOf: (list: string, wanted: number) => string[];
@@ -0,0 +1,28 @@
1
+ import { splitValues } from '@fusion.dev/helix-core';
2
+ export const tracksOf = (list, wanted) => {
3
+ const words = splitValues(list, ' ');
4
+ const valid = (track) => typeof CSS === 'undefined' || CSS.supports('grid-template-columns', track);
5
+ const tracks = [];
6
+ let names = '';
7
+ for (const one of words) {
8
+ if (tracks.length >= wanted) {
9
+ break;
10
+ }
11
+ if (one.startsWith('[')) {
12
+ names += `${one} `;
13
+ continue;
14
+ }
15
+ const repeat = /^repeat\(\s*(\d+)\s*,(.*)\)$/is.exec(one);
16
+ if (repeat) {
17
+ const inner = tracksOf(repeat[2] ?? '', wanted);
18
+ for (let time = 0; time < Number(repeat[1]) && tracks.length < wanted; time += 1) {
19
+ tracks.push(...inner.map((track, index) => (time === 0 && index === 0 ? names + track : track)));
20
+ }
21
+ }
22
+ else if (!/^repeat\(/i.test(one)) {
23
+ tracks.push(valid(names + one) ? names + one : '');
24
+ }
25
+ names = '';
26
+ }
27
+ return tracks.slice(0, wanted);
28
+ };
@@ -1,9 +1,8 @@
1
1
  import { __decorate } from "tslib";
2
- import { AriaForwardController, customElement, deepActiveElement, HelixActivateEvent, HelixElement, IndicatorController, isRtl, KeyboardNavigationController, MIRROR_RULE, MixinControllerActive, scrollItemIntoView, whenHydrated, words, } from '@fusion.dev/helix-core';
2
+ import { AriaForwardController, customElement, deepActiveElement, glyphPath, HelixActivateEvent, HelixElement, IndicatorController, isRtl, KeyboardNavigationController, MIRROR_RULE, MixinControllerActive, scrollWithin, whenHydrated, words, } from '@fusion.dev/helix-core';
3
3
  import { caretDown, caretLeft, caretRight, caretUp } from '@fusion.dev/helix-icons';
4
4
  import { html, nothing } from 'lit';
5
5
  import { property, state } from 'lit/decorators.js';
6
- import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
7
6
  import { HelixHeadlessTabPanel } from '../panel/tab-panel.js';
8
7
  import { HelixHeadlessTab } from '../tab/tab.js';
9
8
  let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(HelixElement) {
@@ -192,7 +191,7 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
192
191
  const strip = this.partNamed('tabs');
193
192
  const tab = this.tabs.find((one) => one.selected);
194
193
  if (strip && tab) {
195
- scrollItemIntoView(tab, { padding: this.scrollControls ? 40 : 0, scroller: strip });
194
+ scrollWithin(tab, strip, { padding: this.scrollControls ? 40 : 0 });
196
195
  }
197
196
  }
198
197
  updated(changed) {
@@ -223,8 +222,8 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
223
222
  const icon = down ? (toward > 0 ? caretDown : caretUp) : toward > 0 ? caretRight : caretLeft;
224
223
  const mirror = down ? nothing : '';
225
224
  return toward > 0
226
- ? html `<slot name="scroll-end-icon"><svg part="scroll-end-mark" data-mirror=${mirror} aria-hidden="true" fill="currentColor" viewBox=${icon.viewBox}>${unsafeSVG(icon.content)}</svg></slot>`
227
- : html `<slot name="scroll-start-icon"><svg part="scroll-start-mark" data-mirror=${mirror} aria-hidden="true" fill="currentColor" viewBox=${icon.viewBox}>${unsafeSVG(icon.content)}</svg></slot>`;
225
+ ? html `<slot name="scroll-end-icon"><svg part="scroll-end-mark" data-mirror=${mirror} aria-hidden="true" fill="currentColor" viewBox=${icon.viewBox}><path d=${glyphPath(icon)}></path></svg></slot>`
226
+ : html `<slot name="scroll-start-icon"><svg part="scroll-start-mark" data-mirror=${mirror} aria-hidden="true" fill="currentColor" viewBox=${icon.viewBox}><path d=${glyphPath(icon)}></path></svg></slot>`;
228
227
  }
229
228
  render() {
230
229
  return html `<div
@@ -1,9 +1,8 @@
1
1
  import { __decorate } from "tslib";
2
- import { customElement, HelixElement, MixinDismissible, MixinSelectableItem, plainClick, reflectElements, words, } from '@fusion.dev/helix-core';
2
+ import { customElement, glyphPath, HelixElement, MixinDismissible, MixinSelectableItem, plainClick, reflectElements, words, } from '@fusion.dev/helix-core';
3
3
  import { x } from '@fusion.dev/helix-icons';
4
4
  import { html, nothing } from 'lit';
5
5
  import { property } from 'lit/decorators.js';
6
- import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
7
6
  let HelixHeadlessTab = class HelixHeadlessTab extends MixinDismissible(MixinSelectableItem(HelixElement)) {
8
7
  constructor() {
9
8
  super(...arguments);
@@ -49,7 +48,7 @@ let HelixHeadlessTab = class HelixHeadlessTab extends MixinDismissible(MixinSele
49
48
  }
50
49
  return html `<span part="dismiss" aria-hidden="true" title=${this.dismissLabel} @click=${this.handleDismissPress}
51
50
  ><slot name="dismiss-icon"
52
- ><svg part="dismiss-mark" fill="currentColor" viewBox=${x.viewBox} aria-hidden="true">${unsafeSVG(x.content)}</svg></slot
51
+ ><svg part="dismiss-mark" fill="currentColor" viewBox=${x.viewBox} aria-hidden="true"><path d=${glyphPath(x)}></path></svg></slot
53
52
  ></span
54
53
  >`;
55
54
  }
@@ -375,7 +375,7 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
375
375
  }
376
376
  firstUpdated(changed) {
377
377
  super.firstUpdated(changed);
378
- auditName(this, () => null, 'Give it a `label`, a label in the `label` slot, or an `aria-label` that says what the time is for.', () => this.listName !== '');
378
+ auditName(this, () => null, 'time-picker', () => this.listName !== '');
379
379
  const list = this.partNamed('list');
380
380
  if (list) {
381
381
  auditName(list, () => null, '', () => true);
@@ -34,7 +34,7 @@ let HelixHeadlessToggle = class HelixHeadlessToggle extends MixinSelectableItem(
34
34
  firstUpdated(changed) {
35
35
  super.firstUpdated(changed);
36
36
  this.finishHydration();
37
- auditName(this, () => this.partNamed('item'), 'Give it words in its default slot, an `aria-label`, or a label on the icon it holds.');
37
+ auditName(this, () => this.partNamed('item'), 'toggle');
38
38
  }
39
39
  click() {
40
40
  if (!this.clicks.click()) {
@@ -233,7 +233,7 @@ let HelixHeadlessTour = class HelixHeadlessTour extends MixinControllerOpen(Heli
233
233
  this.settle();
234
234
  this.light();
235
235
  if (changed.has('open') && this.open) {
236
- auditName(this, () => null, 'Give it a `label`, such as "Getting started".', () => this.label !== '');
236
+ auditName(this, () => null, 'tour', () => this.label !== '');
237
237
  }
238
238
  this.follow(changed);
239
239
  this.wait();
@@ -468,7 +468,7 @@ let HelixHeadlessTree = class HelixHeadlessTree extends MixinCollection(MixinCon
468
468
  firstUpdated(changed) {
469
469
  super.firstUpdated(changed);
470
470
  this.syncItems();
471
- auditName(this, () => null, 'Give it a `label` that says what it holds, such as "Files".', () => this.hasAttribute('label') || this.label !== 'Tree');
471
+ auditName(this, () => null, 'tree', () => this.hasAttribute('label') || this.label !== 'Tree');
472
472
  }
473
473
  updated(changed) {
474
474
  super.updated(changed);
@@ -459,7 +459,7 @@ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
459
459
  firstUpdated(changed) {
460
460
  super.firstUpdated?.(changed);
461
461
  this.finishHydration();
462
- auditName(this, () => null, 'Give it a `label` that says what the video shows, such as "Product tour".', () => this.label !== '');
462
+ auditName(this, () => null, 'video', () => this.label !== '');
463
463
  const video = this.video;
464
464
  if (video) {
465
465
  if (this.defaultMuted || this.silent) {