@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.
- package/LICENSE +21 -0
- package/custom-elements.json +8005 -0
- package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +67 -0
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +146 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +79 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +140 -0
- package/dist/controllers/command-controller/command-controller.d.ts +83 -0
- package/dist/controllers/command-controller/command-controller.js +93 -0
- package/dist/controllers/copy-controller/copy-controller.d.ts +58 -0
- package/dist/controllers/copy-controller/copy-controller.js +128 -0
- package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.d.ts +76 -0
- package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +67 -0
- package/dist/controllers/dismiss-controller/dismiss-controller.d.ts +58 -0
- package/dist/controllers/dismiss-controller/dismiss-controller.js +165 -0
- package/dist/controllers/drilldown-controller/drilldown-controller.d.ts +127 -0
- package/dist/controllers/drilldown-controller/drilldown-controller.js +171 -0
- package/dist/controllers/floating-controller/floating-controller.d.ts +123 -0
- package/dist/controllers/floating-controller/floating-controller.js +227 -0
- package/dist/controllers/focus-trap-controller/focus-trap-controller.d.ts +26 -0
- package/dist/controllers/focus-trap-controller/focus-trap-controller.js +93 -0
- package/dist/controllers/indicator-controller/indicator-controller.d.ts +97 -0
- package/dist/controllers/indicator-controller/indicator-controller.js +164 -0
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +113 -0
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +117 -0
- package/dist/controllers/popover-controller/popover-controller.d.ts +132 -0
- package/dist/controllers/popover-controller/popover-controller.js +150 -0
- package/dist/controllers/source-controller/source-controller.d.ts +55 -0
- package/dist/controllers/source-controller/source-controller.js +94 -0
- package/dist/controllers/token-color-controller/token-color-controller.d.ts +63 -0
- package/dist/controllers/token-color-controller/token-color-controller.js +105 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +127 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.js +216 -0
- package/dist/events/element-events.d.ts +4 -0
- package/dist/events/element-events.js +34 -0
- package/dist/events/events.d.ts +743 -0
- package/dist/events/events.js +799 -0
- package/dist/helix-element/helix-element.d.ts +201 -0
- package/dist/helix-element/helix-element.js +695 -0
- package/dist/helix-form-element/helix-form-element.d.ts +152 -0
- package/dist/helix-form-element/helix-form-element.js +194 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +54 -0
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +137 -0
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +140 -0
- package/dist/mixins/mixin-collection/index.d.ts +1 -0
- package/dist/mixins/mixin-collection/index.js +1 -0
- package/dist/mixins/mixin-collection/mixin-collection.d.ts +58 -0
- package/dist/mixins/mixin-collection/mixin-collection.js +154 -0
- package/dist/mixins/mixin-controller-active/mixin-controller-active.d.ts +53 -0
- package/dist/mixins/mixin-controller-active/mixin-controller-active.js +155 -0
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +54 -0
- package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +157 -0
- package/dist/mixins/mixin-controller-open/mixin-controller-open.d.ts +49 -0
- package/dist/mixins/mixin-controller-open/mixin-controller-open.js +176 -0
- package/dist/mixins/mixin-controller-value/mixin-controller-value.d.ts +73 -0
- package/dist/mixins/mixin-controller-value/mixin-controller-value.js +173 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +41 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +79 -0
- package/dist/mixins/mixin-field/mixin-field.d.ts +145 -0
- package/dist/mixins/mixin-field/mixin-field.js +423 -0
- package/dist/utils/anchor-element/anchor-element.d.ts +23 -0
- package/dist/utils/anchor-element/anchor-element.js +39 -0
- package/dist/utils/capture-pointer/capture-pointer.d.ts +22 -0
- package/dist/utils/capture-pointer/capture-pointer.js +30 -0
- package/dist/utils/closest-instance/closest-instance.d.ts +25 -0
- package/dist/utils/closest-instance/closest-instance.js +34 -0
- package/dist/utils/create-sheet/create-sheet.d.ts +16 -0
- package/dist/utils/create-sheet/create-sheet.js +16 -0
- package/dist/utils/custom-element/custom-element.d.ts +26 -0
- package/dist/utils/custom-element/custom-element.js +114 -0
- package/dist/utils/draw-icon/draw-icon.d.ts +38 -0
- package/dist/utils/draw-icon/draw-icon.js +39 -0
- package/dist/utils/draw-tooltip/draw-tooltip.d.ts +56 -0
- package/dist/utils/draw-tooltip/draw-tooltip.js +89 -0
- package/dist/utils/elapsed-time/elapsed-time.d.ts +10 -0
- package/dist/utils/elapsed-time/elapsed-time.js +117 -0
- package/dist/utils/focusable-elements/focusable-elements.d.ts +40 -0
- package/dist/utils/focusable-elements/focusable-elements.js +88 -0
- package/dist/utils/format-numbers/format-numbers.d.ts +1 -0
- package/dist/utils/format-numbers/format-numbers.js +29 -0
- package/dist/utils/host-aria/host-aria.d.ts +38 -0
- package/dist/utils/host-aria/host-aria.js +41 -0
- package/dist/utils/inherit-disabled/inherit-disabled.d.ts +33 -0
- package/dist/utils/inherit-disabled/inherit-disabled.js +55 -0
- package/dist/utils/line-path/line-path.d.ts +23 -0
- package/dist/utils/line-path/line-path.js +107 -0
- package/dist/utils/on-unless-false/on-unless-false.d.ts +28 -0
- package/dist/utils/on-unless-false/on-unless-false.js +32 -0
- package/dist/utils/read-series/read-series.d.ts +78 -0
- package/dist/utils/read-series/read-series.js +187 -0
- package/dist/utils/relative-time/relative-time.d.ts +24 -0
- package/dist/utils/relative-time/relative-time.js +84 -0
- package/dist/utils/request-change/request-change.d.ts +48 -0
- package/dist/utils/request-change/request-change.js +7 -0
- package/dist/utils/resolve-active-item/resolve-active-item.d.ts +28 -0
- package/dist/utils/resolve-active-item/resolve-active-item.js +21 -0
- package/dist/utils/restore-value/restore-value.d.ts +32 -0
- package/dist/utils/restore-value/restore-value.js +84 -0
- package/dist/utils/scale/scale.d.ts +23 -0
- package/dist/utils/scale/scale.js +32 -0
- package/dist/utils/scale/ticks.d.ts +27 -0
- package/dist/utils/scale/ticks.js +52 -0
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +10 -0
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +98 -0
- package/dist/utils/slot-filled/slot-filled.d.ts +26 -0
- package/dist/utils/slot-filled/slot-filled.js +26 -0
- package/dist/utils/tell-disclosure/tell-disclosure.d.ts +51 -0
- package/dist/utils/tell-disclosure/tell-disclosure.js +76 -0
- package/dist/utils/ticker/ticker.d.ts +8 -0
- package/dist/utils/ticker/ticker.js +55 -0
- package/dist/utils/tone-marks/tone-marks.d.ts +52 -0
- package/dist/utils/tone-marks/tone-marks.js +36 -0
- package/dist/utils/value-as/value-as.d.ts +64 -0
- package/dist/utils/value-as/value-as.js +105 -0
- 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);
|