@hypermedia-components/core 0.0.1-alpha.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.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/anchor-fallback.d.ts +57 -0
- package/dist/anchor-fallback.js +198 -0
- package/dist/avatar.d.ts +10 -0
- package/dist/avatar.js +148 -0
- package/dist/calendar.d.ts +9 -0
- package/dist/calendar.js +559 -0
- package/dist/carousel.d.ts +7 -0
- package/dist/carousel.js +238 -0
- package/dist/chart.d.ts +28 -0
- package/dist/chart.js +277 -0
- package/dist/close-dialog.d.ts +28 -0
- package/dist/close-dialog.js +69 -0
- package/dist/close-popover.d.ts +23 -0
- package/dist/close-popover.js +63 -0
- package/dist/combobox.d.ts +9 -0
- package/dist/combobox.js +503 -0
- package/dist/command.d.ts +22 -0
- package/dist/command.js +407 -0
- package/dist/confirm.d.ts +28 -0
- package/dist/confirm.js +153 -0
- package/dist/context-menu.d.ts +9 -0
- package/dist/context-menu.js +188 -0
- package/dist/datagrid.d.ts +10 -0
- package/dist/datagrid.js +863 -0
- package/dist/drawer.d.ts +25 -0
- package/dist/drawer.js +202 -0
- package/dist/hc-accordion.css +140 -0
- package/dist/hc-alert.css +54 -0
- package/dist/hc-anchored.css +129 -0
- package/dist/hc-aspect.css +53 -0
- package/dist/hc-avatar.css +128 -0
- package/dist/hc-badge.css +45 -0
- package/dist/hc-breadcrumb.css +108 -0
- package/dist/hc-button-group.css +80 -0
- package/dist/hc-button.css +116 -0
- package/dist/hc-calendar.css +198 -0
- package/dist/hc-card.css +32 -0
- package/dist/hc-carousel.css +117 -0
- package/dist/hc-chart.css +57 -0
- package/dist/hc-checkbox.css +119 -0
- package/dist/hc-collapsible.css +106 -0
- package/dist/hc-combobox.css +167 -0
- package/dist/hc-command.css +162 -0
- package/dist/hc-datagrid.css +406 -0
- package/dist/hc-datepicker.css +158 -0
- package/dist/hc-dialog.css +52 -0
- package/dist/hc-drawer.css +185 -0
- package/dist/hc-empty.css +68 -0
- package/dist/hc-field.css +82 -0
- package/dist/hc-hovercard.css +97 -0
- package/dist/hc-input-group.css +90 -0
- package/dist/hc-input.css +92 -0
- package/dist/hc-inputotp.css +132 -0
- package/dist/hc-item.css +116 -0
- package/dist/hc-kbd.css +55 -0
- package/dist/hc-menu.css +205 -0
- package/dist/hc-menubar.css +53 -0
- package/dist/hc-multicombobox.css +206 -0
- package/dist/hc-navmenu.css +109 -0
- package/dist/hc-pagination.css +65 -0
- package/dist/hc-popover.css +31 -0
- package/dist/hc-progress.css +170 -0
- package/dist/hc-radio.css +111 -0
- package/dist/hc-scroll-area.css +86 -0
- package/dist/hc-select.css +124 -0
- package/dist/hc-separator.css +47 -0
- package/dist/hc-shell.css +259 -0
- package/dist/hc-skeleton.css +91 -0
- package/dist/hc-slider.css +218 -0
- package/dist/hc-spinner.css +52 -0
- package/dist/hc-splitter.css +90 -0
- package/dist/hc-switch.css +165 -0
- package/dist/hc-table.css +57 -0
- package/dist/hc-tabs.css +258 -0
- package/dist/hc-toast.css +135 -0
- package/dist/hc-toggle-group.css +124 -0
- package/dist/hc-toolbar.css +34 -0
- package/dist/hc-tooltip.css +61 -0
- package/dist/hc.a11y.css +151 -0
- package/dist/hc.base.css +34 -0
- package/dist/hc.behaviors.d.ts +31 -0
- package/dist/hc.behaviors.js +115 -0
- package/dist/hc.behaviors.min.js +2 -0
- package/dist/hc.core.css +1134 -0
- package/dist/hc.core.min.css +1 -0
- package/dist/hc.css +9568 -0
- package/dist/hc.htmx.css +50 -0
- package/dist/hc.min.css +1 -0
- package/dist/hc.min.js +2 -0
- package/dist/hc.tokens.color-amber.css +63 -0
- package/dist/hc.tokens.color-emerald.css +63 -0
- package/dist/hc.tokens.color-indigo.css +63 -0
- package/dist/hc.tokens.color-rose.css +63 -0
- package/dist/hc.tokens.core.css +1089 -0
- package/dist/hc.tokens.css +3061 -0
- package/dist/hc.tokens.density-compact.css +50 -0
- package/dist/hc.tokens.density-dense.css +50 -0
- package/dist/hc.tokens.neutral-neutral.css +410 -0
- package/dist/hc.tokens.neutral-slate.css +410 -0
- package/dist/hc.tokens.neutral-stone.css +410 -0
- package/dist/hc.tokens.neutral-zinc.css +410 -0
- package/dist/hc.utilities.css +111 -0
- package/dist/hovercard.d.ts +11 -0
- package/dist/hovercard.js +262 -0
- package/dist/i18n.d.ts +52 -0
- package/dist/i18n.js +100 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +45 -0
- package/dist/inputotp.d.ts +9 -0
- package/dist/inputotp.js +225 -0
- package/dist/macros/confirm-action.d.ts +3 -0
- package/dist/macros/confirm-action.js +97 -0
- package/dist/macros/index.d.ts +4 -0
- package/dist/macros/index.js +16 -0
- package/dist/macros/index.min.js +1 -0
- package/dist/macros/live-search.d.ts +3 -0
- package/dist/macros/live-search.js +99 -0
- package/dist/menu-core.d.ts +30 -0
- package/dist/menu-core.js +166 -0
- package/dist/menu.d.ts +10 -0
- package/dist/menu.js +201 -0
- package/dist/menubar.d.ts +10 -0
- package/dist/menubar.js +232 -0
- package/dist/multicombobox.d.ts +10 -0
- package/dist/multicombobox.js +499 -0
- package/dist/navmenu.d.ts +9 -0
- package/dist/navmenu.js +336 -0
- package/dist/password-toggle.d.ts +8 -0
- package/dist/password-toggle.js +97 -0
- package/dist/popover.d.ts +9 -0
- package/dist/popover.js +133 -0
- package/dist/remote-dialog.d.ts +23 -0
- package/dist/remote-dialog.js +63 -0
- package/dist/shell.d.ts +11 -0
- package/dist/shell.js +260 -0
- package/dist/slider.d.ts +9 -0
- package/dist/slider.js +81 -0
- package/dist/splitter.d.ts +9 -0
- package/dist/splitter.js +238 -0
- package/dist/submenu.d.ts +20 -0
- package/dist/submenu.js +232 -0
- package/dist/tabs.d.ts +12 -0
- package/dist/tabs.js +333 -0
- package/dist/toast.d.ts +85 -0
- package/dist/toast.js +295 -0
- package/dist/toggle-group.d.ts +9 -0
- package/dist/toggle-group.js +271 -0
- package/dist/toolbar.d.ts +9 -0
- package/dist/toolbar.js +223 -0
- package/dist/tooltip.d.ts +9 -0
- package/dist/tooltip.js +197 -0
- package/dist/validation.d.ts +9 -0
- package/dist/validation.js +138 -0
- package/package.json +105 -0
- package/scripts/token-transform.mjs +309 -0
- package/src/tokens/README.md +10 -0
- package/src/tokens/color.amber.tokens.json +20 -0
- package/src/tokens/color.default.tokens.json +20 -0
- package/src/tokens/color.emerald.tokens.json +20 -0
- package/src/tokens/color.indigo.tokens.json +20 -0
- package/src/tokens/color.rose.tokens.json +20 -0
- package/src/tokens/component.tokens.json +951 -0
- package/src/tokens/density.comfortable.tokens.json +69 -0
- package/src/tokens/density.compact.tokens.json +69 -0
- package/src/tokens/density.dense.tokens.json +69 -0
- package/src/tokens/neutral.neutral.dark.tokens.json +71 -0
- package/src/tokens/neutral.neutral.tokens.json +67 -0
- package/src/tokens/neutral.slate.dark.tokens.json +71 -0
- package/src/tokens/neutral.slate.tokens.json +67 -0
- package/src/tokens/neutral.stone.dark.tokens.json +71 -0
- package/src/tokens/neutral.stone.tokens.json +67 -0
- package/src/tokens/neutral.zinc.dark.tokens.json +71 -0
- package/src/tokens/neutral.zinc.tokens.json +67 -0
- package/src/tokens/primitive.tokens.json +206 -0
- package/src/tokens/semantic.tokens.json +106 -0
- package/src/tokens/theme.dark.tokens.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ingcreators
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# @hypermedia-components/core
|
|
2
|
+
|
|
3
|
+
Semantic CSS components, DTCG-token-based themes, htmx-friendly recipes, small behavior helpers, and optional Light DOM macros.
|
|
4
|
+
|
|
5
|
+
See the [project documentation](https://ingcreators.com/hypermedia-components/) for usage.
|
|
6
|
+
|
|
7
|
+
## Layout
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
src/
|
|
11
|
+
css/ Component, base, layer, and htmx CSS
|
|
12
|
+
js/ Behavior helpers (vanilla ESM)
|
|
13
|
+
macros/ Optional Light DOM custom-element macros
|
|
14
|
+
tokens/ DTCG token sources (primitive, semantic, component, theme, density)
|
|
15
|
+
dist/ Build output (hc.css, hc.tokens.css, hc.behaviors.js, hc.macros.js, ...)
|
|
16
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature-detect CSS Anchor Positioning. Returns false where `CSS.supports`
|
|
3
|
+
* is missing (e.g. jsdom), which routes those environments through the
|
|
4
|
+
* fallback too.
|
|
5
|
+
*
|
|
6
|
+
* @returns {boolean}
|
|
7
|
+
*/
|
|
8
|
+
export function supportsAnchorPositioning(): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Read a floating element's `data-side` / `data-align` attributes into the
|
|
11
|
+
* `{ side, align }` options {@link positionFloating} understands, falling
|
|
12
|
+
* back to the component's default when an attribute is absent or invalid.
|
|
13
|
+
* The CSS Anchor Positioning path keys off the same attributes
|
|
14
|
+
* (`position-area`), so both paths place the element identically.
|
|
15
|
+
*
|
|
16
|
+
* @param {Element} el
|
|
17
|
+
* @param {{ side?: string, align?: string }} [fallback]
|
|
18
|
+
* @returns {{ side: string, align: 'start'|'center'|'end' }}
|
|
19
|
+
*/
|
|
20
|
+
export function readSideAlign(el: Element, fallback?: {
|
|
21
|
+
side?: string;
|
|
22
|
+
align?: string;
|
|
23
|
+
}): {
|
|
24
|
+
side: string;
|
|
25
|
+
align: "start" | "center" | "end";
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Position `floating` next to `anchor`, once.
|
|
29
|
+
*
|
|
30
|
+
* @param {HTMLElement} floating the popover to place (already in the top layer)
|
|
31
|
+
* @param {HTMLElement} anchor the trigger to place it against
|
|
32
|
+
* @param {object} [opts]
|
|
33
|
+
* @param {'block-end'|'block-start'|'inline-end'|'inline-start'} [opts.side='block-end']
|
|
34
|
+
* primary side. The block sides drop the floating element below / above the
|
|
35
|
+
* anchor (dropdown); the inline sides place it to the right / left
|
|
36
|
+
* (submenu), aligning their block-start edges.
|
|
37
|
+
* @param {'start'|'center'} [opts.align='start'] inline-axis alignment (block sides only)
|
|
38
|
+
* @param {number} [opts.gap=4] distance from the anchor, px
|
|
39
|
+
* @param {boolean} [opts.matchWidth=false] set min-width to the anchor width
|
|
40
|
+
*/
|
|
41
|
+
export function positionFloating(floating: HTMLElement, anchor: HTMLElement, opts?: {
|
|
42
|
+
side?: "block-end" | "block-start" | "inline-end" | "inline-start";
|
|
43
|
+
align?: "start" | "center";
|
|
44
|
+
gap?: number;
|
|
45
|
+
matchWidth?: boolean;
|
|
46
|
+
}): void;
|
|
47
|
+
/**
|
|
48
|
+
* Position `floating` against `anchor` now and keep it tracking while open.
|
|
49
|
+
* Re-runs on scroll (in any ancestor, via capture) and on resize.
|
|
50
|
+
*
|
|
51
|
+
* @param {HTMLElement} floating
|
|
52
|
+
* @param {HTMLElement} anchor
|
|
53
|
+
* @param {object} [opts] see {@link positionFloating}
|
|
54
|
+
* @returns {() => void} cleanup — removes the listeners and clears the inline
|
|
55
|
+
* styles. Idempotent.
|
|
56
|
+
*/
|
|
57
|
+
export function trackFloating(floating: HTMLElement, anchor: HTMLElement, opts?: object): () => void;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Shared fallback positioning for popovers when CSS Anchor Positioning is
|
|
2
|
+
// unavailable (e.g. current Firefox). The components position their popovers
|
|
3
|
+
// with CSS anchor positioning + `position-try-fallbacks`; in engines without
|
|
4
|
+
// it, a `[popover]` would otherwise sit centred in the viewport. This module
|
|
5
|
+
// mirrors that CSS behaviour in JS: place the floating element next to its
|
|
6
|
+
// anchor, flip on overflow, clamp to the viewport, and keep it tracking on
|
|
7
|
+
// scroll / resize until cleaned up.
|
|
8
|
+
//
|
|
9
|
+
// Geometry is set with PHYSICAL `top` / `left` (computed from
|
|
10
|
+
// getBoundingClientRect, which is physical), so it is correct under both LTR
|
|
11
|
+
// and RTL; inline-axis *alignment* is direction-aware.
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Feature-detect CSS Anchor Positioning. Returns false where `CSS.supports`
|
|
15
|
+
* is missing (e.g. jsdom), which routes those environments through the
|
|
16
|
+
* fallback too.
|
|
17
|
+
*
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
export function supportsAnchorPositioning() {
|
|
21
|
+
try {
|
|
22
|
+
return (
|
|
23
|
+
typeof CSS !== 'undefined' &&
|
|
24
|
+
typeof CSS.supports === 'function' &&
|
|
25
|
+
CSS.supports('anchor-name', '--x')
|
|
26
|
+
);
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
|
|
33
|
+
|
|
34
|
+
// data-side (physical) → fallback `side` (logical block / inline axis).
|
|
35
|
+
const SIDE_TO_AXIS = {
|
|
36
|
+
top: 'block-start',
|
|
37
|
+
bottom: 'block-end',
|
|
38
|
+
left: 'inline-start',
|
|
39
|
+
right: 'inline-end',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read a floating element's `data-side` / `data-align` attributes into the
|
|
44
|
+
* `{ side, align }` options {@link positionFloating} understands, falling
|
|
45
|
+
* back to the component's default when an attribute is absent or invalid.
|
|
46
|
+
* The CSS Anchor Positioning path keys off the same attributes
|
|
47
|
+
* (`position-area`), so both paths place the element identically.
|
|
48
|
+
*
|
|
49
|
+
* @param {Element} el
|
|
50
|
+
* @param {{ side?: string, align?: string }} [fallback]
|
|
51
|
+
* @returns {{ side: string, align: 'start'|'center'|'end' }}
|
|
52
|
+
*/
|
|
53
|
+
export function readSideAlign(el, fallback = {}) {
|
|
54
|
+
const side = SIDE_TO_AXIS[el.getAttribute('data-side')] ?? fallback.side ?? 'block-end';
|
|
55
|
+
const alignAttr = el.getAttribute('data-align');
|
|
56
|
+
const align = ['start', 'center', 'end'].includes(alignAttr)
|
|
57
|
+
? alignAttr
|
|
58
|
+
: fallback.align ?? 'start';
|
|
59
|
+
return { side, align };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Position `floating` next to `anchor`, once.
|
|
64
|
+
*
|
|
65
|
+
* @param {HTMLElement} floating the popover to place (already in the top layer)
|
|
66
|
+
* @param {HTMLElement} anchor the trigger to place it against
|
|
67
|
+
* @param {object} [opts]
|
|
68
|
+
* @param {'block-end'|'block-start'|'inline-end'|'inline-start'} [opts.side='block-end']
|
|
69
|
+
* primary side. The block sides drop the floating element below / above the
|
|
70
|
+
* anchor (dropdown); the inline sides place it to the right / left
|
|
71
|
+
* (submenu), aligning their block-start edges.
|
|
72
|
+
* @param {'start'|'center'} [opts.align='start'] inline-axis alignment (block sides only)
|
|
73
|
+
* @param {number} [opts.gap=4] distance from the anchor, px
|
|
74
|
+
* @param {boolean} [opts.matchWidth=false] set min-width to the anchor width
|
|
75
|
+
*/
|
|
76
|
+
export function positionFloating(floating, anchor, opts = {}) {
|
|
77
|
+
const { side = 'block-end', align = 'start', gap = 4, matchWidth = false } = opts;
|
|
78
|
+
const a = anchor.getBoundingClientRect();
|
|
79
|
+
const f = floating.getBoundingClientRect();
|
|
80
|
+
const view = floating.ownerDocument.defaultView;
|
|
81
|
+
const vw = view?.innerWidth ?? 0;
|
|
82
|
+
const vh = view?.innerHeight ?? 0;
|
|
83
|
+
const rtl = view ? view.getComputedStyle(anchor).direction === 'rtl' : false;
|
|
84
|
+
|
|
85
|
+
// Inline sides (submenu): place beside the anchor, align block tops.
|
|
86
|
+
if (side === 'inline-end' || side === 'inline-start') {
|
|
87
|
+
// `inline-end` resolves to the physical right in LTR, left in RTL.
|
|
88
|
+
const toRight = (side === 'inline-end') !== rtl;
|
|
89
|
+
let left;
|
|
90
|
+
if (toRight) {
|
|
91
|
+
left = a.right + gap;
|
|
92
|
+
if (left + f.width > vw && a.left - f.width - gap >= 0) left = a.left - f.width - gap;
|
|
93
|
+
} else {
|
|
94
|
+
left = a.left - f.width - gap;
|
|
95
|
+
if (left < 0 && a.right + f.width + gap <= vw) left = a.right + gap;
|
|
96
|
+
}
|
|
97
|
+
// Cross axis (block): align start (tops) / center / end (bottoms),
|
|
98
|
+
// flipping the chosen edge when it would overflow.
|
|
99
|
+
let top;
|
|
100
|
+
if (align === 'center') {
|
|
101
|
+
top = a.top + (a.height - f.height) / 2;
|
|
102
|
+
} else if (align === 'end') {
|
|
103
|
+
top = a.bottom - f.height;
|
|
104
|
+
if (top < 0 && a.top + f.height <= vh) top = a.top;
|
|
105
|
+
} else {
|
|
106
|
+
top = a.top;
|
|
107
|
+
if (top + f.height > vh && a.bottom - f.height >= 0) top = a.bottom - f.height;
|
|
108
|
+
}
|
|
109
|
+
top = clamp(top, gap, Math.max(gap, vh - f.height - gap));
|
|
110
|
+
left = clamp(left, gap, Math.max(gap, vw - f.width - gap));
|
|
111
|
+
|
|
112
|
+
floating.style.position = 'fixed';
|
|
113
|
+
floating.style.top = `${top}px`;
|
|
114
|
+
floating.style.left = `${left}px`;
|
|
115
|
+
floating.style.insetInlineStart = 'auto';
|
|
116
|
+
floating.style.insetBlockStart = 'auto';
|
|
117
|
+
floating.style.margin = '0';
|
|
118
|
+
if (matchWidth) floating.style.minWidth = `${a.width}px`;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Block axis: primary side, flip when it would overflow and there is room.
|
|
123
|
+
let top;
|
|
124
|
+
if (side === 'block-start') {
|
|
125
|
+
top = a.top - f.height - gap;
|
|
126
|
+
if (top < 0 && a.bottom + f.height + gap <= vh) top = a.bottom + gap;
|
|
127
|
+
} else {
|
|
128
|
+
top = a.bottom + gap;
|
|
129
|
+
if (top + f.height > vh && a.top - f.height - gap >= 0) top = a.top - f.height - gap;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Inline axis: align start / center / end, then flip on overflow.
|
|
133
|
+
let left;
|
|
134
|
+
if (align === 'center') {
|
|
135
|
+
left = a.left + (a.width - f.width) / 2;
|
|
136
|
+
} else {
|
|
137
|
+
// `start` aligns the inline-start edges, `end` the inline-end edges; RTL
|
|
138
|
+
// swaps which physical edge each maps to.
|
|
139
|
+
const startToLeft = (align !== 'end') !== rtl;
|
|
140
|
+
if (startToLeft) {
|
|
141
|
+
left = a.left;
|
|
142
|
+
if (left + f.width > vw && a.right - f.width >= 0) left = a.right - f.width;
|
|
143
|
+
} else {
|
|
144
|
+
left = a.right - f.width;
|
|
145
|
+
if (left < 0 && a.left + f.width <= vw) left = a.left;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Final safety clamp so it can never sit fully off-screen.
|
|
150
|
+
top = clamp(top, gap, Math.max(gap, vh - f.height - gap));
|
|
151
|
+
left = clamp(left, gap, Math.max(gap, vw - f.width - gap));
|
|
152
|
+
|
|
153
|
+
floating.style.position = 'fixed';
|
|
154
|
+
floating.style.top = `${top}px`;
|
|
155
|
+
floating.style.left = `${left}px`;
|
|
156
|
+
floating.style.insetInlineStart = 'auto';
|
|
157
|
+
floating.style.insetBlockStart = 'auto';
|
|
158
|
+
floating.style.margin = '0';
|
|
159
|
+
if (matchWidth) floating.style.minWidth = `${a.width}px`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const CLEARED = [
|
|
163
|
+
'position',
|
|
164
|
+
'top',
|
|
165
|
+
'left',
|
|
166
|
+
'inset-inline-start',
|
|
167
|
+
'inset-block-start',
|
|
168
|
+
'margin',
|
|
169
|
+
'min-width',
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Position `floating` against `anchor` now and keep it tracking while open.
|
|
174
|
+
* Re-runs on scroll (in any ancestor, via capture) and on resize.
|
|
175
|
+
*
|
|
176
|
+
* @param {HTMLElement} floating
|
|
177
|
+
* @param {HTMLElement} anchor
|
|
178
|
+
* @param {object} [opts] see {@link positionFloating}
|
|
179
|
+
* @returns {() => void} cleanup — removes the listeners and clears the inline
|
|
180
|
+
* styles. Idempotent.
|
|
181
|
+
*/
|
|
182
|
+
export function trackFloating(floating, anchor, opts = {}) {
|
|
183
|
+
const view = floating.ownerDocument.defaultView;
|
|
184
|
+
const reposition = () => positionFloating(floating, anchor, opts);
|
|
185
|
+
reposition();
|
|
186
|
+
// capture:true so scrolls in ancestor scroll containers are caught too.
|
|
187
|
+
view?.addEventListener('scroll', reposition, true);
|
|
188
|
+
view?.addEventListener('resize', reposition);
|
|
189
|
+
|
|
190
|
+
let done = false;
|
|
191
|
+
return () => {
|
|
192
|
+
if (done) return;
|
|
193
|
+
done = true;
|
|
194
|
+
view?.removeEventListener('scroll', reposition, true);
|
|
195
|
+
view?.removeEventListener('resize', reposition);
|
|
196
|
+
for (const prop of CLEARED) floating.style.removeProperty(prop);
|
|
197
|
+
};
|
|
198
|
+
}
|
package/dist/avatar.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install the avatar behavior on every composite `.hc-avatar` (one holding a
|
|
3
|
+
* `.hc-avatar__image`) in the document: track the image's load / error state
|
|
4
|
+
* in `data-state` so the initials fallback shows when the image is missing.
|
|
5
|
+
* The CSS works without it; this adds the automatic swap.
|
|
6
|
+
*
|
|
7
|
+
* @param {Document|Element} [root]
|
|
8
|
+
* @returns {() => void}
|
|
9
|
+
*/
|
|
10
|
+
export function installAvatar(root?: Document | Element): () => void;
|
package/dist/avatar.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// installAvatar — image load/error → initials fallback for hc-avatar.
|
|
2
|
+
//
|
|
3
|
+
// Drives the `data-state` of a composite avatar off the native image
|
|
4
|
+
// `load` / `error` events — no network of its own:
|
|
5
|
+
//
|
|
6
|
+
// <span class="hc-avatar" role="img" aria-label="Ada Lovelace">
|
|
7
|
+
// <img class="hc-avatar__image" src="/ada.jpg" alt="">
|
|
8
|
+
// <span class="hc-avatar__fallback" aria-hidden="true">AL</span>
|
|
9
|
+
// </span>
|
|
10
|
+
//
|
|
11
|
+
// State machine (written to `data-state` on the wrapper):
|
|
12
|
+
//
|
|
13
|
+
// loading — image is fetching; the fallback shows so the slot is never
|
|
14
|
+
// empty (this is the default while waiting).
|
|
15
|
+
// pending — image is fetching but `data-delay="<ms>"` is set, so the
|
|
16
|
+
// fallback stays hidden for that window to avoid a flash on a
|
|
17
|
+
// fast connection. Becomes `loading` when the delay elapses.
|
|
18
|
+
// loaded — image decoded successfully; the fallback is hidden.
|
|
19
|
+
// error — the image failed or has no `src`; the fallback shows and the
|
|
20
|
+
// broken image is removed from the box.
|
|
21
|
+
//
|
|
22
|
+
// Each change dispatches a bubbling `hc:avatarstatechange` (detail.state).
|
|
23
|
+
//
|
|
24
|
+
// Only composite avatars (a `.hc-avatar` with a `.hc-avatar__image` child)
|
|
25
|
+
// are managed; plain `<img class="hc-avatar">` / `<span class="hc-avatar">`
|
|
26
|
+
// avatars are left untouched. Progressive: with JS off the image still
|
|
27
|
+
// covers the fallback when it loads (a broken image shows the fallback
|
|
28
|
+
// behind it).
|
|
29
|
+
//
|
|
30
|
+
// installAvatar(root = document) returns an idempotent uninstaller.
|
|
31
|
+
|
|
32
|
+
const INSTALL_KEY = '__hcAvatarUninstall';
|
|
33
|
+
|
|
34
|
+
function imageOf(avatar) {
|
|
35
|
+
return avatar.querySelector(':scope > .hc-avatar__image');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hasSrc(img) {
|
|
39
|
+
const src = img.getAttribute('src');
|
|
40
|
+
return src != null && src.trim() !== '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Synchronous verdict for an image that may already be settled (e.g. served
|
|
44
|
+
// from cache before the behavior runs). null means "still loading".
|
|
45
|
+
function settle(img) {
|
|
46
|
+
if (!hasSrc(img)) return 'error';
|
|
47
|
+
if (img.complete) return img.naturalWidth > 0 ? 'loaded' : 'error';
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function setState(avatar, state) {
|
|
52
|
+
if (avatar.dataset.state === state) return;
|
|
53
|
+
avatar.dataset.state = state;
|
|
54
|
+
avatar.dispatchEvent(
|
|
55
|
+
new CustomEvent('hc:avatarstatechange', { bubbles: true, detail: { state } }),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function attach(avatar, detachers) {
|
|
60
|
+
if (detachers.has(avatar)) return;
|
|
61
|
+
const img = imageOf(avatar);
|
|
62
|
+
if (!img) return; // a plain avatar — nothing to manage
|
|
63
|
+
|
|
64
|
+
let timer = null;
|
|
65
|
+
const clearTimer = () => {
|
|
66
|
+
if (timer != null) {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
timer = null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function onLoad() {
|
|
73
|
+
clearTimer();
|
|
74
|
+
setState(avatar, 'loaded');
|
|
75
|
+
}
|
|
76
|
+
function onError() {
|
|
77
|
+
clearTimer();
|
|
78
|
+
setState(avatar, 'error');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const initial = settle(img);
|
|
82
|
+
if (initial === 'loaded' || initial === 'error') {
|
|
83
|
+
setState(avatar, initial);
|
|
84
|
+
} else {
|
|
85
|
+
const delay = Math.max(0, parseInt(avatar.getAttribute('data-delay'), 10) || 0);
|
|
86
|
+
if (delay > 0) {
|
|
87
|
+
setState(avatar, 'pending');
|
|
88
|
+
timer = setTimeout(() => {
|
|
89
|
+
timer = null;
|
|
90
|
+
if (avatar.dataset.state === 'pending') setState(avatar, 'loading');
|
|
91
|
+
}, delay);
|
|
92
|
+
} else {
|
|
93
|
+
setState(avatar, 'loading');
|
|
94
|
+
}
|
|
95
|
+
img.addEventListener('load', onLoad);
|
|
96
|
+
img.addEventListener('error', onError);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
detachers.set(avatar, () => {
|
|
100
|
+
clearTimer();
|
|
101
|
+
img.removeEventListener('load', onLoad);
|
|
102
|
+
img.removeEventListener('error', onError);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Install the avatar behavior on every composite `.hc-avatar` (one holding a
|
|
108
|
+
* `.hc-avatar__image`) in the document: track the image's load / error state
|
|
109
|
+
* in `data-state` so the initials fallback shows when the image is missing.
|
|
110
|
+
* The CSS works without it; this adds the automatic swap.
|
|
111
|
+
*
|
|
112
|
+
* @param {Document|Element} [root]
|
|
113
|
+
* @returns {() => void}
|
|
114
|
+
*/
|
|
115
|
+
export function installAvatar(
|
|
116
|
+
root = typeof document !== 'undefined' ? document : null,
|
|
117
|
+
) {
|
|
118
|
+
if (!root) return () => {};
|
|
119
|
+
if (root[INSTALL_KEY]) return root[INSTALL_KEY];
|
|
120
|
+
|
|
121
|
+
const detachers = new Map();
|
|
122
|
+
|
|
123
|
+
for (const el of root.querySelectorAll('.hc-avatar')) attach(el, detachers);
|
|
124
|
+
|
|
125
|
+
let observer = null;
|
|
126
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
127
|
+
observer = new MutationObserver((records) => {
|
|
128
|
+
for (const rec of records) {
|
|
129
|
+
for (const node of rec.addedNodes) {
|
|
130
|
+
if (node.nodeType !== 1) continue;
|
|
131
|
+
if (node.matches?.('.hc-avatar')) attach(node, detachers);
|
|
132
|
+
node.querySelectorAll?.('.hc-avatar').forEach((el) => attach(el, detachers));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
observer.observe(root.body ?? root, { childList: true, subtree: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const uninstall = () => {
|
|
140
|
+
if (root[INSTALL_KEY] !== uninstall) return;
|
|
141
|
+
if (observer) observer.disconnect();
|
|
142
|
+
for (const detach of detachers.values()) detach();
|
|
143
|
+
detachers.clear();
|
|
144
|
+
delete root[INSTALL_KEY];
|
|
145
|
+
};
|
|
146
|
+
root[INSTALL_KEY] = uninstall;
|
|
147
|
+
return uninstall;
|
|
148
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install the calendar behavior on every `.hc-calendar` container in the
|
|
3
|
+
* document. The returned uninstaller is idempotent and a no-op when the
|
|
4
|
+
* behavior is not installed.
|
|
5
|
+
*
|
|
6
|
+
* @param {Document|Element} [root]
|
|
7
|
+
* @returns {() => void}
|
|
8
|
+
*/
|
|
9
|
+
export function installCalendar(root?: Document | Element): () => void;
|