@fusion.dev/helix-core 0.0.0-dev

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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/custom-elements.json +8005 -0
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +67 -0
  4. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +146 -0
  5. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +79 -0
  6. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +140 -0
  7. package/dist/controllers/command-controller/command-controller.d.ts +83 -0
  8. package/dist/controllers/command-controller/command-controller.js +93 -0
  9. package/dist/controllers/copy-controller/copy-controller.d.ts +58 -0
  10. package/dist/controllers/copy-controller/copy-controller.js +128 -0
  11. package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.d.ts +76 -0
  12. package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +67 -0
  13. package/dist/controllers/dismiss-controller/dismiss-controller.d.ts +58 -0
  14. package/dist/controllers/dismiss-controller/dismiss-controller.js +165 -0
  15. package/dist/controllers/drilldown-controller/drilldown-controller.d.ts +127 -0
  16. package/dist/controllers/drilldown-controller/drilldown-controller.js +171 -0
  17. package/dist/controllers/floating-controller/floating-controller.d.ts +123 -0
  18. package/dist/controllers/floating-controller/floating-controller.js +227 -0
  19. package/dist/controllers/focus-trap-controller/focus-trap-controller.d.ts +26 -0
  20. package/dist/controllers/focus-trap-controller/focus-trap-controller.js +93 -0
  21. package/dist/controllers/indicator-controller/indicator-controller.d.ts +97 -0
  22. package/dist/controllers/indicator-controller/indicator-controller.js +164 -0
  23. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +113 -0
  24. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +117 -0
  25. package/dist/controllers/popover-controller/popover-controller.d.ts +132 -0
  26. package/dist/controllers/popover-controller/popover-controller.js +150 -0
  27. package/dist/controllers/source-controller/source-controller.d.ts +55 -0
  28. package/dist/controllers/source-controller/source-controller.js +94 -0
  29. package/dist/controllers/token-color-controller/token-color-controller.d.ts +63 -0
  30. package/dist/controllers/token-color-controller/token-color-controller.js +105 -0
  31. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +127 -0
  32. package/dist/controllers/virtualize-controller/virtualize-controller.js +216 -0
  33. package/dist/events/element-events.d.ts +4 -0
  34. package/dist/events/element-events.js +34 -0
  35. package/dist/events/events.d.ts +743 -0
  36. package/dist/events/events.js +799 -0
  37. package/dist/helix-element/helix-element.d.ts +201 -0
  38. package/dist/helix-element/helix-element.js +695 -0
  39. package/dist/helix-form-element/helix-form-element.d.ts +152 -0
  40. package/dist/helix-form-element/helix-form-element.js +194 -0
  41. package/dist/index.d.ts +54 -0
  42. package/dist/index.js +54 -0
  43. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +137 -0
  44. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +140 -0
  45. package/dist/mixins/mixin-collection/index.d.ts +1 -0
  46. package/dist/mixins/mixin-collection/index.js +1 -0
  47. package/dist/mixins/mixin-collection/mixin-collection.d.ts +58 -0
  48. package/dist/mixins/mixin-collection/mixin-collection.js +154 -0
  49. package/dist/mixins/mixin-controller-active/mixin-controller-active.d.ts +53 -0
  50. package/dist/mixins/mixin-controller-active/mixin-controller-active.js +155 -0
  51. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +54 -0
  52. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +157 -0
  53. package/dist/mixins/mixin-controller-open/mixin-controller-open.d.ts +49 -0
  54. package/dist/mixins/mixin-controller-open/mixin-controller-open.js +176 -0
  55. package/dist/mixins/mixin-controller-value/mixin-controller-value.d.ts +73 -0
  56. package/dist/mixins/mixin-controller-value/mixin-controller-value.js +173 -0
  57. package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +41 -0
  58. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +79 -0
  59. package/dist/mixins/mixin-field/mixin-field.d.ts +145 -0
  60. package/dist/mixins/mixin-field/mixin-field.js +423 -0
  61. package/dist/utils/anchor-element/anchor-element.d.ts +23 -0
  62. package/dist/utils/anchor-element/anchor-element.js +39 -0
  63. package/dist/utils/capture-pointer/capture-pointer.d.ts +22 -0
  64. package/dist/utils/capture-pointer/capture-pointer.js +30 -0
  65. package/dist/utils/closest-instance/closest-instance.d.ts +25 -0
  66. package/dist/utils/closest-instance/closest-instance.js +34 -0
  67. package/dist/utils/create-sheet/create-sheet.d.ts +16 -0
  68. package/dist/utils/create-sheet/create-sheet.js +16 -0
  69. package/dist/utils/custom-element/custom-element.d.ts +26 -0
  70. package/dist/utils/custom-element/custom-element.js +114 -0
  71. package/dist/utils/draw-icon/draw-icon.d.ts +38 -0
  72. package/dist/utils/draw-icon/draw-icon.js +39 -0
  73. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +56 -0
  74. package/dist/utils/draw-tooltip/draw-tooltip.js +89 -0
  75. package/dist/utils/elapsed-time/elapsed-time.d.ts +10 -0
  76. package/dist/utils/elapsed-time/elapsed-time.js +117 -0
  77. package/dist/utils/focusable-elements/focusable-elements.d.ts +40 -0
  78. package/dist/utils/focusable-elements/focusable-elements.js +88 -0
  79. package/dist/utils/format-numbers/format-numbers.d.ts +1 -0
  80. package/dist/utils/format-numbers/format-numbers.js +29 -0
  81. package/dist/utils/host-aria/host-aria.d.ts +38 -0
  82. package/dist/utils/host-aria/host-aria.js +41 -0
  83. package/dist/utils/inherit-disabled/inherit-disabled.d.ts +33 -0
  84. package/dist/utils/inherit-disabled/inherit-disabled.js +55 -0
  85. package/dist/utils/line-path/line-path.d.ts +23 -0
  86. package/dist/utils/line-path/line-path.js +107 -0
  87. package/dist/utils/on-unless-false/on-unless-false.d.ts +28 -0
  88. package/dist/utils/on-unless-false/on-unless-false.js +32 -0
  89. package/dist/utils/read-series/read-series.d.ts +78 -0
  90. package/dist/utils/read-series/read-series.js +187 -0
  91. package/dist/utils/relative-time/relative-time.d.ts +24 -0
  92. package/dist/utils/relative-time/relative-time.js +84 -0
  93. package/dist/utils/request-change/request-change.d.ts +48 -0
  94. package/dist/utils/request-change/request-change.js +7 -0
  95. package/dist/utils/resolve-active-item/resolve-active-item.d.ts +28 -0
  96. package/dist/utils/resolve-active-item/resolve-active-item.js +21 -0
  97. package/dist/utils/restore-value/restore-value.d.ts +32 -0
  98. package/dist/utils/restore-value/restore-value.js +84 -0
  99. package/dist/utils/scale/scale.d.ts +23 -0
  100. package/dist/utils/scale/scale.js +32 -0
  101. package/dist/utils/scale/ticks.d.ts +27 -0
  102. package/dist/utils/scale/ticks.js +52 -0
  103. package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +10 -0
  104. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +98 -0
  105. package/dist/utils/slot-filled/slot-filled.d.ts +26 -0
  106. package/dist/utils/slot-filled/slot-filled.js +26 -0
  107. package/dist/utils/tell-disclosure/tell-disclosure.d.ts +51 -0
  108. package/dist/utils/tell-disclosure/tell-disclosure.js +76 -0
  109. package/dist/utils/ticker/ticker.d.ts +8 -0
  110. package/dist/utils/ticker/ticker.js +55 -0
  111. package/dist/utils/tone-marks/tone-marks.d.ts +52 -0
  112. package/dist/utils/tone-marks/tone-marks.js +36 -0
  113. package/dist/utils/value-as/value-as.d.ts +64 -0
  114. package/dist/utils/value-as/value-as.js +105 -0
  115. package/package.json +58 -0
@@ -0,0 +1,39 @@
1
+ /** How far to descend before giving up on finding a box. */
2
+ const DEPTH = 4;
3
+ /**
4
+ * The element a floating surface should open against, given the slot it wraps.
5
+ *
6
+ * **A wrapper with no box is not an anchor.** `display: contents` puts an
7
+ * element's children into the layout and leaves the element itself without a
8
+ * box, so its `getBoundingClientRect()` is `0 0 0 0`. Measured: a tooltip host
9
+ * reports exactly that while the button inside it reports 32 by 32. Anchoring
10
+ * to it opens the surface against the top left of the window, and focusing it
11
+ * on close puts focus nowhere.
12
+ *
13
+ * That is not an edge case, because wrapping is how a tooltip attaches to the
14
+ * thing it describes. Without this, an icon button used as a trigger is the one
15
+ * button on a toolbar that cannot have a tooltip.
16
+ *
17
+ * So this descends past anything that draws nothing, and the first child with a
18
+ * box is the anchor. The wrapper is transparent by construction, so the thing
19
+ * with the box is the thing the reader is pointing at.
20
+ *
21
+ * Bounded rather than a walk to the leaves: a chain of box-less wrappers is
22
+ * already unusual, and descending without a limit through a tree this library
23
+ * does not own is worse than stopping and anchoring to something imperfect.
24
+ */
25
+ export const anchorElement = (slot, fallback) => {
26
+ const assigned = slot?.assignedElements({ flatten: true })[0];
27
+ if (!assigned) {
28
+ return fallback;
29
+ }
30
+ let element = assigned;
31
+ for (let depth = 0; depth < DEPTH; depth += 1) {
32
+ const child = element.firstElementChild;
33
+ if (!child || element.getClientRects().length > 0) {
34
+ return element;
35
+ }
36
+ element = child;
37
+ }
38
+ return element;
39
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Takes the pointer for a drag, and carries on if the platform refuses.
3
+ *
4
+ * **Capture is an enhancement, and the drag has to survive losing it.** What it
5
+ * buys is events continuing to arrive once the pointer leaves the target, which
6
+ * matters because a thin divider is easy to outrun. What it costs, called bare,
7
+ * is the rest of the handler: `setPointerCapture` throws `NotFoundError` when
8
+ * the id names no active pointer, and every one of these calls sits above the
9
+ * lines that set the dragging flag and call `preventDefault`. So a refusal did
10
+ * not degrade the drag, it canceled it.
11
+ *
12
+ * That is reachable outside a test: a pointer released between the event being
13
+ * queued and the handler running is gone by the time this is called, and a
14
+ * synthetic `pointerdown` has no active pointer at all. The browser spec run
15
+ * reported four of those as uncaught page errors, and three splitter specs had
16
+ * each stubbed the method to get past it, which is the shape of a workaround
17
+ * that has been in place long enough to read as setup.
18
+ *
19
+ * Returns whether it took, for the callers that then ask `hasPointerCapture`
20
+ * before believing a move belongs to them.
21
+ */
22
+ export declare const capturePointer: (target: Element, pointerId: number) => boolean;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Takes the pointer for a drag, and carries on if the platform refuses.
3
+ *
4
+ * **Capture is an enhancement, and the drag has to survive losing it.** What it
5
+ * buys is events continuing to arrive once the pointer leaves the target, which
6
+ * matters because a thin divider is easy to outrun. What it costs, called bare,
7
+ * is the rest of the handler: `setPointerCapture` throws `NotFoundError` when
8
+ * the id names no active pointer, and every one of these calls sits above the
9
+ * lines that set the dragging flag and call `preventDefault`. So a refusal did
10
+ * not degrade the drag, it canceled it.
11
+ *
12
+ * That is reachable outside a test: a pointer released between the event being
13
+ * queued and the handler running is gone by the time this is called, and a
14
+ * synthetic `pointerdown` has no active pointer at all. The browser spec run
15
+ * reported four of those as uncaught page errors, and three splitter specs had
16
+ * each stubbed the method to get past it, which is the shape of a workaround
17
+ * that has been in place long enough to read as setup.
18
+ *
19
+ * Returns whether it took, for the callers that then ask `hasPointerCapture`
20
+ * before believing a move belongs to them.
21
+ */
22
+ export const capturePointer = (target, pointerId) => {
23
+ try {
24
+ target.setPointerCapture(pointerId);
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The nearest ancestor that is an instance of a class, crossing shadow roots.
3
+ *
4
+ * **Identity rather than tag name, and the rule already existed.** `Accordion`'s
5
+ * root states it two files from a violation of it: "Items are matched with
6
+ * `instanceof`, never by tag name. Matching `helix-accordion-item` would tie
7
+ * this tier to the styled one." Nine headless families resolve their
8
+ * relationships that way; two did it by tag, and both named `helix-*` tags that
9
+ * only `@fusion.dev/helix` registers, which is a headless package depending on a
10
+ * styled one by string.
11
+ *
12
+ * The consequence is silent rather than loud. Subclassing a headless element under
13
+ * a different tag is exactly what the headless tier exists to allow, and it is
14
+ * what `helix/styled` itself does. Under a third tag the lookup returned `null`,
15
+ * so an accordion's `mode` was ignored and every section behaved as `multiple`
16
+ * with no error anywhere.
17
+ *
18
+ * **`closest()` alone is not enough**, which is why this walks rather than
19
+ * delegating. `closest()` stops at a shadow boundary: a component slotted into
20
+ * another component's shadow root has a `parentNode` chain that ends at a
21
+ * `ShadowRoot`, and `closest` will not step across it. Following `host` is what
22
+ * makes a nested arrangement resolve, and nesting through a slot is the
23
+ * ordinary case for anything composable.
24
+ */
25
+ export declare const closestInstance: <T extends Element>(from: Element | null | undefined, Ctor: abstract new (...args: never[]) => T) => T | null;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The nearest ancestor that is an instance of a class, crossing shadow roots.
3
+ *
4
+ * **Identity rather than tag name, and the rule already existed.** `Accordion`'s
5
+ * root states it two files from a violation of it: "Items are matched with
6
+ * `instanceof`, never by tag name. Matching `helix-accordion-item` would tie
7
+ * this tier to the styled one." Nine headless families resolve their
8
+ * relationships that way; two did it by tag, and both named `helix-*` tags that
9
+ * only `@fusion.dev/helix` registers, which is a headless package depending on a
10
+ * styled one by string.
11
+ *
12
+ * The consequence is silent rather than loud. Subclassing a headless element under
13
+ * a different tag is exactly what the headless tier exists to allow, and it is
14
+ * what `helix/styled` itself does. Under a third tag the lookup returned `null`,
15
+ * so an accordion's `mode` was ignored and every section behaved as `multiple`
16
+ * with no error anywhere.
17
+ *
18
+ * **`closest()` alone is not enough**, which is why this walks rather than
19
+ * delegating. `closest()` stops at a shadow boundary: a component slotted into
20
+ * another component's shadow root has a `parentNode` chain that ends at a
21
+ * `ShadowRoot`, and `closest` will not step across it. Following `host` is what
22
+ * makes a nested arrangement resolve, and nesting through a slot is the
23
+ * ordinary case for anything composable.
24
+ */
25
+ export const closestInstance = (from, Ctor) => {
26
+ let node = from ?? null;
27
+ while (node) {
28
+ if (node instanceof Ctor) {
29
+ return node;
30
+ }
31
+ node = node.parentNode instanceof ShadowRoot ? node.parentNode.host : node.parentNode;
32
+ }
33
+ return null;
34
+ };
@@ -0,0 +1,16 @@
1
+ import { type CSSResult } from 'lit';
2
+ /**
3
+ * Builds a stylesheet from CSS authored in a `.css` file.
4
+ *
5
+ * Component styles live in real `.css` files rather than `css` tagged
6
+ * templates, so they get CSS tooling: syntax highlighting, formatters, the
7
+ * token linter, and a diff that reads as CSS. `bun run styles` compiles each
8
+ * one into a sibling `.css.ts` that calls this.
9
+ *
10
+ * The `unsafe` in `unsafeCSS` warns about interpolating untrusted input into a
11
+ * stylesheet. That is not what happens here: the argument is a build-time
12
+ * constant read from a file in this repo, never a runtime value. Wrapping it
13
+ * keeps the warning where it belongs, on `unsafeCSS` itself, and gives one seam
14
+ * to change if constructable stylesheets become the better primitive.
15
+ */
16
+ export declare const createSheet: (cssText: string) => CSSResult;
@@ -0,0 +1,16 @@
1
+ import { unsafeCSS } from 'lit';
2
+ /**
3
+ * Builds a stylesheet from CSS authored in a `.css` file.
4
+ *
5
+ * Component styles live in real `.css` files rather than `css` tagged
6
+ * templates, so they get CSS tooling: syntax highlighting, formatters, the
7
+ * token linter, and a diff that reads as CSS. `bun run styles` compiles each
8
+ * one into a sibling `.css.ts` that calls this.
9
+ *
10
+ * The `unsafe` in `unsafeCSS` warns about interpolating untrusted input into a
11
+ * stylesheet. That is not what happens here: the argument is a build-time
12
+ * constant read from a file in this repo, never a runtime value. Wrapping it
13
+ * keeps the warning where it belongs, on `unsafeCSS` itself, and gives one seam
14
+ * to change if constructable stylesheets become the better primitive.
15
+ */
16
+ export const createSheet = (cssText) => unsafeCSS(cssText);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Registers a custom element, once.
3
+ *
4
+ * A drop-in replacement for Lit's `@customElement` that does not throw when the
5
+ * tag is already registered.
6
+ *
7
+ * The duplicate is not hypothetical. Next.js evaluates a module in both the
8
+ * server and client graphs, so a component imported by a server component and a
9
+ * client component runs its registration twice against one registry, and
10
+ * `customElements.define` throws:
11
+ *
12
+ * > Failed to execute 'define': the name "helix-card" has already been used
13
+ *
14
+ * That failure takes down the whole prerender, and it appears only once an app
15
+ * mixes server and client usage of the same component, which is exactly what a
16
+ * real app does.
17
+ *
18
+ * The analyzer detects `@customElement` **by decorator name**, not by where it
19
+ * is imported from, so the manifest still records `tagName` and nothing
20
+ * downstream changes.
21
+ */
22
+ /** Every tag registered twice by different classes, in registration order. */
23
+ export declare const duplicateRegistrations: () => string[];
24
+ /** Every tag registered through `customElement`, whoever imported it first. */
25
+ export declare const definedTags: () => string[];
26
+ export declare const customElement: (tagName: string) => (target: CustomElementConstructor) => void;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Registers a custom element, once.
3
+ *
4
+ * A drop-in replacement for Lit's `@customElement` that does not throw when the
5
+ * tag is already registered.
6
+ *
7
+ * The duplicate is not hypothetical. Next.js evaluates a module in both the
8
+ * server and client graphs, so a component imported by a server component and a
9
+ * client component runs its registration twice against one registry, and
10
+ * `customElements.define` throws:
11
+ *
12
+ * > Failed to execute 'define': the name "helix-card" has already been used
13
+ *
14
+ * That failure takes down the whole prerender, and it appears only once an app
15
+ * mixes server and client usage of the same component, which is exactly what a
16
+ * real app does.
17
+ *
18
+ * The analyzer detects `@customElement` **by decorator name**, not by where it
19
+ * is imported from, so the manifest still records `tagName` and nothing
20
+ * downstream changes.
21
+ */
22
+ /**
23
+ * Tags already reported, so a page holding duplicates says each one once rather
24
+ * than once per module that tried.
25
+ */
26
+ const reported = new Set();
27
+ /**
28
+ * The tags this page registered twice, for a reader that is not a person.
29
+ *
30
+ * **The warning below is the right thing and it is not a readable output.** It
31
+ * fires at the moment of the second registration, which is the best possible
32
+ * position and better than anything a later pass can do, and it goes to a
33
+ * console: a CI job cannot see it, and a developer whose console already has
34
+ * forty lines in it will not. `@fusion.dev/helix-diagnostics` reads this instead, so the
35
+ * same fault reaches a structured report without a second detector guessing at
36
+ * it from the outside. Detect once, at the source; surface twice.
37
+ */
38
+ const duplicates = new Set();
39
+ /** Every tag registered twice by different classes, in registration order. */
40
+ export const duplicateRegistrations = () => [...duplicates];
41
+ /**
42
+ * Every tag this decorator has defined, in definition order.
43
+ *
44
+ * **Recorded here because here is the only place that sees all of them once.** A
45
+ * spec wanting "every element this package defines" reached for patching
46
+ * `customElements.define` around an import of the barrel, and that answers
47
+ * correctly only for the first importer in the process: a second import is a
48
+ * module-cache hit and defines nothing, so any spec that touched a component
49
+ * first silently shrank the list. Measured when one did, and what it reported
50
+ * was a floor count, 164 against 250, which names nothing about the cause.
51
+ *
52
+ * Not `registerElementEvents`, which was tried and is a different set: the
53
+ * generated map holds the tags that *dispatch* something, so it came out 68
54
+ * against 90. Definition is the question, so the definition site is the answer.
55
+ */
56
+ const defined = [];
57
+ /** Every tag registered through `customElement`, whoever imported it first. */
58
+ export const definedTags = () => [...defined];
59
+ /**
60
+ * Says so when a second, different class asks for a tag that is already taken.
61
+ *
62
+ * **Returning quietly is right and saying nothing is not.** The registry keeps
63
+ * the first constructor, so on a page carrying two copies of Helix every
64
+ * `<helix-button>` is the first copy's class whatever the second copy exports.
65
+ * Nothing fails: the element renders, behaves, and answers its own API. What
66
+ * breaks is identity, `el instanceof HelixButton` returning `false` against an
67
+ * element that is visibly a button, and a version's fixes being absent from
68
+ * elements the consumer believes are on that version. Both are hours of
69
+ * debugging with no error to search for, which is the worst shape a library
70
+ * failure can take.
71
+ *
72
+ * **Only in a browser, which is the line that keeps this quiet where the guard
73
+ * was written for.** The prerender case above is a server one: Lit's SSR shim
74
+ * provides `customElements` and `document` and leaves `window` undefined, so
75
+ * `typeof window` separates a real page from a render pass exactly. Measured
76
+ * against `@lit-labs/ssr`'s global DOM shim rather than assumed, because a
77
+ * diagnostic that fires during a build is a diagnostic somebody turns off.
78
+ *
79
+ * **Different constructors, not merely a second call.** A module evaluated
80
+ * twice in one graph produces two class objects and is a genuine duplicate; the
81
+ * same class arriving twice through two import specifiers is not, and has
82
+ * nothing to report.
83
+ *
84
+ * No `throw`. The first definition is already live and elements are already
85
+ * upgraded against it, so there is nothing left to refuse and a lot to take
86
+ * down.
87
+ */
88
+ const warnDuplicate = (tagName) => {
89
+ if (typeof window === 'undefined' || reported.has(tagName)) {
90
+ return;
91
+ }
92
+ reported.add(tagName);
93
+ duplicates.add(tagName);
94
+ console.warn(`<${tagName}> is already registered by a different class, so this page holds two copies of ` +
95
+ 'Helix. The first one keeps the tag: every element of this name is built from it, whatever ' +
96
+ 'the second copy exports, so `instanceof` against the second copy is false and its fixes are ' +
97
+ 'not in the DOM. Two dependencies asking for different versions is the usual cause, and a ' +
98
+ 'bundler shipping the same version twice is the other. Deduplicate the dependency.');
99
+ };
100
+ export const customElement = (tagName) => (target) => {
101
+ // Lit SSR shims the registry, but a bare Node context may have none.
102
+ if (typeof customElements === 'undefined') {
103
+ return;
104
+ }
105
+ const existing = customElements.get(tagName);
106
+ if (existing) {
107
+ if (existing !== target) {
108
+ warnDuplicate(tagName);
109
+ }
110
+ return;
111
+ }
112
+ customElements.define(tagName, target);
113
+ defined.push(tagName);
114
+ };
@@ -0,0 +1,38 @@
1
+ import type { HelixIcon } from '@fusion.dev/helix-icons';
2
+ import { type TemplateResult } from 'lit';
3
+ /** Where a glyph goes, which is the one attribute that varies. */
4
+ export interface DrawIconOptions {
5
+ /**
6
+ * A `slot`, for a glyph being handed to another component.
7
+ */
8
+ slot?: string;
9
+ }
10
+ /**
11
+ * Draws an icon that was imported by name, rather than looked up by string.
12
+ *
13
+ * **`<helix-icon name="play">` indexes the whole icon set at runtime.** Its
14
+ * render is `icons[this.name]`, and no bundler can shake a dynamic index, so
15
+ * every consumer of any component that renders one ships all of the generated
16
+ * path data: measured at 50,814 bytes raw and 15,010 gzipped for 102 icons, and
17
+ * growing about 147 gzipped bytes per icon added. A named import ships one path
18
+ * and is checked when the package compiles rather than when `check:icons` runs.
19
+ *
20
+ * That element still earns its place as *public* API, where a consumer writes a
21
+ * name in markup and a lookup is the only thing that can work. What it should
22
+ * not be is how this library draws its own marks.
23
+ *
24
+ * **There is no `part` option, and there cannot be.** A part name behind an
25
+ * interpolation is a part the manifest cannot see, so `check:selectors` reports
26
+ * the component's own stylesheet as matching nothing and the part stops being
27
+ * API. `Trend` needs one and therefore writes its `<svg>` out with a literal
28
+ * `part="icon"`, which is the honest cost: a helper can carry everything except
29
+ * the thing static analysis has to read.
30
+ *
31
+ * `data-glyph` rather than a class, because the sizing lives in
32
+ * `helix/styled/src/icon/glyph.css` and a component's own `--icon-size`
33
+ * declarations have to keep working untouched: the variable was read by
34
+ * `helix-icon`, and now it is read by the rule that sheet carries. Without it
35
+ * every one of those declarations would go quietly dead and every mark in six
36
+ * families would change size.
37
+ */
38
+ export declare const drawIcon: (icon: HelixIcon, options?: DrawIconOptions) => TemplateResult;
@@ -0,0 +1,39 @@
1
+ import { html, nothing } from 'lit';
2
+ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
3
+ /**
4
+ * Draws an icon that was imported by name, rather than looked up by string.
5
+ *
6
+ * **`<helix-icon name="play">` indexes the whole icon set at runtime.** Its
7
+ * render is `icons[this.name]`, and no bundler can shake a dynamic index, so
8
+ * every consumer of any component that renders one ships all of the generated
9
+ * path data: measured at 50,814 bytes raw and 15,010 gzipped for 102 icons, and
10
+ * growing about 147 gzipped bytes per icon added. A named import ships one path
11
+ * and is checked when the package compiles rather than when `check:icons` runs.
12
+ *
13
+ * That element still earns its place as *public* API, where a consumer writes a
14
+ * name in markup and a lookup is the only thing that can work. What it should
15
+ * not be is how this library draws its own marks.
16
+ *
17
+ * **There is no `part` option, and there cannot be.** A part name behind an
18
+ * interpolation is a part the manifest cannot see, so `check:selectors` reports
19
+ * the component's own stylesheet as matching nothing and the part stops being
20
+ * API. `Trend` needs one and therefore writes its `<svg>` out with a literal
21
+ * `part="icon"`, which is the honest cost: a helper can carry everything except
22
+ * the thing static analysis has to read.
23
+ *
24
+ * `data-glyph` rather than a class, because the sizing lives in
25
+ * `helix/styled/src/icon/glyph.css` and a component's own `--icon-size`
26
+ * declarations have to keep working untouched: the variable was read by
27
+ * `helix-icon`, and now it is read by the rule that sheet carries. Without it
28
+ * every one of those declarations would go quietly dead and every mark in six
29
+ * families would change size.
30
+ */
31
+ export const drawIcon = (icon, options = {}) => html `<svg
32
+ aria-hidden="true"
33
+ data-glyph
34
+ fill="currentColor"
35
+ slot=${options.slot ?? nothing}
36
+ viewBox=${icon.viewBox}
37
+ >
38
+ ${unsafeSVG(icon.content)}
39
+ </svg>`;
@@ -0,0 +1,56 @@
1
+ import { nothing, type TemplateResult } from 'lit';
2
+ import { type StaticValue } from 'lit/static-html.js';
3
+ import type { Row } from '../read-series/read-series.js';
4
+ /** Where a tooltip points, in the plot's own percentages, and which side it prefers. */
5
+ export interface TooltipAt {
6
+ /**
7
+ * The side the bubble prefers. It moves if that side does not fit.
8
+ */
9
+ placement: 'bottom' | 'left' | 'right' | 'top';
10
+ /**
11
+ * How far across the plot the reading is, as a percentage.
12
+ */
13
+ x: string;
14
+ /**
15
+ * How far down the plot the reading is, as a percentage.
16
+ */
17
+ y: string;
18
+ }
19
+ /**
20
+ * What a pointer resting on a reading shows, as `Tooltip` pointed at a mark.
21
+ *
22
+ * **A chart does not draw its own tooltip, and the top layer is why.** `Tooltip`
23
+ * renders `popover="manual"` through `PopoverController`, so it clears every
24
+ * stacking context and every `overflow` on the page. A box drawn by the chart is
25
+ * `position: absolute` inside the plot and is confined to the chart's own
26
+ * context, so it is clipped by, or layered under, whatever sits beside the
27
+ * chart. Escaping that means reimplementing `PopoverController`, which is
28
+ * `Tooltip` with extra steps and a second copy of the hard part.
29
+ *
30
+ * **`for` was already the anchor and needed nothing added.** It resolves through
31
+ * `getRootNode().getElementById()`, and a chart's shadow root *is* a
32
+ * `ShadowRoot`, so a chart names an element inside itself and `Tooltip` finds
33
+ * it. What the chart provides is an invisible mark at the reading's point.
34
+ *
35
+ * **An HTML mark rather than the `<rect>` or `<circle>` already there.**
36
+ * `FloatingController` types its anchor as `HTMLElement` and an SVG element is
37
+ * not one; and a slice's `<circle>` has the bounding box of the whole ring
38
+ * rather than of its arc, so anchoring to it would point at the ring's center
39
+ * for every slice.
40
+ *
41
+ * **And the arrow now follows from placement rather than from a compass.** The
42
+ * chart says which side it prefers and `FloatingController` moves it if that
43
+ * side does not fit, which is how a slice at any bearing gets a bubble that
44
+ * points at it. Four hard-coded sides could not express a bearing, which is what
45
+ * made the donut's arrow miss its own ring.
46
+ *
47
+ * **It shows the table's text and not the chart's numbers.** `$128k` is what the
48
+ * cell says and `128000` is what the drawing measured. Every cell of the row is
49
+ * here, including columns nothing was plotted from, because a note beside the
50
+ * figures is most of the reason to want a tooltip.
51
+ *
52
+ * **Hidden from assistive technology, on purpose.** The table is the accessible
53
+ * representation and the drawing is `aria-hidden`; a reader on a keyboard gets
54
+ * the table, which they can reach, rather than a tooltip which they cannot.
55
+ */
56
+ export declare const drawTooltip: (rows: Row[], at: number, tag: StaticValue, where?: TooltipAt) => TemplateResult | typeof nothing;
@@ -0,0 +1,89 @@
1
+ import { nothing } from 'lit';
2
+ import { styleMap } from 'lit/directives/style-map.js';
3
+ import { html } from 'lit/static-html.js';
4
+ import { seriesIndex } from '../../mixins/mixin-chart-source/mixin-chart-source.js';
5
+ /**
6
+ * What a pointer resting on a reading shows, as `Tooltip` pointed at a mark.
7
+ *
8
+ * **A chart does not draw its own tooltip, and the top layer is why.** `Tooltip`
9
+ * renders `popover="manual"` through `PopoverController`, so it clears every
10
+ * stacking context and every `overflow` on the page. A box drawn by the chart is
11
+ * `position: absolute` inside the plot and is confined to the chart's own
12
+ * context, so it is clipped by, or layered under, whatever sits beside the
13
+ * chart. Escaping that means reimplementing `PopoverController`, which is
14
+ * `Tooltip` with extra steps and a second copy of the hard part.
15
+ *
16
+ * **`for` was already the anchor and needed nothing added.** It resolves through
17
+ * `getRootNode().getElementById()`, and a chart's shadow root *is* a
18
+ * `ShadowRoot`, so a chart names an element inside itself and `Tooltip` finds
19
+ * it. What the chart provides is an invisible mark at the reading's point.
20
+ *
21
+ * **An HTML mark rather than the `<rect>` or `<circle>` already there.**
22
+ * `FloatingController` types its anchor as `HTMLElement` and an SVG element is
23
+ * not one; and a slice's `<circle>` has the bounding box of the whole ring
24
+ * rather than of its arc, so anchoring to it would point at the ring's center
25
+ * for every slice.
26
+ *
27
+ * **And the arrow now follows from placement rather than from a compass.** The
28
+ * chart says which side it prefers and `FloatingController` moves it if that
29
+ * side does not fit, which is how a slice at any bearing gets a bubble that
30
+ * points at it. Four hard-coded sides could not express a bearing, which is what
31
+ * made the donut's arrow miss its own ring.
32
+ *
33
+ * **It shows the table's text and not the chart's numbers.** `$128k` is what the
34
+ * cell says and `128000` is what the drawing measured. Every cell of the row is
35
+ * here, including columns nothing was plotted from, because a note beside the
36
+ * figures is most of the reason to want a tooltip.
37
+ *
38
+ * **Hidden from assistive technology, on purpose.** The table is the accessible
39
+ * representation and the drawing is `aria-hidden`; a reader on a keyboard gets
40
+ * the table, which they can reach, rather than a tooltip which they cannot.
41
+ */
42
+ export const drawTooltip = (rows, at, tag, where) => {
43
+ const row = rows[at];
44
+ if (!row || rows.length === 0) {
45
+ return nothing;
46
+ }
47
+ const slot = 100 / rows.length;
48
+ const place = where ?? {
49
+ placement: 'top',
50
+ x: `${at * slot + slot / 2}%`,
51
+ y: '0%',
52
+ };
53
+ /* The mark and the bubble are siblings, and the id is fixed rather than
54
+ generated: it is scoped to this chart's shadow root, where it is the only
55
+ one, so nothing can collide with it and nothing has to be threaded through
56
+ a render to stay in step. */
57
+ return html `<div
58
+ id="helix-chart-mark"
59
+ part="mark"
60
+ style=${styleMap({ insetBlockStart: place.y, insetInlineStart: place.x })}
61
+ ></div>
62
+ <${tag}
63
+ exportparts="tooltip"
64
+ for="helix-chart-mark"
65
+ open
66
+ part="tooltip-anchor"
67
+ placement=${place.placement}
68
+ within="helix-chart-plot"
69
+ >
70
+ <div aria-hidden="true" slot="content">
71
+ <span part="tooltip-label">${row.label}</span>
72
+ ${row.cells.map((cell, index) =>
73
+ /* A palette color only where something was plotted in it.
74
+ *
75
+ * A cell with no number is a column no chart drew: a note, a status,
76
+ * a name. Given `data-series` anyway it takes a series color and the
77
+ * stylesheet draws a dot, so a row reading `Note Outage` arrived
78
+ * wearing series three's green while the legend beside the chart said
79
+ * series three was something else. */
80
+ html `<span
81
+ data-series=${cell.value === undefined ? nothing : seriesIndex(index)}
82
+ part="tooltip-cell"
83
+ >
84
+ <span part="tooltip-name">${cell.name}</span>
85
+ <span part="tooltip-value">${cell.text}</span>
86
+ </span>`)}
87
+ </div>
88
+ </${tag}>`;
89
+ };
@@ -0,0 +1,10 @@
1
+ import type { Relative } from '../relative-time/relative-time.js';
2
+ /**
3
+ * A formatter for one locale, kept.
4
+ *
5
+ * `now` is a parameter rather than read from the clock, for the reason
6
+ * `relativeTime` gives: a function that reads `Date.now()` internally can only
7
+ * be tested by waiting or by replacing a global, and both produce specs that
8
+ * fail on a slow machine.
9
+ */
10
+ export declare const elapsedTime: (locale: string) => ((from: Date, now: Date) => Relative);