@joewinke/jatui 0.1.33 → 0.2.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/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/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/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/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 +59 -0
- package/dist/index.js +67 -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/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/index.d.ts +36 -0
- package/dist/showcase/index.js +46 -0
- package/dist/showcase/scroll/SmoothScroll.svelte +49 -0
- package/dist/showcase/scroll/SmoothScroll.svelte.d.ts +12 -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 +50 -3
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
text?: string;
|
|
4
|
+
getText?: () => string | Promise<string>;
|
|
5
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
6
|
+
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'error' | 'warning' | 'success' | 'info';
|
|
7
|
+
label?: string;
|
|
8
|
+
copiedLabel?: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
text,
|
|
14
|
+
getText,
|
|
15
|
+
size = 'sm',
|
|
16
|
+
variant = 'ghost',
|
|
17
|
+
label = 'Copy',
|
|
18
|
+
copiedLabel = 'Copied',
|
|
19
|
+
class: className = ''
|
|
20
|
+
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
let copied = $state(false);
|
|
23
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
24
|
+
|
|
25
|
+
const sizeClass = $derived(
|
|
26
|
+
({ xs: 'btn-xs', sm: 'btn-sm', md: '', lg: 'btn-lg' } as Record<string, string>)[size] ?? ''
|
|
27
|
+
);
|
|
28
|
+
const variantClass = $derived(
|
|
29
|
+
({
|
|
30
|
+
primary: 'btn-primary',
|
|
31
|
+
secondary: 'btn-secondary',
|
|
32
|
+
outline: 'btn-outline',
|
|
33
|
+
ghost: 'btn-ghost',
|
|
34
|
+
error: 'btn-error',
|
|
35
|
+
warning: 'btn-warning',
|
|
36
|
+
success: 'btn-success',
|
|
37
|
+
info: 'btn-info'
|
|
38
|
+
} as Record<string, string>)[variant] ?? 'btn-ghost'
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
async function handleCopy() {
|
|
42
|
+
if (copied) return;
|
|
43
|
+
try {
|
|
44
|
+
const value = getText ? await getText() : (text ?? '');
|
|
45
|
+
await navigator.clipboard.writeText(value);
|
|
46
|
+
copied = true;
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
timer = setTimeout(() => {
|
|
49
|
+
copied = false;
|
|
50
|
+
}, 1400);
|
|
51
|
+
} catch {
|
|
52
|
+
// clipboard not available — no-op
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
class="btn {variantClass} {sizeClass} copy-btn {className}"
|
|
60
|
+
class:copied
|
|
61
|
+
onclick={handleCopy}
|
|
62
|
+
aria-label={copied ? copiedLabel : label}
|
|
63
|
+
>
|
|
64
|
+
<span class="copy-icon-wrap" aria-hidden="true">
|
|
65
|
+
<!-- Copy icon -->
|
|
66
|
+
<svg
|
|
67
|
+
class="ic-copy"
|
|
68
|
+
width="1em"
|
|
69
|
+
height="1em"
|
|
70
|
+
viewBox="0 0 24 24"
|
|
71
|
+
fill="none"
|
|
72
|
+
stroke="currentColor"
|
|
73
|
+
stroke-width="2"
|
|
74
|
+
stroke-linecap="round"
|
|
75
|
+
stroke-linejoin="round"
|
|
76
|
+
>
|
|
77
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
78
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
79
|
+
</svg>
|
|
80
|
+
<!-- Check icon -->
|
|
81
|
+
<svg
|
|
82
|
+
class="ic-check"
|
|
83
|
+
width="1em"
|
|
84
|
+
height="1em"
|
|
85
|
+
viewBox="0 0 24 24"
|
|
86
|
+
fill="none"
|
|
87
|
+
stroke="currentColor"
|
|
88
|
+
stroke-width="2.5"
|
|
89
|
+
stroke-linecap="round"
|
|
90
|
+
stroke-linejoin="round"
|
|
91
|
+
>
|
|
92
|
+
<polyline points="20 6 9 17 4 12" />
|
|
93
|
+
</svg>
|
|
94
|
+
</span>
|
|
95
|
+
<span class="copy-label">{copied ? copiedLabel : label}</span>
|
|
96
|
+
|
|
97
|
+
<!-- Screen-reader live announcement -->
|
|
98
|
+
<span class="sr-only" aria-live="polite" aria-atomic="true">
|
|
99
|
+
{copied ? copiedLabel : ''}
|
|
100
|
+
</span>
|
|
101
|
+
</button>
|
|
102
|
+
|
|
103
|
+
<style>
|
|
104
|
+
.copy-btn {
|
|
105
|
+
gap: 0.35em;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.copy-icon-wrap {
|
|
109
|
+
position: relative;
|
|
110
|
+
display: inline-flex;
|
|
111
|
+
width: 1em;
|
|
112
|
+
height: 1em;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.copy-icon-wrap svg {
|
|
116
|
+
position: absolute;
|
|
117
|
+
inset: 0;
|
|
118
|
+
transition:
|
|
119
|
+
opacity var(--motion-fast, 150ms) var(--motion-glide, cubic-bezier(0.16, 1, 0.3, 1)),
|
|
120
|
+
transform var(--motion-fast, 150ms) var(--motion-spring, cubic-bezier(0.34, 1.56, 0.64, 1));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.ic-check {
|
|
124
|
+
opacity: 0;
|
|
125
|
+
transform: scale(0.5);
|
|
126
|
+
color: oklch(var(--su, 0.72 0.19 150));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Use DaisyUI success color if --su is not available */
|
|
130
|
+
.ic-check {
|
|
131
|
+
color: var(--color-success, #4cd08a);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Copied state */
|
|
135
|
+
.copy-btn.copied .ic-copy {
|
|
136
|
+
opacity: 0;
|
|
137
|
+
transform: scale(0.5);
|
|
138
|
+
}
|
|
139
|
+
.copy-btn.copied .ic-check {
|
|
140
|
+
opacity: 1;
|
|
141
|
+
transform: scale(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.copy-label {
|
|
145
|
+
transition: opacity var(--motion-fast, 150ms) var(--motion-glide, cubic-bezier(0.16, 1, 0.3, 1));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@media (prefers-reduced-motion: reduce) {
|
|
149
|
+
.copy-icon-wrap svg {
|
|
150
|
+
transition: opacity var(--motion-instant, 80ms) step-end;
|
|
151
|
+
}
|
|
152
|
+
.copy-label {
|
|
153
|
+
transition: none;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
text?: string;
|
|
3
|
+
getText?: () => string | Promise<string>;
|
|
4
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
5
|
+
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'error' | 'warning' | 'success' | 'info';
|
|
6
|
+
label?: string;
|
|
7
|
+
copiedLabel?: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const CopyButton: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type CopyButton = ReturnType<typeof CopyButton>;
|
|
12
|
+
export default CopyButton;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
label?: string;
|
|
4
|
+
holdMs?: number;
|
|
5
|
+
onconfirm?: () => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
label = 'Hold to confirm',
|
|
13
|
+
holdMs = 800,
|
|
14
|
+
onconfirm,
|
|
15
|
+
disabled = false,
|
|
16
|
+
size = 'md',
|
|
17
|
+
class: className = ''
|
|
18
|
+
}: Props = $props();
|
|
19
|
+
|
|
20
|
+
const RADIUS = 33;
|
|
21
|
+
const CIRCUMFERENCE = 2 * Math.PI * RADIUS; // ≈ 207.3
|
|
22
|
+
|
|
23
|
+
let holding = $state(false);
|
|
24
|
+
let done = $state(false);
|
|
25
|
+
let progress = $state(0); // 0–1
|
|
26
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
27
|
+
let raf: number | undefined;
|
|
28
|
+
let startTime: number;
|
|
29
|
+
|
|
30
|
+
const hintId = `htc-hint-${Math.random().toString(36).slice(2)}`;
|
|
31
|
+
|
|
32
|
+
function startHold() {
|
|
33
|
+
if (disabled || done) return;
|
|
34
|
+
holding = true;
|
|
35
|
+
startTime = performance.now();
|
|
36
|
+
|
|
37
|
+
function tick(now: number) {
|
|
38
|
+
progress = Math.min((now - startTime) / holdMs, 1);
|
|
39
|
+
if (progress < 1) {
|
|
40
|
+
raf = requestAnimationFrame(tick);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
raf = requestAnimationFrame(tick);
|
|
44
|
+
|
|
45
|
+
timer = setTimeout(() => {
|
|
46
|
+
holding = false;
|
|
47
|
+
done = true;
|
|
48
|
+
progress = 1;
|
|
49
|
+
onconfirm?.();
|
|
50
|
+
// spring settle then reset
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
done = false;
|
|
53
|
+
progress = 0;
|
|
54
|
+
}, 600);
|
|
55
|
+
}, holdMs);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cancelHold() {
|
|
59
|
+
if (!holding) return;
|
|
60
|
+
holding = false;
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
cancelAnimationFrame(raf!);
|
|
63
|
+
// quick drain
|
|
64
|
+
progress = 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onkeydown(e: KeyboardEvent) {
|
|
68
|
+
if ((e.key === ' ' || e.key === 'Enter') && !e.repeat) {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
startHold();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function onkeyup(e: KeyboardEvent) {
|
|
75
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
76
|
+
cancelHold();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const strokeOffset = $derived(CIRCUMFERENCE * (1 - progress));
|
|
81
|
+
|
|
82
|
+
const sizeMap: Record<string, { btn: string; ring: number; font: string }> = {
|
|
83
|
+
xs: { btn: 'btn-xs', ring: 40, font: 'text-xs' },
|
|
84
|
+
sm: { btn: 'btn-sm', ring: 52, font: 'text-sm' },
|
|
85
|
+
md: { btn: '', ring: 64, font: 'text-sm' },
|
|
86
|
+
lg: { btn: 'btn-lg', ring: 80, font: 'text-base' }
|
|
87
|
+
};
|
|
88
|
+
const sz = $derived(sizeMap[size] ?? sizeMap.md);
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
class="hold-btn btn btn-error {sz.btn} relative inline-flex items-center gap-2 select-none {className}"
|
|
94
|
+
class:holding
|
|
95
|
+
class:done
|
|
96
|
+
{disabled}
|
|
97
|
+
aria-describedby={hintId}
|
|
98
|
+
onpointerdown={startHold}
|
|
99
|
+
onpointerup={cancelHold}
|
|
100
|
+
onpointerleave={cancelHold}
|
|
101
|
+
onpointercancel={cancelHold}
|
|
102
|
+
{onkeydown}
|
|
103
|
+
{onkeyup}
|
|
104
|
+
>
|
|
105
|
+
<!-- Radial progress ring -->
|
|
106
|
+
<span class="ring-wrap" aria-hidden="true" style="width:{sz.ring}px;height:{sz.ring}px;">
|
|
107
|
+
<svg viewBox="0 0 72 72" class="ring-svg" style="width:{sz.ring}px;height:{sz.ring}px;">
|
|
108
|
+
<!-- track -->
|
|
109
|
+
<circle
|
|
110
|
+
class="ring-track"
|
|
111
|
+
cx="36"
|
|
112
|
+
cy="36"
|
|
113
|
+
r={RADIUS}
|
|
114
|
+
fill="none"
|
|
115
|
+
stroke-width="4"
|
|
116
|
+
stroke-linecap="round"
|
|
117
|
+
/>
|
|
118
|
+
<!-- progress arc, starts at top (rotate -90deg) -->
|
|
119
|
+
<circle
|
|
120
|
+
class="ring-prog"
|
|
121
|
+
cx="36"
|
|
122
|
+
cy="36"
|
|
123
|
+
r={RADIUS}
|
|
124
|
+
fill="none"
|
|
125
|
+
stroke-width="4"
|
|
126
|
+
stroke-linecap="round"
|
|
127
|
+
stroke-dasharray={CIRCUMFERENCE}
|
|
128
|
+
stroke-dashoffset={strokeOffset}
|
|
129
|
+
transform="rotate(-90 36 36)"
|
|
130
|
+
/>
|
|
131
|
+
</svg>
|
|
132
|
+
</span>
|
|
133
|
+
|
|
134
|
+
<span class={sz.font}>{done ? 'Confirmed' : label}</span>
|
|
135
|
+
</button>
|
|
136
|
+
|
|
137
|
+
<!-- Accessible hint (screen-reader only, referenced by aria-describedby) -->
|
|
138
|
+
<span id={hintId} class="sr-only">Press and hold to confirm. Release early to cancel.</span>
|
|
139
|
+
|
|
140
|
+
<style>
|
|
141
|
+
.hold-btn {
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
}
|
|
144
|
+
.hold-btn:disabled {
|
|
145
|
+
cursor: not-allowed;
|
|
146
|
+
opacity: 0.5;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.ring-wrap {
|
|
150
|
+
display: inline-flex;
|
|
151
|
+
flex-shrink: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.ring-track {
|
|
155
|
+
stroke: oklch(from currentColor l c h / 0.2);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.ring-prog {
|
|
159
|
+
stroke: currentColor;
|
|
160
|
+
/* Default: quick drain on release */
|
|
161
|
+
transition: stroke-dashoffset var(--motion-fast, 150ms) var(--motion-glide, cubic-bezier(0.16, 1, 0.3, 1));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* While holding: fill over holdMs (JS drives via $state) */
|
|
165
|
+
.hold-btn.holding .ring-prog,
|
|
166
|
+
.hold-btn.done .ring-prog {
|
|
167
|
+
transition: none; /* JS RAF drives progress; no CSS transition needed */
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* On done: spring settle */
|
|
171
|
+
.hold-btn.done .ring-prog {
|
|
172
|
+
transition: stroke-dashoffset var(--motion-fast, 150ms) var(--motion-spring, cubic-bezier(0.34, 1.56, 0.64, 1));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@media (prefers-reduced-motion: reduce) {
|
|
176
|
+
.ring-prog {
|
|
177
|
+
transition: none;
|
|
178
|
+
}
|
|
179
|
+
.hold-btn.done .ring-prog {
|
|
180
|
+
transition: none;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
label?: string;
|
|
3
|
+
holdMs?: number;
|
|
4
|
+
onconfirm?: () => void;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const HoldToConfirm: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type HoldToConfirm = ReturnType<typeof HoldToConfirm>;
|
|
11
|
+
export default HoldToConfirm;
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* `Escape` to dismiss. Input/contenteditable fields and Ctrl/Meta/Alt
|
|
6
6
|
* combos are ignored so the shortcut never interferes with typing.
|
|
7
7
|
*
|
|
8
|
+
* While open, physical keydown events depress the matching on-screen keycap
|
|
9
|
+
* with a brief translateY(1px) + tint (var(--motion-instant)).
|
|
10
|
+
*
|
|
8
11
|
* Usage:
|
|
9
12
|
*
|
|
10
13
|
* <KeyboardShortcutsOverlay shortcuts={[
|
|
@@ -57,6 +60,10 @@
|
|
|
57
60
|
open = isOpen;
|
|
58
61
|
});
|
|
59
62
|
|
|
63
|
+
// Track which key label is currently depressed
|
|
64
|
+
let depressedKey = $state<string | null>(null);
|
|
65
|
+
let depressTimer: ReturnType<typeof setTimeout> | null = null;
|
|
66
|
+
|
|
60
67
|
function isTypingTarget(target: EventTarget | null): boolean {
|
|
61
68
|
if (!(target instanceof HTMLElement)) return false;
|
|
62
69
|
const tag = target.tagName;
|
|
@@ -78,12 +85,34 @@
|
|
|
78
85
|
if (isOpen && e.key === 'Escape') {
|
|
79
86
|
e.preventDefault();
|
|
80
87
|
isOpen = false;
|
|
88
|
+
return;
|
|
81
89
|
}
|
|
90
|
+
|
|
91
|
+
// Keycap depress — highlight matching key while overlay is open
|
|
92
|
+
if (isOpen) {
|
|
93
|
+
depressKey(e.key);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function depressKey(key: string) {
|
|
98
|
+
if (depressTimer) clearTimeout(depressTimer);
|
|
99
|
+
depressedKey = key;
|
|
100
|
+
depressTimer = setTimeout(() => { depressedKey = null; }, 120);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function keyMatchesPressed(keyLabel: string, pressed: string | null): boolean {
|
|
104
|
+
if (!pressed) return false;
|
|
105
|
+
// Match single-char labels directly
|
|
106
|
+
const parts = keyLabel.split(/\s*\/\s*/);
|
|
107
|
+
return parts.some(p => p.trim() === pressed || p.trim().toLowerCase() === pressed.toLowerCase());
|
|
82
108
|
}
|
|
83
109
|
|
|
84
110
|
$effect(() => {
|
|
85
111
|
window.addEventListener('keydown', onKeydown);
|
|
86
|
-
return () =>
|
|
112
|
+
return () => {
|
|
113
|
+
window.removeEventListener('keydown', onKeydown);
|
|
114
|
+
if (depressTimer) clearTimeout(depressTimer);
|
|
115
|
+
};
|
|
87
116
|
});
|
|
88
117
|
</script>
|
|
89
118
|
|
|
@@ -121,6 +150,7 @@
|
|
|
121
150
|
<div class="kso-row">
|
|
122
151
|
<kbd
|
|
123
152
|
class="kso-key"
|
|
153
|
+
class:kso-key-pressed={keyMatchesPressed(key, depressedKey)}
|
|
124
154
|
style={color
|
|
125
155
|
? `color:${color};border-color:color-mix(in oklch,${color} 50%,oklch(0.35 0.02 250));background:color-mix(in oklch,${color} 12%,oklch(0.25 0.02 250))`
|
|
126
156
|
: ''}
|
|
@@ -134,17 +164,20 @@
|
|
|
134
164
|
{:else}
|
|
135
165
|
{#each shortcuts as { key, description }, i (key + '::' + i)}
|
|
136
166
|
<div class="kso-row">
|
|
137
|
-
<kbd
|
|
167
|
+
<kbd
|
|
168
|
+
class="kso-key"
|
|
169
|
+
class:kso-key-pressed={keyMatchesPressed(key, depressedKey)}
|
|
170
|
+
>{key}</kbd>
|
|
138
171
|
<span class="kso-description">{description}</span>
|
|
139
172
|
</div>
|
|
140
173
|
{/each}
|
|
141
174
|
{/if}
|
|
142
175
|
<div class="kso-row kso-row-meta">
|
|
143
|
-
<kbd class="kso-key">?</kbd>
|
|
176
|
+
<kbd class="kso-key" class:kso-key-pressed={keyMatchesPressed('?', depressedKey)}>?</kbd>
|
|
144
177
|
<span class="kso-description">Toggle this overlay</span>
|
|
145
178
|
</div>
|
|
146
179
|
<div class="kso-row kso-row-meta">
|
|
147
|
-
<kbd class="kso-key">Esc</kbd>
|
|
180
|
+
<kbd class="kso-key" class:kso-key-pressed={keyMatchesPressed('Escape', depressedKey)}>Esc</kbd>
|
|
148
181
|
<span class="kso-description">Close overlay</span>
|
|
149
182
|
</div>
|
|
150
183
|
</div>
|
|
@@ -270,12 +303,18 @@
|
|
|
270
303
|
color: oklch(0.93 0.02 250);
|
|
271
304
|
flex-shrink: 0;
|
|
272
305
|
white-space: nowrap;
|
|
306
|
+
/* Keycap physical press: quick down + tint, instant easing */
|
|
307
|
+
transition:
|
|
308
|
+
transform var(--motion-instant) ease-out,
|
|
309
|
+
background var(--motion-instant) ease-out,
|
|
310
|
+
border-color var(--motion-instant) ease-out;
|
|
273
311
|
}
|
|
274
312
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
313
|
+
/* Depressed state — 1px down, tinted background */
|
|
314
|
+
.kso-key-pressed {
|
|
315
|
+
transform: translateY(1px);
|
|
316
|
+
background: oklch(0.32 0.04 240);
|
|
317
|
+
border-color: oklch(0.45 0.06 240);
|
|
279
318
|
}
|
|
280
319
|
|
|
281
320
|
@keyframes kso-fade {
|
|
@@ -292,5 +331,12 @@
|
|
|
292
331
|
.kso-panel {
|
|
293
332
|
animation: none !important;
|
|
294
333
|
}
|
|
334
|
+
.kso-key {
|
|
335
|
+
transition: none;
|
|
336
|
+
}
|
|
337
|
+
.kso-key-pressed {
|
|
338
|
+
transform: none;
|
|
339
|
+
background: oklch(0.32 0.04 240);
|
|
340
|
+
}
|
|
295
341
|
}
|
|
296
342
|
</style>
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* `Escape` to dismiss. Input/contenteditable fields and Ctrl/Meta/Alt
|
|
5
5
|
* combos are ignored so the shortcut never interferes with typing.
|
|
6
6
|
*
|
|
7
|
+
* While open, physical keydown events depress the matching on-screen keycap
|
|
8
|
+
* with a brief translateY(1px) + tint (var(--motion-instant)).
|
|
9
|
+
*
|
|
7
10
|
* Usage:
|
|
8
11
|
*
|
|
9
12
|
* <KeyboardShortcutsOverlay shortcuts={[
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
value?: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
showLabel?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
value = '',
|
|
10
|
+
label = 'Password strength',
|
|
11
|
+
showLabel = true
|
|
12
|
+
}: Props = $props();
|
|
13
|
+
|
|
14
|
+
// zxcvbn-free heuristic: 0-4
|
|
15
|
+
const score = $derived((() => {
|
|
16
|
+
if (!value) return 0;
|
|
17
|
+
let s = 0;
|
|
18
|
+
if (value.length > 4) s++;
|
|
19
|
+
if (value.length > 8) s++;
|
|
20
|
+
if (/[0-9]/.test(value) && /[a-zA-Z]/.test(value)) s++;
|
|
21
|
+
if (/[^a-zA-Z0-9]/.test(value)) s++;
|
|
22
|
+
return Math.min(s, 4) as 0 | 1 | 2 | 3 | 4;
|
|
23
|
+
})());
|
|
24
|
+
|
|
25
|
+
const scoreLabels = ['', 'Weak', 'Fair', 'Good', 'Strong'] as const;
|
|
26
|
+
|
|
27
|
+
// DaisyUI semantic color classes per score band
|
|
28
|
+
const scoreColors = ['', 'bg-error', 'bg-warning', 'bg-warning', 'bg-success'] as const;
|
|
29
|
+
const scoreTextColors = ['text-base-content/45', 'text-error', 'text-warning', 'text-warning', 'text-success'] as const;
|
|
30
|
+
|
|
31
|
+
const announcementId = `pm-live-${Math.random().toString(36).slice(2, 9)}`;
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<div class="password-meter space-y-1.5">
|
|
35
|
+
{#if showLabel || score > 0}
|
|
36
|
+
<div class="flex items-center justify-between">
|
|
37
|
+
{#if showLabel}
|
|
38
|
+
<span class="text-[0.8125rem] font-medium text-base-content/85">{label}</span>
|
|
39
|
+
{/if}
|
|
40
|
+
<span
|
|
41
|
+
id={announcementId}
|
|
42
|
+
role="status"
|
|
43
|
+
aria-live="polite"
|
|
44
|
+
aria-atomic="true"
|
|
45
|
+
class="text-[0.8125rem] font-medium transition-colors duration-[var(--motion-fast)] {scoreTextColors[score]}"
|
|
46
|
+
>
|
|
47
|
+
{scoreLabels[score]}
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
{/if}
|
|
51
|
+
|
|
52
|
+
<div class="flex gap-1" aria-hidden="true">
|
|
53
|
+
{#each [1, 2, 3, 4] as segment}
|
|
54
|
+
<div
|
|
55
|
+
class="meter-segment h-1.5 flex-1 overflow-hidden rounded-full bg-base-300"
|
|
56
|
+
>
|
|
57
|
+
<div
|
|
58
|
+
class="meter-fill h-full w-full rounded-full {scoreColors[score] || 'bg-base-300'}"
|
|
59
|
+
class:meter-fill--active={score >= segment}
|
|
60
|
+
></div>
|
|
61
|
+
</div>
|
|
62
|
+
{/each}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<style>
|
|
67
|
+
.meter-fill {
|
|
68
|
+
transform: scaleX(0);
|
|
69
|
+
transform-origin: left;
|
|
70
|
+
transition:
|
|
71
|
+
transform var(--motion-enter) var(--motion-glide),
|
|
72
|
+
background-color var(--motion-fast) var(--motion-glide);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.meter-fill--active {
|
|
76
|
+
transform: scaleX(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@media (prefers-reduced-motion: reduce) {
|
|
80
|
+
.meter-fill {
|
|
81
|
+
transition: background-color var(--motion-fast) var(--motion-glide);
|
|
82
|
+
}
|
|
83
|
+
.meter-fill--active {
|
|
84
|
+
transform: scaleX(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
</style>
|