@fusion.dev/helix-headless 0.185.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 (43) hide show
  1. package/custom-elements.json +2663 -31
  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/dialog/root/dialog-root.js +1 -1
  6. package/dist/drawer/root/drawer-root.js +1 -1
  7. package/dist/drilldown/item/drilldown-item.js +1 -1
  8. package/dist/element-events.generated.js +7 -0
  9. package/dist/graph/root/graph-root.d.ts +6 -4
  10. package/dist/graph/root/graph-root.js +3 -3
  11. package/dist/heatmap/root/heatmap-root.d.ts +15 -15
  12. package/dist/icon/all/index.js +0 -3
  13. package/dist/icon/root/icon-root.d.ts +11 -18
  14. package/dist/icon/root/icon-root.js +4 -16
  15. package/dist/icon/set.js +2 -16
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.js +2 -1
  18. package/dist/isometric-heatmap/index.d.ts +1 -0
  19. package/dist/isometric-heatmap/index.js +1 -0
  20. package/dist/isometric-heatmap/projection.d.ts +70 -0
  21. package/dist/isometric-heatmap/projection.js +49 -0
  22. package/dist/isometric-heatmap/root/isometric-heatmap-root.d.ts +118 -0
  23. package/dist/isometric-heatmap/root/isometric-heatmap-root.js +127 -0
  24. package/dist/lightbox/root/lightbox-root.js +1 -1
  25. package/dist/list/root/list-root.js +1 -1
  26. package/dist/map/root/map-root.js +1 -1
  27. package/dist/menu/item/menu-item.js +1 -1
  28. package/dist/outline/root/outline-root.js +2 -2
  29. package/dist/popover/root/popover-root.js +1 -1
  30. package/dist/table/root/table-root.js +1 -1
  31. package/dist/tabs/root/tabs-root.js +4 -5
  32. package/dist/tabs/tab/tab.js +2 -3
  33. package/dist/time-picker/root/time-picker-root.js +1 -1
  34. package/dist/toggle/root/toggle-root.js +1 -1
  35. package/dist/tour/root/tour-root.js +1 -1
  36. package/dist/tree/root/tree-root.js +1 -1
  37. package/dist/video/root/video-root.js +1 -1
  38. package/lint.json +1 -1
  39. package/package.json +6 -6
  40. package/types/jsx.d.ts +1274 -40
  41. package/types/svelte.d.ts +1 -0
  42. package/types/vue.d.ts +264 -6
  43. package/types/words.d.ts +69 -0
@@ -83,7 +83,7 @@ let HelixHeadlessAccordionItem = class HelixHeadlessAccordionItem extends MixinC
83
83
  }
84
84
  firstUpdated(changed) {
85
85
  super.firstUpdated(changed);
86
- auditName(this, () => this.renderRoot.querySelector(`#${TRIGGER_ID}`), 'Write its title in the `summary` slot.');
86
+ auditName(this, () => this.renderRoot.querySelector(`#${TRIGGER_ID}`), 'accordion-item');
87
87
  }
88
88
  get accordion() {
89
89
  return closestInstance(this, HelixHeadlessAccordion);
@@ -206,7 +206,7 @@ let HelixHeadlessButtonGroup = class HelixHeadlessButtonGroup extends HelixEleme
206
206
  }
207
207
  firstUpdated(changed) {
208
208
  super.firstUpdated(changed);
209
- auditName(this, () => null, 'Give it a `label`, or an `aria-label` that says what the buttons are for.', () => this.label !== '');
209
+ auditName(this, () => null, 'button-group', () => this.label !== '');
210
210
  }
211
211
  updated(changed) {
212
212
  super.updated(changed);
@@ -1951,7 +1951,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1951
1951
  firstUpdated(changed) {
1952
1952
  super.firstUpdated(changed);
1953
1953
  this.finishHydration();
1954
- auditName(this, () => null, 'Give it a `label` that says what the board holds, such as "Release plan".', () => this.label !== '');
1954
+ auditName(this, () => null, 'canvas', () => this.label !== '');
1955
1955
  if (this.waiting) {
1956
1956
  this.measured = this.box;
1957
1957
  this.view = this.waiting;
@@ -144,7 +144,7 @@ let HelixHeadlessDialog = class HelixHeadlessDialog extends MixinControllerOpen(
144
144
  return;
145
145
  }
146
146
  if (this.open && !dialog.open) {
147
- auditName(this, () => null, 'Give it a `label`, or a heading in the `header` slot.', () => this.label !== '' ||
147
+ auditName(this, () => null, 'dialog', () => this.label !== '' ||
148
148
  this.namedByHeader ||
149
149
  headingsIn(this.renderRoot
150
150
  .querySelector('slot[name="header"]')
@@ -242,7 +242,7 @@ let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(
242
242
  return;
243
243
  }
244
244
  if (this.open && !panel.open) {
245
- auditName(this, () => null, 'Give it a `label`, or a heading in the `header` slot.', () => this.label !== '' ||
245
+ auditName(this, () => null, 'dialog', () => this.label !== '' ||
246
246
  this.namedByHeader ||
247
247
  headingsIn(this.renderRoot
248
248
  .querySelector('slot[name="header"]')
@@ -58,7 +58,7 @@ let HelixHeadlessDrilldownItem = class HelixHeadlessDrilldownItem extends HelixE
58
58
  updated(changed) {
59
59
  super.updated(changed);
60
60
  if (this.partNamed('item')) {
61
- auditName(this, () => this.partNamed('item'), 'Give the row words in its default slot, or a label on the icon it holds.');
61
+ auditName(this, () => this.partNamed('item'), 'row');
62
62
  }
63
63
  }
64
64
  firstUpdated(changed) {
@@ -148,6 +148,13 @@ registerElementEvents({
148
148
  'helix-headless-icon-button': ['helix-press'],
149
149
  'helix-headless-image': ['error', 'load'],
150
150
  'helix-headless-input': ['change', 'helix-before-change', 'helix-clear', 'input'],
151
+ 'helix-headless-isometric-heatmap': [
152
+ 'change',
153
+ 'helix-before-change',
154
+ 'helix-reading-change',
155
+ 'helix-reading-press',
156
+ 'helix-resize',
157
+ ],
151
158
  'helix-headless-kbd': ['helix-shortcut'],
152
159
  'helix-headless-legend': ['change', 'helix-before-change'],
153
160
  'helix-headless-lightbox': [
@@ -6,10 +6,12 @@ declare const HelixHeadlessGraph_base: (new (...args: any[]) => import("@fusion.
6
6
  * @summary A frame around a graph of notes drawn by another library, and the list that makes it readable.
7
7
  *
8
8
  * @description
9
- * Helix draws no graph. A force layout of thousands of notes is physics and
10
- * WebGL, which a renderer such as Sigma.js does. Put the renderer's box in the
11
- * frame, and `connectGraph` from `@fusion.dev/helix-graph` draws `nodes` and
12
- * `links` in it and keeps it in step with the frame.
9
+ * The frame draws no graph itself. A force layout of thousands of notes is
10
+ * physics and WebGL, which a renderer such as Sigma.js does, and an
11
+ * architecture diagram is drawn by `isometricDriver` from
12
+ * `@fusion.dev/helix-graph/isometric`. Put the drawing's box in the frame, and
13
+ * `connectGraph` from `@fusion.dev/helix-graph` draws `nodes` and `links` in
14
+ * it and keeps it in step with the frame.
13
15
  *
14
16
  * What the frame adds is what a drawing of a graph cannot do: a list of every
15
17
  * note beside it, which is the graph for a screen reader and a quick way to
@@ -1,5 +1,5 @@
1
1
  import { __decorate } from "tslib";
2
- import { auditName, customElement, focusItemOf, formatWords, HelixElement, HelixGraphFilterEvent, HelixGraphLayoutEvent, HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphReadyEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, isRtl, KeyboardNavigationController, MixinControllerActive, MixinViewport, SCROLLER_RULE, scrollItemIntoView, scrollRoom, Typeahead, typeaheadBy, VIRTUAL_RULE, VirtualizeController, words, wordsLocale, } from '@fusion.dev/helix-core';
2
+ import { auditName, customElement, focusItemOf, formatWords, HelixElement, HelixGraphFilterEvent, HelixGraphLayoutEvent, HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphReadyEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, isRtl, KeyboardNavigationController, MixinControllerActive, MixinViewport, SCROLLER_RULE, scrollRoom, scrollWithin, Typeahead, typeaheadBy, VIRTUAL_RULE, VirtualizeController, words, wordsLocale, } from '@fusion.dev/helix-core';
3
3
  import { list, magnifyingGlass } from '@fusion.dev/helix-icons';
4
4
  import { html, nothing } from 'lit';
5
5
  import { property, state } from 'lit/decorators.js';
@@ -213,7 +213,7 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
213
213
  }
214
214
  firstUpdated(changed) {
215
215
  super.firstUpdated(changed);
216
- auditName(this, () => null, 'Give it a `label` that says what the notes are, such as "Research notes".', () => this.label !== '');
216
+ auditName(this, () => null, 'graph', () => this.label !== '');
217
217
  }
218
218
  willUpdate(changed) {
219
219
  super.willUpdate(changed);
@@ -384,7 +384,7 @@ let HelixHeadlessGraph = class HelixHeadlessGraph extends MixinViewport(MixinCon
384
384
  if (this.cursor >= first && this.cursor < last) {
385
385
  const row = this.renderRoot.querySelector?.(`[data-index="${this.cursor}"]`);
386
386
  if (row) {
387
- scrollItemIntoView(row, { scroller: list });
387
+ scrollWithin(row, list);
388
388
  }
389
389
  return;
390
390
  }
@@ -1,4 +1,4 @@
1
- import { AriaForwardController, HelixElement } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixElement, StatusController } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../tooltip/index.js';
4
4
  import { type Reading } from './buckets.js';
@@ -309,7 +309,7 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
309
309
  * difference is a whole strip: unmeasured has to show everything, since a
310
310
  * first paint that shows one cell and then expands is a flicker on every load.
311
311
  */
312
- private width;
312
+ protected width: number | undefined;
313
313
  /**
314
314
  * The cell and gap the strip is drawing, in pixels, once it has been read.
315
315
  *
@@ -353,13 +353,13 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
353
353
  /** The `value` the last rectangle wrote. */
354
354
  private shaped;
355
355
  /** The region a change of selection is said in, rendered from the first change. */
356
- private readonly status;
356
+ protected readonly status: StatusController;
357
357
  /**
358
358
  * The name each drawn cell stands for in `value`: the date its row's header
359
359
  * carries in a `<time datetime>`, or the header's words where it carries
360
360
  * none.
361
361
  */
362
- private keysOf;
362
+ protected keysOf(cells: readonly Reading[]): string[];
363
363
  /**
364
364
  * Moves `value` to `next`, asking first: `helix-before-change` carries it,
365
365
  * a listener that refuses keeps the selection, and `change` follows, with
@@ -375,7 +375,7 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
375
375
  */
376
376
  private stretch;
377
377
  /** Notes where the keys rest before the pointer controller moves them. */
378
- private readonly onStripKeydown;
378
+ protected readonly onStripKeydown: () => void;
379
379
  /**
380
380
  * Space and Enter select the cell the keys rest on, and Shift with a key
381
381
  * that moved them takes in the range from the anchor. After the pointer
@@ -386,7 +386,7 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
386
386
  * A press on a cell selects it, under `selection-mode`, and Shift takes the
387
387
  * rectangle to it under `selection-shape="rectangle"`.
388
388
  */
389
- private readonly onClick;
389
+ protected readonly onClick: (event: MouseEvent) => void;
390
390
  /**
391
391
  * Every row in the table, oldest first, as the table is written, with its
392
392
  * reading where the row has one.
@@ -400,7 +400,7 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
400
400
  * cell along, while the tooltip still counted rows and named the day beside
401
401
  * the one under the pointer.
402
402
  */
403
- private get points();
403
+ protected get points(): Reading[];
404
404
  /**
405
405
  * `rows` as a usable count, which is not always what the attribute says.
406
406
  *
@@ -408,7 +408,7 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
408
408
  * need it, and a `rows="0"` or a `rows="2.5"` that reached one of them and not
409
409
  * the other would draw a strip whose columns and whose capacity disagreed.
410
410
  */
411
- private get rowCount();
411
+ protected get rowCount(): number;
412
412
  /**
413
413
  * The cell width the arithmetic should plan around, in pixels.
414
414
  *
@@ -416,11 +416,11 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
416
416
  * it is for; otherwise what the strip draws, and the declared default only
417
417
  * until the first measurement lands.
418
418
  */
419
- private get cellPx();
419
+ protected get cellPx(): number;
420
420
  /** The gap the arithmetic should plan around, on the same terms as `cellPx`. */
421
- private get gapPx();
421
+ protected get gapPx(): number;
422
422
  /** How many cells the container has room for, as a whole number of columns. */
423
- private get room();
423
+ protected get room(): number;
424
424
  /**
425
425
  * Reads back the cell and the gap the strip resolved.
426
426
  *
@@ -443,7 +443,7 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
443
443
  * The tail rather than the head: a status strip that drops the newest days to
444
444
  * keep the oldest is answering a question nobody asked.
445
445
  */
446
- private get drawn();
446
+ protected get drawn(): Reading[];
447
447
  /** How many of the oldest rows a narrow strip leaves undrawn. */
448
448
  private get dropped();
449
449
  /**
@@ -533,9 +533,9 @@ export declare class HelixHeadlessHeatmap extends HelixHeadlessHeatmap_base {
533
533
  * bubbles, so one handler covers however many cells there are, and unlike
534
534
  * `mouseover` it is delivered for a pen.
535
535
  */
536
- private readonly onPointerOver;
537
- private readonly onPointerUp;
538
- private readonly onPointerLeave;
536
+ protected readonly onPointerOver: (event: PointerEvent) => void;
537
+ protected readonly onPointerUp: (event: PointerEvent) => void;
538
+ protected readonly onPointerLeave: () => void;
539
539
  }
540
540
  declare global {
541
541
  interface HTMLElementTagNameMap {
@@ -1,4 +1 @@
1
- import { icons } from '@fusion.dev/helix-icons';
2
- import { registerIcons } from '../registry.js';
3
- registerIcons(icons);
4
1
  export * from '../index.js';
@@ -18,14 +18,11 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
18
18
  * a dark background. Shapes that set their own `fill` or `stroke` are
19
19
  * unaffected.
20
20
  *
21
- * **A name is looked up in a registry.** The family's entry registers a small
22
- * set: the four carets, `arrow-left`, `arrow-right`, `check`, `x`, `plus`,
23
- * `minus`, `magnifying-glass`, `info` and `warning`. `icon/all` registers the
24
- * whole set, so any name in it draws, and a name that resolves to nothing
25
- * says to import it. The element's own entry,
26
- * `@fusion.dev/helix-headless/icon/root`, registers none: a page that imports
27
- * it and calls `registerIcons` with the glyphs it names ships those and no
28
- * others. `registerIconLibrary` resolves names under a prefix of your own,
21
+ * **A name is looked up in a registry.** The family's entry registers the
22
+ * whole set, so any name in it draws, and `icon/all` registers the same set.
23
+ * The element's own entry, `@fusion.dev/helix-headless/icon/root`, registers
24
+ * none: a page that imports it and calls `registerIcons` with the glyphs it
25
+ * names ships those and no others. `registerIconLibrary` resolves names under a prefix of your own,
29
26
  * such as `lucide:camera`, at once or later, and the element fires `load` when
30
27
  * a glyph that came later is drawn. A glyph registered after the element
31
28
  * rendered is drawn then.
@@ -98,10 +95,12 @@ export declare class HelixHeadlessIcon extends HelixElement {
98
95
  * The name of the icon in the Helix set.
99
96
  *
100
97
  * **A lookup, and the reason both this and `icon` exist.** A name is a string,
101
- * so it is what markup can carry, and resolving one means an index that no
102
- * bundler can shake. The family's entry indexes a small set, `icon/all`
103
- * indexes the whole set at 15 kB, and `icon/root` indexes nothing, for a
104
- * page that registers the glyphs it names or hands over the value instead.
98
+ * so it is what markup can carry, and resolving one means an index of the
99
+ * whole set that no bundler can shake: 15 kB for a consumer who renders one
100
+ * mark. An attribute is the only thing an HTML author can write, and that is
101
+ * worth the index; a consumer who would rather not pay it imports the
102
+ * element from `icon/root` and registers the glyphs it names, or hands over
103
+ * the value instead.
105
104
  *
106
105
  * A name written `prefix:name` asks the library registered under that
107
106
  * prefix.
@@ -152,12 +151,6 @@ export declare class HelixHeadlessIcon extends HelixElement {
152
151
  protected updated(changed: PropertyValues): void;
153
152
  /** Hands the drawn `<svg>` to the mutator of the library `name` asks, where it has one. */
154
153
  private mutate;
155
- /**
156
- * Asks the button or link a decorative icon sits in whether it has a name.
157
- * Its content is this icon alone where it has none, and the audit says so
158
- * once, in a development build only. A Helix button reports its own name.
159
- */
160
- private auditControl;
161
154
  /** The name a report is waiting on the page to settle for, so it is asked once. */
162
155
  private pending;
163
156
  /**
@@ -1,5 +1,5 @@
1
1
  import { __decorate } from "tslib";
2
- import { auditName, customElement, HelixElement, hostAria, MIRROR_RULE, reportRuntime, } from '@fusion.dev/helix-core';
2
+ import { auditIconControl, customElement, HelixElement, hostAria, MIRROR_RULE, reportRuntime, } from '@fusion.dev/helix-core';
3
3
  import { html, nothing } from 'lit';
4
4
  import { property } from 'lit/decorators.js';
5
5
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
@@ -81,7 +81,7 @@ let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
81
81
  super.updated(changed);
82
82
  if (this.drawn !== undefined) {
83
83
  this.mutate();
84
- this.auditControl();
84
+ auditIconControl(this, this.label);
85
85
  }
86
86
  const asked = this.asked;
87
87
  if (this.arrived === asked && this.drawn !== undefined) {
@@ -105,16 +105,6 @@ let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
105
105
  mutator(svg);
106
106
  }
107
107
  }
108
- auditControl() {
109
- if (this.label !== '' || this.hasAttribute('aria-label')) {
110
- return;
111
- }
112
- const control = this.closest('a[href], button, summary, [role="button"], [role="link"]');
113
- if (control) {
114
- auditName(control, () => null, `Its only content is <${this.localName}>, which is decorative: give the icon a \`label\`, or the control an \`aria-label\`.`, () => (control.textContent ?? '').trim() !== '' ||
115
- control.querySelector('[alt]:not([alt=""]), [aria-label]:not([aria-label=""])') !== null);
116
- }
117
- }
118
108
  reportMissing() {
119
109
  const asked = this.pending;
120
110
  this.pending = '';
@@ -127,10 +117,8 @@ let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
127
117
  kind: 'unknown-icon',
128
118
  message: this.src === ''
129
119
  ? `<${this.localName} name="${asked}"> names no registered icon, so it draws ` +
130
- `nothing. For a glyph of the Helix set, import '${this.localName.startsWith('helix-headless-')
131
- ? '@fusion.dev/helix-headless'
132
- : '@fusion.dev/helix'}/icon/all', which registers every one. Otherwise check the name, or register ` +
133
- 'the glyph with `registerIcons`.'
120
+ 'nothing. Check the name, register the glyph with `registerIcons`, or import it ' +
121
+ 'and pass it as `icon`.'
134
122
  : `<${this.localName} src="${asked}"> could not be fetched as an SVG file, so it ` +
135
123
  'draws nothing. Check the URL, and that the file is an `<svg>`.',
136
124
  tag: this.localName,
package/dist/icon/set.js CHANGED
@@ -1,17 +1,3 @@
1
- import { arrowLeft, arrowRight, caretDown, caretLeft, caretRight, caretUp, check, info, magnifyingGlass, minus, plus, warning, x, } from '@fusion.dev/helix-icons';
1
+ import { icons } from '@fusion.dev/helix-icons';
2
2
  import { registerIcons } from './registry.js';
3
- registerIcons([
4
- arrowLeft,
5
- arrowRight,
6
- caretDown,
7
- caretLeft,
8
- caretRight,
9
- caretUp,
10
- check,
11
- info,
12
- magnifyingGlass,
13
- minus,
14
- plus,
15
- warning,
16
- x,
17
- ]);
3
+ registerIcons(icons);
package/dist/index.d.ts CHANGED
@@ -50,10 +50,11 @@ export * from './filter-input/menu/index.js';
50
50
  export * from './format-date/index.js';
51
51
  export * from './graph/index.js';
52
52
  export * from './heatmap/index.js';
53
- export * from './icon/all/index.js';
53
+ export * from './icon/index.js';
54
54
  export * from './icon-button/index.js';
55
55
  export * from './image/index.js';
56
56
  export * from './input/index.js';
57
+ export * from './isometric-heatmap/index.js';
57
58
  export * from './kbd/index.js';
58
59
  export * from './legend/index.js';
59
60
  export * from './lightbox/index.js';
package/dist/index.js CHANGED
@@ -41,10 +41,11 @@ export * from './filter-input/menu/index.js';
41
41
  export * from './format-date/index.js';
42
42
  export * from './graph/index.js';
43
43
  export * from './heatmap/index.js';
44
- export * from './icon/all/index.js';
44
+ export * from './icon/index.js';
45
45
  export * from './icon-button/index.js';
46
46
  export * from './image/index.js';
47
47
  export * from './input/index.js';
48
+ export * from './isometric-heatmap/index.js';
48
49
  export * from './kbd/index.js';
49
50
  export * from './legend/index.js';
50
51
  export * from './lightbox/index.js';
@@ -0,0 +1 @@
1
+ export { HelixHeadlessIsometricHeatmap } from './root/isometric-heatmap-root.js';
@@ -0,0 +1 @@
1
+ export { HelixHeadlessIsometricHeatmap } from './root/isometric-heatmap-root.js';
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Where each reading of an isometric heatmap stands, as SVG path data.
3
+ *
4
+ * Kept apart from the element because it is arithmetic a picture hides: a face
5
+ * drawn in the wrong order is covered by the prism it should cover, and a
6
+ * height off by one reading's share still looks like a relief.
7
+ *
8
+ * **The units are cells.** A step along a column or a row is one cell, and a
9
+ * prism's height is in the same unit, so the drawing scales as a whole through
10
+ * the `viewBox` and a server can write it with no width to measure.
11
+ */
12
+ /** How far across the page one step along either axis goes: the cosine of 30 degrees. */
13
+ export declare const ACROSS: number;
14
+ /** A point on the page, in cells. */
15
+ export interface Point {
16
+ x: number;
17
+ y: number;
18
+ }
19
+ /** One reading drawn as a prism: three visible faces, as path data. */
20
+ export interface Prism {
21
+ /** The reading's place among those drawn. */
22
+ index: number;
23
+ /** The face toward the next row, drawn at the lower left. */
24
+ left: string;
25
+ /** The middle of the top face, which is where the tooltip points. */
26
+ peak: Point;
27
+ /** The face toward the next column, drawn at the lower right. */
28
+ right: string;
29
+ /** The face the reading's tone is painted on. */
30
+ top: string;
31
+ }
32
+ /** Every prism, and the box they fit in. */
33
+ export interface Relief {
34
+ /** The box every face fits in, in cells: its left, top, width, and height. */
35
+ box: {
36
+ height: number;
37
+ width: number;
38
+ x: number;
39
+ y: number;
40
+ };
41
+ /** In the order they are painted, which is the order of the readings. */
42
+ prisms: Prism[];
43
+ }
44
+ /**
45
+ * Each reading's height, from zero, as a share of the highest reading times
46
+ * `tallest`.
47
+ *
48
+ * **From zero, because the height is the value.** A height measured from the
49
+ * lowest reading would make a day of 9 against a day of 10 look like nothing
50
+ * against something. A blank reading, a zero, and a negative one stand at zero,
51
+ * a prism having nothing below the floor to show.
52
+ */
53
+ export declare const heightsOf: (values: readonly (number | undefined)[], highest: number, tallest: number) => number[];
54
+ /** The highest positive reading, or zero where there is none. */
55
+ export declare const highestOf: (values: readonly (number | undefined)[]) => number;
56
+ /**
57
+ * Every reading as a prism, filling down each column of `rows` and then
58
+ * across, as the flat heatmap does.
59
+ *
60
+ * **Columns run down to the right and rows down to the left, so the order of
61
+ * the readings is the order to paint them in.** A prism can only cover the
62
+ * neighbors behind it, which are the one before it in its column and the one in
63
+ * the same row of the column before, and both come earlier in the table. With
64
+ * the axes the other way round, the newest column would stand at the back and
65
+ * every tall reading in front of it would hide it.
66
+ *
67
+ * `footprint` is how much of its cell a prism covers along each edge, so `1`
68
+ * leaves no gap between neighbors.
69
+ */
70
+ export declare const reliefOf: (heights: readonly number[], rows: number, footprint: number) => Relief;
@@ -0,0 +1,49 @@
1
+ export const ACROSS = Math.sqrt(3) / 2;
2
+ export const heightsOf = (values, highest, tallest) => values.map((value) => highest > 0 && value !== undefined && value > 0
3
+ ? (Math.min(value, highest) / highest) * tallest
4
+ : 0);
5
+ export const highestOf = (values) => values.reduce((high, value) => (value !== undefined && value > high ? value : high), 0);
6
+ const short = (value) => String(Math.round(value * 1000) / 1000 || 0);
7
+ const at = (column, row, height) => ({
8
+ x: (column - row) * ACROSS,
9
+ y: (column + row) / 2 - height,
10
+ });
11
+ const path = (points) => `M${points.map((point) => `${short(point.x)} ${short(point.y)}`).join('L')}Z`;
12
+ export const reliefOf = (heights, rows, footprint) => {
13
+ const count = Math.max(1, Math.floor(rows) || 1);
14
+ const inset = (1 - Math.min(1, Math.max(0, footprint))) / 2;
15
+ let left = Number.POSITIVE_INFINITY;
16
+ let right = Number.NEGATIVE_INFINITY;
17
+ let top = Number.POSITIVE_INFINITY;
18
+ let bottom = Number.NEGATIVE_INFINITY;
19
+ const prisms = heights.map((height, index) => {
20
+ const column = Math.floor(index / count);
21
+ const row = index % count;
22
+ const near = column + inset;
23
+ const far = column + 1 - inset;
24
+ const start = row + inset;
25
+ const end = row + 1 - inset;
26
+ const back = at(near, start, height);
27
+ const across = at(far, start, height);
28
+ const front = at(far, end, height);
29
+ const down = at(near, end, height);
30
+ const acrossFloor = at(far, start, 0);
31
+ const frontFloor = at(far, end, 0);
32
+ const downFloor = at(near, end, 0);
33
+ left = Math.min(left, down.x);
34
+ right = Math.max(right, across.x);
35
+ top = Math.min(top, back.y);
36
+ bottom = Math.max(bottom, frontFloor.y);
37
+ return {
38
+ index,
39
+ left: path([down, front, frontFloor, downFloor]),
40
+ peak: at(column + 0.5, row + 0.5, height),
41
+ right: path([front, across, acrossFloor, frontFloor]),
42
+ top: path([back, across, front, down]),
43
+ };
44
+ });
45
+ if (prisms.length === 0) {
46
+ return { box: { height: 0, width: 0, x: 0, y: 0 }, prisms };
47
+ }
48
+ return { box: { height: bottom - top, width: right - left, x: left, y: top }, prisms };
49
+ };
@@ -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
+ }