@joewinke/jatui 0.1.33 → 0.2.1
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/CLAUDE.md +22 -0
- package/dist/actions/attention.d.ts +24 -0
- package/dist/actions/attention.js +94 -0
- package/dist/actions/floating.d.ts +53 -0
- package/dist/actions/floating.js +158 -0
- package/dist/actions/staggerEntrance.d.ts +28 -0
- package/dist/actions/staggerEntrance.js +71 -0
- package/dist/actions/tooltip.d.ts +28 -0
- package/dist/actions/tooltip.js +186 -0
- package/dist/assets/sounds/README.md +47 -0
- package/dist/assets/sounds/error.d.ts +1 -0
- package/dist/assets/sounds/error.js +4 -0
- package/dist/assets/sounds/notify.d.ts +1 -0
- package/dist/assets/sounds/notify.js +4 -0
- package/dist/assets/sounds/pop.d.ts +1 -0
- package/dist/assets/sounds/pop.js +4 -0
- package/dist/assets/sounds/send.d.ts +1 -0
- package/dist/assets/sounds/send.js +4 -0
- package/dist/assets/sounds/success.d.ts +1 -0
- package/dist/assets/sounds/success.js +4 -0
- package/dist/assets/sounds/toggle.d.ts +1 -0
- package/dist/assets/sounds/toggle.js +4 -0
- package/dist/carousel/index.d.ts +1 -0
- package/dist/carousel/index.js +2 -0
- package/dist/components/AnimatedNumber.svelte +112 -0
- package/dist/components/AnimatedNumber.svelte.d.ts +8 -0
- package/dist/components/Badge.svelte +75 -4
- package/dist/components/Badge.svelte.d.ts +6 -1
- package/dist/components/Button.svelte +196 -21
- package/dist/components/Button.svelte.d.ts +10 -1
- package/dist/components/Carousel.svelte +265 -0
- package/dist/components/Carousel.svelte.d.ts +21 -0
- package/dist/components/ChipInput.svelte +63 -3
- package/dist/components/Collapse.svelte +70 -14
- package/dist/components/Collapse.svelte.d.ts +11 -0
- package/dist/components/ColorSelector.svelte +21 -24
- package/dist/components/CommandPalette.svelte +475 -0
- package/dist/components/CommandPalette.svelte.d.ts +3 -0
- package/dist/components/ContextMenu.svelte +9 -73
- package/dist/components/CopyButton.svelte +156 -0
- package/dist/components/CopyButton.svelte.d.ts +12 -0
- package/dist/components/HoldToConfirm.svelte +183 -0
- package/dist/components/HoldToConfirm.svelte.d.ts +11 -0
- package/dist/components/KeyboardShortcutsOverlay.svelte +54 -8
- package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +3 -0
- package/dist/components/PasswordMeter.svelte +87 -0
- package/dist/components/PasswordMeter.svelte.d.ts +8 -0
- package/dist/components/PinInput.svelte +186 -0
- package/dist/components/PinInput.svelte.d.ts +13 -0
- package/dist/components/QuantityStepper.svelte +185 -0
- package/dist/components/QuantityStepper.svelte.d.ts +12 -0
- package/dist/components/Skeleton.svelte +123 -0
- package/dist/components/Skeleton.svelte.d.ts +12 -0
- package/dist/components/SpeechForm.svelte +37 -0
- package/dist/components/StatusBadge.svelte +31 -3
- package/dist/components/StatusBadge.svelte.d.ts +0 -5
- package/dist/components/Tabs.svelte +270 -0
- package/dist/components/Tabs.svelte.d.ts +15 -0
- package/dist/components/TextInput.svelte +138 -38
- package/dist/components/TextInput.svelte.d.ts +9 -1
- package/dist/components/VoicePlayer.svelte +51 -0
- package/dist/components/basic/Alert.svelte +36 -0
- package/dist/components/basic/Alert.svelte.d.ts +9 -0
- package/dist/components/basic/Breadcrumb.svelte +27 -0
- package/dist/components/basic/Breadcrumb.svelte.d.ts +11 -0
- package/dist/components/basic/ButtonGroup.svelte +15 -0
- package/dist/components/basic/ButtonGroup.svelte.d.ts +8 -0
- package/dist/components/basic/Checkbox.svelte +85 -0
- package/dist/components/basic/Checkbox.svelte.d.ts +15 -0
- package/dist/components/basic/Empty.svelte +17 -0
- package/dist/components/basic/Empty.svelte.d.ts +8 -0
- package/dist/components/basic/Kbd.svelte +16 -0
- package/dist/components/basic/Kbd.svelte.d.ts +8 -0
- package/dist/components/basic/Pagination.svelte +104 -0
- package/dist/components/basic/Pagination.svelte.d.ts +12 -0
- package/dist/components/basic/RadioGroup.svelte +71 -0
- package/dist/components/basic/RadioGroup.svelte.d.ts +18 -0
- package/dist/components/basic/Separator.svelte +19 -0
- package/dist/components/basic/Separator.svelte.d.ts +8 -0
- package/dist/components/basic/Slider.svelte +72 -0
- package/dist/components/basic/Slider.svelte.d.ts +17 -0
- package/dist/components/basic/Switch.svelte +88 -0
- package/dist/components/basic/Switch.svelte.d.ts +14 -0
- package/dist/components/basic/Toggle.svelte +43 -0
- package/dist/components/basic/Toggle.svelte.d.ts +14 -0
- package/dist/components/graph/GraphCanvas.svelte +103 -0
- package/dist/components/graph/GraphCanvas.svelte.d.ts +22 -0
- package/dist/components/graph/JatuiNode.svelte +128 -0
- package/dist/components/graph/JatuiNode.svelte.d.ts +14 -0
- package/dist/components/graph/index.d.ts +4 -0
- package/dist/components/graph/index.js +4 -0
- package/dist/components/graph/theme.d.ts +27 -0
- package/dist/components/graph/theme.js +136 -0
- package/dist/components/messaging/MessageItem.svelte +92 -0
- package/dist/components/messaging/MessageItem.svelte.d.ts +5 -0
- package/dist/components/messaging/TypingIndicator.svelte +63 -0
- package/dist/components/messaging/TypingIndicator.svelte.d.ts +7 -0
- package/dist/components/pipeline/PipelineColumn.svelte +44 -3
- package/dist/components/progress/ProgressBar.svelte +189 -0
- package/dist/components/progress/ProgressBar.svelte.d.ts +17 -0
- package/dist/components/progress/ProgressRing.svelte +138 -0
- package/dist/components/progress/ProgressRing.svelte.d.ts +12 -0
- package/dist/components/progress/Steps.svelte +195 -0
- package/dist/components/progress/Steps.svelte.d.ts +14 -0
- package/dist/components/shell/NavigationMenu.svelte +422 -0
- package/dist/components/shell/NavigationMenu.svelte.d.ts +43 -0
- package/dist/components/shell/Sidebar.svelte +276 -0
- package/dist/components/shell/Sidebar.svelte.d.ts +32 -0
- package/dist/components/shell/SidebarGroup.svelte +180 -0
- package/dist/components/shell/SidebarGroup.svelte.d.ts +19 -0
- package/dist/components/shell/SidebarItem.svelte +170 -0
- package/dist/components/shell/SidebarItem.svelte.d.ts +24 -0
- package/dist/components/shell/shell.d.ts +30 -0
- package/dist/components/shell/shell.js +65 -0
- package/dist/components/table/DataTable.svelte +364 -0
- package/dist/components/table/DataTable.svelte.d.ts +4 -0
- package/dist/components/table/flexRender.d.ts +31 -0
- package/dist/components/table/flexRender.js +32 -0
- package/dist/components/table/index.d.ts +3 -0
- package/dist/components/table/index.js +3 -0
- package/dist/components/table/types.d.ts +28 -0
- package/dist/components/table/types.js +1 -0
- package/dist/components/toast/ToastHost.svelte +21 -0
- package/dist/components/toast/ToastHost.svelte.d.ts +3 -0
- package/dist/components/toast/ToastItem.svelte +287 -0
- package/dist/components/toast/ToastItem.svelte.d.ts +9 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +64 -0
- package/dist/showcase/AnimatedBeam.svelte +183 -0
- package/dist/showcase/AnimatedBeam.svelte.d.ts +26 -0
- package/dist/showcase/AuroraDrift.svelte +74 -0
- package/dist/showcase/AuroraDrift.svelte.d.ts +9 -0
- package/dist/showcase/BentoCard.svelte +50 -0
- package/dist/showcase/BentoCard.svelte.d.ts +14 -0
- package/dist/showcase/BentoGrid.svelte +43 -0
- package/dist/showcase/BentoGrid.svelte.d.ts +12 -0
- package/dist/showcase/BorderBeam.svelte +79 -0
- package/dist/showcase/BorderBeam.svelte.d.ts +12 -0
- package/dist/showcase/ClipWipe.svelte +82 -0
- package/dist/showcase/ClipWipe.svelte.d.ts +12 -0
- package/dist/showcase/Confetti.svelte +71 -0
- package/dist/showcase/Confetti.svelte.d.ts +10 -0
- package/dist/showcase/CursorSpotlight.svelte +85 -0
- package/dist/showcase/CursorSpotlight.svelte.d.ts +9 -0
- package/dist/showcase/DeviceMock.svelte +186 -0
- package/dist/showcase/DeviceMock.svelte.d.ts +11 -0
- package/dist/showcase/Dock.svelte +123 -0
- package/dist/showcase/Dock.svelte.d.ts +17 -0
- package/dist/showcase/FlipCard.svelte +103 -0
- package/dist/showcase/FlipCard.svelte.d.ts +10 -0
- package/dist/showcase/FocusReveal.svelte +83 -0
- package/dist/showcase/FocusReveal.svelte.d.ts +11 -0
- package/dist/showcase/Globe.svelte +202 -0
- package/dist/showcase/Globe.svelte.d.ts +16 -0
- package/dist/showcase/KenBurns.svelte +105 -0
- package/dist/showcase/KenBurns.svelte.d.ts +13 -0
- package/dist/showcase/Marquee.svelte +89 -0
- package/dist/showcase/Marquee.svelte.d.ts +9 -0
- package/dist/showcase/ParallaxTilt.svelte +98 -0
- package/dist/showcase/ParallaxTilt.svelte.d.ts +10 -0
- package/dist/showcase/ScrambleText.svelte +98 -0
- package/dist/showcase/ScrambleText.svelte.d.ts +11 -0
- package/dist/showcase/ShineSweep.svelte +81 -0
- package/dist/showcase/ShineSweep.svelte.d.ts +9 -0
- package/dist/showcase/Terminal.svelte +230 -0
- package/dist/showcase/Terminal.svelte.d.ts +18 -0
- package/dist/showcase/TextSplitReveal.svelte +146 -0
- package/dist/showcase/TextSplitReveal.svelte.d.ts +13 -0
- package/dist/showcase/TextWave.svelte +55 -0
- package/dist/showcase/TextWave.svelte.d.ts +8 -0
- package/dist/showcase/TypewriterText.svelte +130 -0
- package/dist/showcase/TypewriterText.svelte.d.ts +9 -0
- package/dist/showcase/backdrop/GrainGradient.svelte +126 -0
- package/dist/showcase/backdrop/GrainGradient.svelte.d.ts +23 -0
- package/dist/showcase/backdrop/MeshGradient.svelte +104 -0
- package/dist/showcase/backdrop/MeshGradient.svelte.d.ts +20 -0
- package/dist/showcase/backdrop/Metaballs.svelte +116 -0
- package/dist/showcase/backdrop/Metaballs.svelte.d.ts +18 -0
- package/dist/showcase/backdrop/NeuroNoise.svelte +115 -0
- package/dist/showcase/backdrop/NeuroNoise.svelte.d.ts +20 -0
- package/dist/showcase/backdrop/ShowcaseBackdrop.svelte +48 -0
- package/dist/showcase/backdrop/ShowcaseBackdrop.svelte.d.ts +12 -0
- package/dist/showcase/backdrop/index.d.ts +5 -0
- package/dist/showcase/backdrop/index.js +6 -0
- package/dist/showcase/generative/GenerativeBackdrop.svelte +234 -0
- package/dist/showcase/generative/GenerativeBackdrop.svelte.d.ts +9 -0
- package/dist/showcase/generative/intent.d.ts +7 -0
- package/dist/showcase/generative/intent.js +67 -0
- package/dist/showcase/generative/manifest.d.ts +9 -0
- package/dist/showcase/generative/manifest.js +97 -0
- package/dist/showcase/generative/types.d.ts +48 -0
- package/dist/showcase/generative/types.js +8 -0
- package/dist/showcase/globe/index.d.ts +1 -0
- package/dist/showcase/globe/index.js +2 -0
- package/dist/showcase/index.d.ts +25 -0
- package/dist/showcase/index.js +39 -0
- package/dist/showcase/scroll/SmoothScroll.svelte +49 -0
- package/dist/showcase/scroll/SmoothScroll.svelte.d.ts +12 -0
- package/dist/showcase/scroll/index.d.ts +5 -0
- package/dist/showcase/scroll/index.js +4 -0
- package/dist/showcase/scroll/scrollProgress.d.ts +24 -0
- package/dist/showcase/scroll/scrollProgress.js +65 -0
- package/dist/showcase/scroll/scrollReveal.d.ts +30 -0
- package/dist/showcase/scroll/scrollReveal.js +70 -0
- package/dist/stores/commandPalette.svelte.d.ts +24 -0
- package/dist/stores/commandPalette.svelte.js +82 -0
- package/dist/stores/toast.svelte.d.ts +28 -0
- package/dist/stores/toast.svelte.js +33 -0
- package/dist/styles/motion.css +46 -0
- package/dist/styles/tokens.css +31 -0
- package/dist/transitions/glide.d.ts +31 -0
- package/dist/transitions/glide.js +58 -0
- package/dist/utils/sound.d.ts +6 -0
- package/dist/utils/sound.js +113 -0
- package/dist/utils/splitText.d.ts +25 -0
- package/dist/utils/splitText.js +94 -0
- package/dist/utils/themeWipe.d.ts +21 -0
- package/dist/utils/themeWipe.js +94 -0
- package/dist/utils/viewTransition.d.ts +23 -0
- package/dist/utils/viewTransition.js +75 -0
- package/package.json +78 -4
package/CLAUDE.md
CHANGED
|
@@ -239,6 +239,28 @@ If the project uses JST (Joe's Software Template), browse component demos at `/c
|
|
|
239
239
|
- Check the component source in `src/lib/components/` for full prop types
|
|
240
240
|
- Prefer jatui components over building custom ones when there's a match
|
|
241
241
|
|
|
242
|
+
# Layering Policy (DaisyUI is an implementation detail)
|
|
243
|
+
|
|
244
|
+
Three rules keep DaisyUI swappable instead of load-bearing:
|
|
245
|
+
|
|
246
|
+
1. **jatui is the only API consumers touch.** App code imports `<Button>`,
|
|
247
|
+
`<TextInput>`, `<Modal>` — it never hand-writes `btn btn-primary` for
|
|
248
|
+
anything jatui covers. Consumer repos enforce this with the ratchet:
|
|
249
|
+
`node scripts/check-daisy-leakage.mjs <src> --baseline <file>` (count may
|
|
250
|
+
only shrink; marketing routes can be `--allow`ed).
|
|
251
|
+
2. **DaisyUI class names live only inside jatui components.** They are
|
|
252
|
+
internal skin, like vendored CSS — never part of a component's public
|
|
253
|
+
props/API.
|
|
254
|
+
3. **jatui owns the token layer.** Components consume `--jatui-*`
|
|
255
|
+
(`styles/tokens.css`: radius/spacing/border) and `--motion-*`
|
|
256
|
+
(`styles/motion.css`: easing/duration) custom properties. DaisyUI theme
|
|
257
|
+
variables feed INTO those tokens (as fallback-chained inputs), never
|
|
258
|
+
appear directly in component styles for values the tokens cover.
|
|
259
|
+
|
|
260
|
+
Net effect: dropping or replacing DaisyUI would be a change to the token
|
|
261
|
+
files and component internals — a skin swap inside this package, not an
|
|
262
|
+
app-wide migration for consumers.
|
|
263
|
+
|
|
242
264
|
# Accessibility (a11y)
|
|
243
265
|
|
|
244
266
|
@~/code/jat/shared/a11y-patterns.md
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type AttentionEffect = 'shake' | 'ping';
|
|
2
|
+
export interface AttentionOptions {
|
|
3
|
+
effect: AttentionEffect;
|
|
4
|
+
/** Change this value to retrigger. Any truthy/changing value fires the effect. */
|
|
5
|
+
trigger?: unknown;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Retriggerable attention action — fires once per `trigger` change.
|
|
9
|
+
*
|
|
10
|
+
* use:attention={{ effect: 'shake', trigger: myTrigger }}
|
|
11
|
+
*
|
|
12
|
+
* shake — horizontal translate, 4–6px amplitude, 2–3 oscillations, ≤300ms total.
|
|
13
|
+
* Use for invalid-input / reject feedback. Pair with a static error affordance
|
|
14
|
+
* (color, icon, label) so the state survives without motion.
|
|
15
|
+
* Reduced-motion: skipped entirely.
|
|
16
|
+
*
|
|
17
|
+
* ping — single expanding fading ring behind the element. Use for notification
|
|
18
|
+
* dots and badges. Fires once per trigger change; NEVER loops infinitely.
|
|
19
|
+
* Reduced-motion: skipped entirely.
|
|
20
|
+
*/
|
|
21
|
+
export declare function attention(node: HTMLElement, options: AttentionOptions): {
|
|
22
|
+
update(newOptions: AttentionOptions): void;
|
|
23
|
+
destroy(): void;
|
|
24
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// direction taxonomy after animista.net (Ana Travas, BSD-2-Clause site license); keyframes re-authored on jatui motion tokens
|
|
2
|
+
/**
|
|
3
|
+
* Retriggerable attention action — fires once per `trigger` change.
|
|
4
|
+
*
|
|
5
|
+
* use:attention={{ effect: 'shake', trigger: myTrigger }}
|
|
6
|
+
*
|
|
7
|
+
* shake — horizontal translate, 4–6px amplitude, 2–3 oscillations, ≤300ms total.
|
|
8
|
+
* Use for invalid-input / reject feedback. Pair with a static error affordance
|
|
9
|
+
* (color, icon, label) so the state survives without motion.
|
|
10
|
+
* Reduced-motion: skipped entirely.
|
|
11
|
+
*
|
|
12
|
+
* ping — single expanding fading ring behind the element. Use for notification
|
|
13
|
+
* dots and badges. Fires once per trigger change; NEVER loops infinitely.
|
|
14
|
+
* Reduced-motion: skipped entirely.
|
|
15
|
+
*/
|
|
16
|
+
export function attention(node, options) {
|
|
17
|
+
var currentTrigger = options.trigger;
|
|
18
|
+
var pingEl = null;
|
|
19
|
+
var shakeId = null;
|
|
20
|
+
function prefersReduced() {
|
|
21
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
22
|
+
}
|
|
23
|
+
function clearShake() {
|
|
24
|
+
node.style.animation = '';
|
|
25
|
+
if (shakeId !== null) {
|
|
26
|
+
clearTimeout(shakeId);
|
|
27
|
+
shakeId = null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function clearPing() {
|
|
31
|
+
if (pingEl) {
|
|
32
|
+
pingEl.remove();
|
|
33
|
+
pingEl = null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function runShake() {
|
|
37
|
+
if (prefersReduced())
|
|
38
|
+
return;
|
|
39
|
+
clearShake();
|
|
40
|
+
// 3 oscillations × 2 half-cycles = 6 keyframe segments in ~280ms
|
|
41
|
+
// translate: 0 → +5px → -5px → +4px → -4px → +2px → 0
|
|
42
|
+
var kf = [
|
|
43
|
+
{ transform: 'translateX(0)' },
|
|
44
|
+
{ transform: 'translateX(5px)' },
|
|
45
|
+
{ transform: 'translateX(-5px)' },
|
|
46
|
+
{ transform: 'translateX(4px)' },
|
|
47
|
+
{ transform: 'translateX(-4px)' },
|
|
48
|
+
{ transform: 'translateX(2px)' },
|
|
49
|
+
{ transform: 'translateX(0)' }
|
|
50
|
+
];
|
|
51
|
+
node.animate(kf, { duration: 280, easing: 'ease-in-out', fill: 'none' });
|
|
52
|
+
}
|
|
53
|
+
function runPing() {
|
|
54
|
+
if (prefersReduced())
|
|
55
|
+
return;
|
|
56
|
+
clearPing();
|
|
57
|
+
var rect = node.getBoundingClientRect();
|
|
58
|
+
var size = Math.max(rect.width, rect.height);
|
|
59
|
+
// Ensure node is position-capable for the pseudo-ring
|
|
60
|
+
var pos = getComputedStyle(node).position;
|
|
61
|
+
if (pos === 'static')
|
|
62
|
+
node.style.position = 'relative';
|
|
63
|
+
pingEl = document.createElement('span');
|
|
64
|
+
pingEl.style.cssText = "\n\t\t\tposition:absolute;\n\t\t\tinset:-4px;\n\t\t\tborder-radius:inherit;\n\t\t\tborder:2px solid currentColor;\n\t\t\topacity:0.7;\n\t\t\tpointer-events:none;\n\t\t\tz-index:-1;\n\t\t";
|
|
65
|
+
node.appendChild(pingEl);
|
|
66
|
+
var scale = 1 + 24 / size;
|
|
67
|
+
pingEl.animate([
|
|
68
|
+
{ transform: 'scale(1)', opacity: '0.7' },
|
|
69
|
+
{ transform: "scale(".concat(scale.toFixed(2), ")"), opacity: '0' }
|
|
70
|
+
], { duration: 600, easing: 'ease-out', fill: 'forwards' }).onfinish = function () { return clearPing(); };
|
|
71
|
+
}
|
|
72
|
+
function fire(effect) {
|
|
73
|
+
if (effect === 'shake')
|
|
74
|
+
runShake();
|
|
75
|
+
else
|
|
76
|
+
runPing();
|
|
77
|
+
}
|
|
78
|
+
// Fire on mount if trigger is set
|
|
79
|
+
if (options.trigger !== undefined) {
|
|
80
|
+
fire(options.effect);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
update: function (newOptions) {
|
|
84
|
+
if (newOptions.trigger !== currentTrigger) {
|
|
85
|
+
currentTrigger = newOptions.trigger;
|
|
86
|
+
fire(newOptions.effect);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
destroy: function () {
|
|
90
|
+
clearShake();
|
|
91
|
+
clearPing();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* use:floating — anchored-positioning Svelte action
|
|
3
|
+
*
|
|
4
|
+
* Wraps @floating-ui/dom computePosition + autoUpdate so every popover-like
|
|
5
|
+
* component shares one battle-tested positioning primitive instead of each
|
|
6
|
+
* hand-rolling viewport clamping.
|
|
7
|
+
*
|
|
8
|
+
* Usage (element anchor):
|
|
9
|
+
* <div use:floating={{ anchor: triggerEl, placement: 'bottom-start', flip: true, shift: true, offset: 4, onDismiss: close }}>
|
|
10
|
+
*
|
|
11
|
+
* Usage (virtual / cursor point anchor):
|
|
12
|
+
* const virtualAnchor = makeVirtualAnchor(x, y)
|
|
13
|
+
* <div use:floating={{ anchor: virtualAnchor, placement: 'bottom-start', onDismiss: close }}>
|
|
14
|
+
*
|
|
15
|
+
* Animation: entrance fade + 2px rise, var(--motion-glide) var(--motion-fast).
|
|
16
|
+
* Exit is handled by the caller removing the element from the DOM.
|
|
17
|
+
* Reduced-motion guard: animation is skipped when the user prefers reduced motion.
|
|
18
|
+
*
|
|
19
|
+
* Dismiss:
|
|
20
|
+
* - Click outside the floating element fires onDismiss
|
|
21
|
+
* - Escape key fires onDismiss
|
|
22
|
+
* - Focus returns to the anchor element (or document.activeElement before mount)
|
|
23
|
+
* when the floating element is dismissed
|
|
24
|
+
*
|
|
25
|
+
* Accessibility:
|
|
26
|
+
* - The node must already carry role="menu" / role="listbox" etc. — this action
|
|
27
|
+
* does not set ARIA roles.
|
|
28
|
+
*/
|
|
29
|
+
import type { Placement, ReferenceElement } from '@floating-ui/dom';
|
|
30
|
+
export type { Placement };
|
|
31
|
+
export interface FloatingOptions {
|
|
32
|
+
/** Anchor element or virtual element (with getBoundingClientRect). */
|
|
33
|
+
anchor: ReferenceElement | null;
|
|
34
|
+
/** Preferred placement. @floating-ui/dom may flip/shift if enabled. Default 'bottom-start'. */
|
|
35
|
+
placement?: Placement;
|
|
36
|
+
/** Whether to flip to the opposite side when there's not enough room. Default true. */
|
|
37
|
+
flip?: boolean;
|
|
38
|
+
/** Whether to shift along the axis to stay in view. Default true. */
|
|
39
|
+
shift?: boolean;
|
|
40
|
+
/** Pixel distance between anchor and floating element. Default 4. */
|
|
41
|
+
offset?: number;
|
|
42
|
+
/** Called when the floating element should be dismissed (outside-click or Escape). */
|
|
43
|
+
onDismiss?: () => void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create a virtual anchor from a viewport point (e.g. cursor coordinates).
|
|
47
|
+
* Pass the returned object as `anchor` when there is no real DOM element to anchor to.
|
|
48
|
+
*/
|
|
49
|
+
export declare function makeVirtualAnchor(x: number, y: number): ReferenceElement;
|
|
50
|
+
export declare function floating(node: HTMLElement, options: FloatingOptions): {
|
|
51
|
+
update(next: FloatingOptions): void;
|
|
52
|
+
destroy(): void;
|
|
53
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* use:floating — anchored-positioning Svelte action
|
|
3
|
+
*
|
|
4
|
+
* Wraps @floating-ui/dom computePosition + autoUpdate so every popover-like
|
|
5
|
+
* component shares one battle-tested positioning primitive instead of each
|
|
6
|
+
* hand-rolling viewport clamping.
|
|
7
|
+
*
|
|
8
|
+
* Usage (element anchor):
|
|
9
|
+
* <div use:floating={{ anchor: triggerEl, placement: 'bottom-start', flip: true, shift: true, offset: 4, onDismiss: close }}>
|
|
10
|
+
*
|
|
11
|
+
* Usage (virtual / cursor point anchor):
|
|
12
|
+
* const virtualAnchor = makeVirtualAnchor(x, y)
|
|
13
|
+
* <div use:floating={{ anchor: virtualAnchor, placement: 'bottom-start', onDismiss: close }}>
|
|
14
|
+
*
|
|
15
|
+
* Animation: entrance fade + 2px rise, var(--motion-glide) var(--motion-fast).
|
|
16
|
+
* Exit is handled by the caller removing the element from the DOM.
|
|
17
|
+
* Reduced-motion guard: animation is skipped when the user prefers reduced motion.
|
|
18
|
+
*
|
|
19
|
+
* Dismiss:
|
|
20
|
+
* - Click outside the floating element fires onDismiss
|
|
21
|
+
* - Escape key fires onDismiss
|
|
22
|
+
* - Focus returns to the anchor element (or document.activeElement before mount)
|
|
23
|
+
* when the floating element is dismissed
|
|
24
|
+
*
|
|
25
|
+
* Accessibility:
|
|
26
|
+
* - The node must already carry role="menu" / role="listbox" etc. — this action
|
|
27
|
+
* does not set ARIA roles.
|
|
28
|
+
*/
|
|
29
|
+
var __assign = (this && this.__assign) || function () {
|
|
30
|
+
__assign = Object.assign || function(t) {
|
|
31
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
32
|
+
s = arguments[i];
|
|
33
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
34
|
+
t[p] = s[p];
|
|
35
|
+
}
|
|
36
|
+
return t;
|
|
37
|
+
};
|
|
38
|
+
return __assign.apply(this, arguments);
|
|
39
|
+
};
|
|
40
|
+
import { computePosition, autoUpdate, flip, shift, offset as fuOffset } from '@floating-ui/dom';
|
|
41
|
+
/**
|
|
42
|
+
* Create a virtual anchor from a viewport point (e.g. cursor coordinates).
|
|
43
|
+
* Pass the returned object as `anchor` when there is no real DOM element to anchor to.
|
|
44
|
+
*/
|
|
45
|
+
export function makeVirtualAnchor(x, y) {
|
|
46
|
+
return {
|
|
47
|
+
getBoundingClientRect: function () {
|
|
48
|
+
return {
|
|
49
|
+
width: 0,
|
|
50
|
+
height: 0,
|
|
51
|
+
x: x,
|
|
52
|
+
y: y,
|
|
53
|
+
top: y,
|
|
54
|
+
left: x,
|
|
55
|
+
right: x,
|
|
56
|
+
bottom: y,
|
|
57
|
+
toJSON: function () { return this; }
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function prefersReducedMotion() {
|
|
63
|
+
return typeof window !== 'undefined' &&
|
|
64
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
65
|
+
}
|
|
66
|
+
export function floating(node, options) {
|
|
67
|
+
var opts = __assign({ placement: 'bottom-start', flip: true, shift: true, offset: 4 }, options);
|
|
68
|
+
var cleanup = null;
|
|
69
|
+
// Remember the element focused before this floating opened so we can return focus on dismiss.
|
|
70
|
+
var returnFocusTarget = document.activeElement;
|
|
71
|
+
// Entrance animation
|
|
72
|
+
node.style.opacity = '0';
|
|
73
|
+
node.style.transform = 'translateY(2px)';
|
|
74
|
+
node.style.transition = prefersReducedMotion()
|
|
75
|
+
? 'none'
|
|
76
|
+
: [
|
|
77
|
+
"opacity var(--motion-fast, 150ms) var(--motion-glide, cubic-bezier(0.16,1,0.3,1))",
|
|
78
|
+
"transform var(--motion-fast, 150ms) var(--motion-glide, cubic-bezier(0.16,1,0.3,1))"
|
|
79
|
+
].join(', ');
|
|
80
|
+
function applyPosition() {
|
|
81
|
+
if (!opts.anchor)
|
|
82
|
+
return;
|
|
83
|
+
var middlewares = [];
|
|
84
|
+
if (opts.offset != null)
|
|
85
|
+
middlewares.push(fuOffset(opts.offset));
|
|
86
|
+
if (opts.flip)
|
|
87
|
+
middlewares.push(flip());
|
|
88
|
+
if (opts.shift)
|
|
89
|
+
middlewares.push(shift({ padding: 8 }));
|
|
90
|
+
computePosition(opts.anchor, node, {
|
|
91
|
+
placement: opts.placement,
|
|
92
|
+
strategy: 'fixed',
|
|
93
|
+
middleware: middlewares,
|
|
94
|
+
}).then(function (_a) {
|
|
95
|
+
var x = _a.x, y = _a.y;
|
|
96
|
+
node.style.position = 'fixed';
|
|
97
|
+
node.style.left = "".concat(x, "px");
|
|
98
|
+
node.style.top = "".concat(y, "px");
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function startAutoUpdate() {
|
|
102
|
+
cleanup === null || cleanup === void 0 ? void 0 : cleanup();
|
|
103
|
+
cleanup = null;
|
|
104
|
+
if (!opts.anchor)
|
|
105
|
+
return;
|
|
106
|
+
// For virtual anchors (plain objects), autoUpdate won't observe DOM mutations
|
|
107
|
+
// on the reference. That's fine — the caller re-runs update() when coords change.
|
|
108
|
+
var isVirtual = !('nodeType' in opts.anchor);
|
|
109
|
+
if (isVirtual) {
|
|
110
|
+
applyPosition();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
cleanup = autoUpdate(opts.anchor, node, applyPosition);
|
|
114
|
+
}
|
|
115
|
+
function handleOutsidePointer(e) {
|
|
116
|
+
var _a;
|
|
117
|
+
if (!node.contains(e.target)) {
|
|
118
|
+
(_a = opts.onDismiss) === null || _a === void 0 ? void 0 : _a.call(opts);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function handleKeydown(e) {
|
|
122
|
+
var _a;
|
|
123
|
+
if (e.key === 'Escape') {
|
|
124
|
+
(_a = opts.onDismiss) === null || _a === void 0 ? void 0 : _a.call(opts);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function returnFocus() {
|
|
128
|
+
if (returnFocusTarget && typeof returnFocusTarget.focus === 'function') {
|
|
129
|
+
returnFocusTarget.focus();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
startAutoUpdate();
|
|
133
|
+
// Animate in after first position has been applied (next microtask)
|
|
134
|
+
requestAnimationFrame(function () {
|
|
135
|
+
node.style.opacity = '1';
|
|
136
|
+
node.style.transform = 'translateY(0)';
|
|
137
|
+
});
|
|
138
|
+
// Defer outside-pointer listeners to avoid catching the opening event itself
|
|
139
|
+
var listenerTimer = setTimeout(function () {
|
|
140
|
+
document.addEventListener('click', handleOutsidePointer, true);
|
|
141
|
+
document.addEventListener('contextmenu', handleOutsidePointer, true);
|
|
142
|
+
}, 0);
|
|
143
|
+
document.addEventListener('keydown', handleKeydown);
|
|
144
|
+
return {
|
|
145
|
+
update: function (next) {
|
|
146
|
+
opts = __assign({ placement: 'bottom-start', flip: true, shift: true, offset: 4 }, next);
|
|
147
|
+
startAutoUpdate();
|
|
148
|
+
},
|
|
149
|
+
destroy: function () {
|
|
150
|
+
clearTimeout(listenerTimer);
|
|
151
|
+
cleanup === null || cleanup === void 0 ? void 0 : cleanup();
|
|
152
|
+
document.removeEventListener('click', handleOutsidePointer, true);
|
|
153
|
+
document.removeEventListener('contextmenu', handleOutsidePointer, true);
|
|
154
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
155
|
+
returnFocus();
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface StaggerEntranceOptions {
|
|
2
|
+
/** Delay between successive children in ms. Default 35. */
|
|
3
|
+
stepMs?: number;
|
|
4
|
+
/** Maximum total stagger time in ms; caps step for large lists. Default 250. */
|
|
5
|
+
maxTotalMs?: number;
|
|
6
|
+
/** translateY distance in px for the rise effect. Default 8. */
|
|
7
|
+
rise?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Stagger-entrance action — apply to a list container to fan children in on
|
|
11
|
+
* mount with a sequenced fade + rise. Each child fades from opacity 0 → 1 and
|
|
12
|
+
* rises from translateY(risepx) → 0, staggered by stepMs.
|
|
13
|
+
*
|
|
14
|
+
* The total stagger time is capped at maxTotalMs regardless of child count,
|
|
15
|
+
* so large lists don't drag on. Runs once per mount; re-mount re-runs it.
|
|
16
|
+
*
|
|
17
|
+
* Reduced-motion: children are set visible immediately with no transition.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* <ul use:staggerEntrance={{ stepMs: 35, maxTotalMs: 250, rise: 8 }}>
|
|
21
|
+
* {#each items as item}
|
|
22
|
+
* <li>{item}</li>
|
|
23
|
+
* {/each}
|
|
24
|
+
* </ul>
|
|
25
|
+
*/
|
|
26
|
+
export declare function staggerEntrance(node: HTMLElement, options?: StaggerEntranceOptions): {
|
|
27
|
+
destroy(): void;
|
|
28
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stagger-entrance action — apply to a list container to fan children in on
|
|
3
|
+
* mount with a sequenced fade + rise. Each child fades from opacity 0 → 1 and
|
|
4
|
+
* rises from translateY(risepx) → 0, staggered by stepMs.
|
|
5
|
+
*
|
|
6
|
+
* The total stagger time is capped at maxTotalMs regardless of child count,
|
|
7
|
+
* so large lists don't drag on. Runs once per mount; re-mount re-runs it.
|
|
8
|
+
*
|
|
9
|
+
* Reduced-motion: children are set visible immediately with no transition.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* <ul use:staggerEntrance={{ stepMs: 35, maxTotalMs: 250, rise: 8 }}>
|
|
13
|
+
* {#each items as item}
|
|
14
|
+
* <li>{item}</li>
|
|
15
|
+
* {/each}
|
|
16
|
+
* </ul>
|
|
17
|
+
*/
|
|
18
|
+
export function staggerEntrance(node, options) {
|
|
19
|
+
if (options === void 0) { options = {}; }
|
|
20
|
+
var _a = options.stepMs, stepMs = _a === void 0 ? 35 : _a, _b = options.maxTotalMs, maxTotalMs = _b === void 0 ? 250 : _b, _c = options.rise, rise = _c === void 0 ? 8 : _c;
|
|
21
|
+
var prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
22
|
+
var children = Array.from(node.children);
|
|
23
|
+
if (prefersReduced || children.length === 0) {
|
|
24
|
+
// Render final state immediately — no animation
|
|
25
|
+
return { destroy: function () { } };
|
|
26
|
+
}
|
|
27
|
+
// Cap the per-item step so total time doesn't exceed maxTotalMs
|
|
28
|
+
var effectiveStep = children.length > 1
|
|
29
|
+
? Math.min(stepMs, Math.floor(maxTotalMs / (children.length - 1)))
|
|
30
|
+
: stepMs;
|
|
31
|
+
var duration = 200; // var(--motion-enter) = 200ms
|
|
32
|
+
// Set all children to their start state (opacity 0, risen)
|
|
33
|
+
for (var _i = 0, children_1 = children; _i < children_1.length; _i++) {
|
|
34
|
+
var child = children_1[_i];
|
|
35
|
+
child.style.opacity = '0';
|
|
36
|
+
child.style.transform = "translateY(".concat(rise, "px)");
|
|
37
|
+
child.style.transition = 'none';
|
|
38
|
+
}
|
|
39
|
+
// Trigger entrance in next frame so the start state has been painted
|
|
40
|
+
var rafId = requestAnimationFrame(function () {
|
|
41
|
+
children.forEach(function (child, i) {
|
|
42
|
+
var delay = i * effectiveStep;
|
|
43
|
+
child.style.transition = "opacity ".concat(duration, "ms var(--motion-glide) ").concat(delay, "ms, transform ").concat(duration, "ms var(--motion-glide) ").concat(delay, "ms");
|
|
44
|
+
child.style.opacity = '1';
|
|
45
|
+
child.style.transform = 'translateY(0)';
|
|
46
|
+
});
|
|
47
|
+
// Clean up inline styles after all transitions complete
|
|
48
|
+
var totalMs = (children.length - 1) * effectiveStep + duration + 50;
|
|
49
|
+
var cleanupId = setTimeout(function () {
|
|
50
|
+
for (var _i = 0, children_2 = children; _i < children_2.length; _i++) {
|
|
51
|
+
var child = children_2[_i];
|
|
52
|
+
child.style.opacity = '';
|
|
53
|
+
child.style.transform = '';
|
|
54
|
+
child.style.transition = '';
|
|
55
|
+
}
|
|
56
|
+
}, totalMs);
|
|
57
|
+
return function () { return clearTimeout(cleanupId); };
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
destroy: function () {
|
|
61
|
+
cancelAnimationFrame(rafId);
|
|
62
|
+
// Ensure children are visible if action is destroyed mid-animation
|
|
63
|
+
for (var _i = 0, children_3 = children; _i < children_3.length; _i++) {
|
|
64
|
+
var child = children_3[_i];
|
|
65
|
+
child.style.opacity = '';
|
|
66
|
+
child.style.transform = '';
|
|
67
|
+
child.style.transition = '';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tooltip Svelte action — use:tooltip={{ text, placement?, delayMs? }}
|
|
3
|
+
*
|
|
4
|
+
* Warm/cool window: the first tooltip on a page waits delayMs (default 400ms)
|
|
5
|
+
* before appearing. If another tooltip was shown within the last ~500ms the
|
|
6
|
+
* warm window is still active, so the new tooltip appears instantly — the
|
|
7
|
+
* cursor is already "in tooltip mode".
|
|
8
|
+
*
|
|
9
|
+
* Animation: fade-in + 2px upward rise, var(--motion-glide) var(--motion-fast).
|
|
10
|
+
* Exit is instant (no delay, no motion) so it disappears as soon as you leave.
|
|
11
|
+
*
|
|
12
|
+
* Placement: 'top' | 'bottom' | 'left' | 'right' (default 'top'). Flips to
|
|
13
|
+
* the opposite side when the tooltip would overflow the viewport.
|
|
14
|
+
*
|
|
15
|
+
* Accessibility: sets aria-describedby on the host element.
|
|
16
|
+
* Escape key hides the active tooltip.
|
|
17
|
+
* Focus events (focusin/focusout) trigger alongside mouse hover.
|
|
18
|
+
*/
|
|
19
|
+
export interface TooltipOptions {
|
|
20
|
+
text: string;
|
|
21
|
+
placement?: 'top' | 'bottom' | 'left' | 'right';
|
|
22
|
+
/** ms to wait before showing (default 400). 0 = instant. */
|
|
23
|
+
delayMs?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function tooltip(node: HTMLElement, options: TooltipOptions): {
|
|
26
|
+
update(next: TooltipOptions): void;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tooltip Svelte action — use:tooltip={{ text, placement?, delayMs? }}
|
|
3
|
+
*
|
|
4
|
+
* Warm/cool window: the first tooltip on a page waits delayMs (default 400ms)
|
|
5
|
+
* before appearing. If another tooltip was shown within the last ~500ms the
|
|
6
|
+
* warm window is still active, so the new tooltip appears instantly — the
|
|
7
|
+
* cursor is already "in tooltip mode".
|
|
8
|
+
*
|
|
9
|
+
* Animation: fade-in + 2px upward rise, var(--motion-glide) var(--motion-fast).
|
|
10
|
+
* Exit is instant (no delay, no motion) so it disappears as soon as you leave.
|
|
11
|
+
*
|
|
12
|
+
* Placement: 'top' | 'bottom' | 'left' | 'right' (default 'top'). Flips to
|
|
13
|
+
* the opposite side when the tooltip would overflow the viewport.
|
|
14
|
+
*
|
|
15
|
+
* Accessibility: sets aria-describedby on the host element.
|
|
16
|
+
* Escape key hides the active tooltip.
|
|
17
|
+
* Focus events (focusin/focusout) trigger alongside mouse hover.
|
|
18
|
+
*/
|
|
19
|
+
var __assign = (this && this.__assign) || function () {
|
|
20
|
+
__assign = Object.assign || function(t) {
|
|
21
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
22
|
+
s = arguments[i];
|
|
23
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
24
|
+
t[p] = s[p];
|
|
25
|
+
}
|
|
26
|
+
return t;
|
|
27
|
+
};
|
|
28
|
+
return __assign.apply(this, arguments);
|
|
29
|
+
};
|
|
30
|
+
var COOL_WINDOW_MS = 500;
|
|
31
|
+
var GAP = 8;
|
|
32
|
+
var tooltipEl = null;
|
|
33
|
+
var lastHideTime = 0;
|
|
34
|
+
var idCounter = 0;
|
|
35
|
+
function getOrCreatePortal() {
|
|
36
|
+
if (tooltipEl)
|
|
37
|
+
return tooltipEl;
|
|
38
|
+
tooltipEl = document.createElement('div');
|
|
39
|
+
tooltipEl.id = 'jatui-tooltip-portal';
|
|
40
|
+
tooltipEl.style.cssText = "\n\t\tposition: fixed;\n\t\ttop: 0; left: 0;\n\t\tpointer-events: none;\n\t\tz-index: 9999;\n\t";
|
|
41
|
+
document.body.appendChild(tooltipEl);
|
|
42
|
+
return tooltipEl;
|
|
43
|
+
}
|
|
44
|
+
function createBubble(text) {
|
|
45
|
+
var el = document.createElement('div');
|
|
46
|
+
el.role = 'tooltip';
|
|
47
|
+
el.textContent = text;
|
|
48
|
+
el.style.cssText = "\n\t\tposition: absolute;\n\t\tpadding: 4px 10px;\n\t\tborder-radius: 6px;\n\t\tfont-size: 0.8125rem;\n\t\tline-height: 1.4;\n\t\twhite-space: nowrap;\n\t\tmax-width: 240px;\n\t\twhite-space: normal;\n\t\tword-break: break-word;\n\t\tbackground: oklch(var(--b3, 20% 0 0));\n\t\tcolor: oklch(var(--bc, 95% 0 0));\n\t\tbox-shadow: 0 2px 6px oklch(0% 0 0 / 18%);\n\t\topacity: 0;\n\t\ttransform: translateY(4px);\n\t\ttransition:\n\t\t\topacity var(--motion-fast, 150ms) var(--motion-glide, cubic-bezier(0.16, 1, 0.3, 1)),\n\t\t\ttransform var(--motion-fast, 150ms) var(--motion-glide, cubic-bezier(0.16, 1, 0.3, 1));\n\t";
|
|
49
|
+
// reduced-motion: remove transform, keep opacity fade
|
|
50
|
+
el.dataset.tooltipBubble = '1';
|
|
51
|
+
return el;
|
|
52
|
+
}
|
|
53
|
+
function positionBubble(bubble, anchor, placement) {
|
|
54
|
+
var rect = anchor.getBoundingClientRect();
|
|
55
|
+
var bw = bubble.offsetWidth;
|
|
56
|
+
var bh = bubble.offsetHeight;
|
|
57
|
+
var vw = window.innerWidth;
|
|
58
|
+
var vh = window.innerHeight;
|
|
59
|
+
var top;
|
|
60
|
+
var left;
|
|
61
|
+
var resolved = placement;
|
|
62
|
+
// Compute primary position, then flip if needed
|
|
63
|
+
var placements = {
|
|
64
|
+
top: function () { return ({ top: rect.top - bh - GAP, left: rect.left + rect.width / 2 - bw / 2 }); },
|
|
65
|
+
bottom: function () { return ({ top: rect.bottom + GAP, left: rect.left + rect.width / 2 - bw / 2 }); },
|
|
66
|
+
left: function () { return ({ top: rect.top + rect.height / 2 - bh / 2, left: rect.left - bw - GAP }); },
|
|
67
|
+
right: function () { return ({ top: rect.top + rect.height / 2 - bh / 2, left: rect.right + GAP }); },
|
|
68
|
+
};
|
|
69
|
+
var pos = placements[resolved]();
|
|
70
|
+
top = pos.top;
|
|
71
|
+
left = pos.left;
|
|
72
|
+
// Flip on viewport edge
|
|
73
|
+
if (resolved === 'top' && top < 0) {
|
|
74
|
+
resolved = 'bottom';
|
|
75
|
+
var p = placements.bottom();
|
|
76
|
+
top = p.top;
|
|
77
|
+
left = p.left;
|
|
78
|
+
}
|
|
79
|
+
else if (resolved === 'bottom' && top + bh > vh) {
|
|
80
|
+
resolved = 'top';
|
|
81
|
+
var p = placements.top();
|
|
82
|
+
top = p.top;
|
|
83
|
+
left = p.left;
|
|
84
|
+
}
|
|
85
|
+
else if (resolved === 'left' && left < 0) {
|
|
86
|
+
resolved = 'right';
|
|
87
|
+
var p = placements.right();
|
|
88
|
+
top = p.top;
|
|
89
|
+
left = p.left;
|
|
90
|
+
}
|
|
91
|
+
else if (resolved === 'right' && left + bw > vw) {
|
|
92
|
+
resolved = 'left';
|
|
93
|
+
var p = placements.left();
|
|
94
|
+
top = p.top;
|
|
95
|
+
left = p.left;
|
|
96
|
+
}
|
|
97
|
+
// Clamp horizontal within viewport
|
|
98
|
+
left = Math.max(GAP, Math.min(left, vw - bw - GAP));
|
|
99
|
+
top = Math.max(GAP, Math.min(top, vh - bh - GAP));
|
|
100
|
+
// Rise direction depends on resolved placement
|
|
101
|
+
var riseY = resolved === 'bottom' ? -4 : 4;
|
|
102
|
+
bubble.style.setProperty('--tooltip-rise-y', "".concat(riseY, "px"));
|
|
103
|
+
bubble.style.top = "".concat(top, "px");
|
|
104
|
+
bubble.style.left = "".concat(left, "px");
|
|
105
|
+
}
|
|
106
|
+
function isWarm() {
|
|
107
|
+
return Date.now() - lastHideTime < COOL_WINDOW_MS;
|
|
108
|
+
}
|
|
109
|
+
export function tooltip(node, options) {
|
|
110
|
+
var opts = __assign({ placement: 'top', delayMs: 400 }, options);
|
|
111
|
+
var bubble = null;
|
|
112
|
+
var showTimer = null;
|
|
113
|
+
var tooltipId = "jatui-tt-".concat(++idCounter);
|
|
114
|
+
function show() {
|
|
115
|
+
var portal = getOrCreatePortal();
|
|
116
|
+
if (bubble) {
|
|
117
|
+
// Already showing — update text and reposition
|
|
118
|
+
bubble.textContent = opts.text;
|
|
119
|
+
positionBubble(bubble, node, opts.placement);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
bubble = createBubble(opts.text);
|
|
123
|
+
bubble.id = tooltipId;
|
|
124
|
+
portal.appendChild(bubble);
|
|
125
|
+
node.setAttribute('aria-describedby', tooltipId);
|
|
126
|
+
// Force a reflow so the initial transform is applied before transitioning
|
|
127
|
+
void bubble.offsetHeight;
|
|
128
|
+
// Measure and position after layout
|
|
129
|
+
positionBubble(bubble, node, opts.placement);
|
|
130
|
+
// Animate in
|
|
131
|
+
bubble.style.opacity = '1';
|
|
132
|
+
bubble.style.transform = 'translateY(0)';
|
|
133
|
+
}
|
|
134
|
+
function hide() {
|
|
135
|
+
if (showTimer) {
|
|
136
|
+
clearTimeout(showTimer);
|
|
137
|
+
showTimer = null;
|
|
138
|
+
}
|
|
139
|
+
if (bubble) {
|
|
140
|
+
lastHideTime = Date.now();
|
|
141
|
+
bubble.remove();
|
|
142
|
+
bubble = null;
|
|
143
|
+
node.removeAttribute('aria-describedby');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function scheduleShow() {
|
|
147
|
+
if (showTimer || bubble)
|
|
148
|
+
return;
|
|
149
|
+
var delay = isWarm() ? 0 : opts.delayMs;
|
|
150
|
+
if (delay === 0) {
|
|
151
|
+
show();
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
showTimer = setTimeout(function () {
|
|
155
|
+
showTimer = null;
|
|
156
|
+
show();
|
|
157
|
+
}, delay);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function handleKeydown(e) {
|
|
161
|
+
if (e.key === 'Escape')
|
|
162
|
+
hide();
|
|
163
|
+
}
|
|
164
|
+
node.addEventListener('mouseenter', scheduleShow);
|
|
165
|
+
node.addEventListener('mouseleave', hide);
|
|
166
|
+
node.addEventListener('focusin', scheduleShow);
|
|
167
|
+
node.addEventListener('focusout', hide);
|
|
168
|
+
document.addEventListener('keydown', handleKeydown);
|
|
169
|
+
return {
|
|
170
|
+
update: function (next) {
|
|
171
|
+
opts = __assign({ placement: 'top', delayMs: 400 }, next);
|
|
172
|
+
if (bubble) {
|
|
173
|
+
bubble.textContent = opts.text;
|
|
174
|
+
positionBubble(bubble, node, opts.placement);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
destroy: function () {
|
|
178
|
+
hide();
|
|
179
|
+
node.removeEventListener('mouseenter', scheduleShow);
|
|
180
|
+
node.removeEventListener('mouseleave', hide);
|
|
181
|
+
node.removeEventListener('focusin', scheduleShow);
|
|
182
|
+
node.removeEventListener('focusout', hide);
|
|
183
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|