@gigo-ui/components 1.0.0-alpha
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/README.md +49 -0
- package/dist/assets/favicon.svg +1 -0
- package/dist/components/chaos/CatchSubmit.svelte +144 -0
- package/dist/components/chaos/CatchSubmit.svelte.d.ts +11 -0
- package/dist/components/chaos/ChaosButton.svelte +132 -0
- package/dist/components/chaos/ChaosButton.svelte.d.ts +4 -0
- package/dist/components/chaos/ChaosForm.svelte +206 -0
- package/dist/components/chaos/ChaosForm.svelte.d.ts +10 -0
- package/dist/components/chaos/ColorPickerWrong.svelte +141 -0
- package/dist/components/chaos/ColorPickerWrong.svelte.d.ts +11 -0
- package/dist/components/chaos/DropdownCalc.svelte +195 -0
- package/dist/components/chaos/DropdownCalc.svelte.d.ts +8 -0
- package/dist/components/chaos/GhostCard.svelte +157 -0
- package/dist/components/chaos/GhostCard.svelte.d.ts +13 -0
- package/dist/components/chaos/GravityInput.svelte +161 -0
- package/dist/components/chaos/GravityInput.svelte.d.ts +13 -0
- package/dist/components/chaos/PasswordPeekhole.svelte +141 -0
- package/dist/components/chaos/PasswordPeekhole.svelte.d.ts +11 -0
- package/dist/components/chaos/ProgressDoom.svelte +139 -0
- package/dist/components/chaos/ProgressDoom.svelte.d.ts +12 -0
- package/dist/components/chaos/RotaryDial.svelte +221 -0
- package/dist/components/chaos/RotaryDial.svelte.d.ts +10 -0
- package/dist/components/chaos/SliderPhone.svelte +92 -0
- package/dist/components/chaos/SliderPhone.svelte.d.ts +10 -0
- package/dist/components/chaos/TermsSidescroll.svelte +121 -0
- package/dist/components/chaos/TermsSidescroll.svelte.d.ts +12 -0
- package/dist/components/chaos/VolumeSlider.svelte +116 -0
- package/dist/components/chaos/VolumeSlider.svelte.d.ts +14 -0
- package/dist/components/ui/Button.svelte +162 -0
- package/dist/components/ui/Button.svelte.d.ts +4 -0
- package/dist/components/ui/Card.svelte +141 -0
- package/dist/components/ui/Card.svelte.d.ts +4 -0
- package/dist/components/ui/Carousel.svelte +164 -0
- package/dist/components/ui/Carousel.svelte.d.ts +4 -0
- package/dist/components/ui/Form.svelte +178 -0
- package/dist/components/ui/Form.svelte.d.ts +4 -0
- package/dist/components/ui/Input.svelte +135 -0
- package/dist/components/ui/Input.svelte.d.ts +4 -0
- package/dist/components/ui/Modal.svelte +173 -0
- package/dist/components/ui/Modal.svelte.d.ts +4 -0
- package/dist/components/ui/Navigation.svelte +91 -0
- package/dist/components/ui/Navigation.svelte.d.ts +4 -0
- package/dist/docs/categories.d.ts +13 -0
- package/dist/docs/categories.js +17 -0
- package/dist/docs/component-data.d.ts +6 -0
- package/dist/docs/component-data.js +49 -0
- package/dist/docs/components/badui/catch-submit.d.ts +2 -0
- package/dist/docs/components/badui/catch-submit.js +49 -0
- package/dist/docs/components/badui/color-picker-wrong.d.ts +2 -0
- package/dist/docs/components/badui/color-picker-wrong.js +40 -0
- package/dist/docs/components/badui/dropdown-calc.d.ts +2 -0
- package/dist/docs/components/badui/dropdown-calc.js +28 -0
- package/dist/docs/components/badui/ghost-card.d.ts +2 -0
- package/dist/docs/components/badui/ghost-card.js +54 -0
- package/dist/docs/components/badui/gravity-input.d.ts +2 -0
- package/dist/docs/components/badui/gravity-input.js +64 -0
- package/dist/docs/components/badui/password-peekhole.d.ts +2 -0
- package/dist/docs/components/badui/password-peekhole.js +51 -0
- package/dist/docs/components/badui/progress-doom.d.ts +2 -0
- package/dist/docs/components/badui/progress-doom.js +48 -0
- package/dist/docs/components/badui/rotary-dial.d.ts +2 -0
- package/dist/docs/components/badui/rotary-dial.js +40 -0
- package/dist/docs/components/badui/slider-phone.d.ts +2 -0
- package/dist/docs/components/badui/slider-phone.js +40 -0
- package/dist/docs/components/badui/terms-sidescroll.d.ts +2 -0
- package/dist/docs/components/badui/terms-sidescroll.js +46 -0
- package/dist/docs/components/badui/volume-slider.d.ts +2 -0
- package/dist/docs/components/badui/volume-slider.js +52 -0
- package/dist/docs/components/chaos/chaos-button.d.ts +2 -0
- package/dist/docs/components/chaos/chaos-button.js +48 -0
- package/dist/docs/components/chaos/chaos-form.d.ts +2 -0
- package/dist/docs/components/chaos/chaos-form.js +68 -0
- package/dist/docs/components/standard/button.d.ts +2 -0
- package/dist/docs/components/standard/button.js +66 -0
- package/dist/docs/components/standard/card.d.ts +2 -0
- package/dist/docs/components/standard/card.js +55 -0
- package/dist/docs/components/standard/carousel.d.ts +2 -0
- package/dist/docs/components/standard/carousel.js +63 -0
- package/dist/docs/components/standard/form.d.ts +2 -0
- package/dist/docs/components/standard/form.js +57 -0
- package/dist/docs/components/standard/input.d.ts +2 -0
- package/dist/docs/components/standard/input.js +65 -0
- package/dist/docs/components/standard/modal.d.ts +2 -0
- package/dist/docs/components/standard/modal.js +63 -0
- package/dist/docs/components/standard/navigation.d.ts +2 -0
- package/dist/docs/components/standard/navigation.js +51 -0
- package/dist/docs/types.d.ts +16 -0
- package/dist/docs/types.js +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +21 -0
- package/dist/styles/globals.css +569 -0
- package/dist/types/index.d.ts +177 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/cn.d.ts +9 -0
- package/dist/utils/cn.js +90 -0
- package/package.json +61 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value?: string;
|
|
3
|
+
placeholder?: string;
|
|
4
|
+
spawnInterval?: number;
|
|
5
|
+
maxFalling?: number;
|
|
6
|
+
characters?: string;
|
|
7
|
+
arenaHeight?: number;
|
|
8
|
+
class?: string;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
};
|
|
11
|
+
declare const GravityInput: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
12
|
+
type GravityInput = ReturnType<typeof GravityInput>;
|
|
13
|
+
export default GravityInput;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils/cn.js';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
value = $bindable(''),
|
|
6
|
+
placeholder = 'Type your password...',
|
|
7
|
+
holeSize = 40,
|
|
8
|
+
accentColor = '#e040fb',
|
|
9
|
+
class: className,
|
|
10
|
+
...restProps
|
|
11
|
+
}: {
|
|
12
|
+
value?: string;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
holeSize?: number;
|
|
15
|
+
accentColor?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
} = $props();
|
|
19
|
+
|
|
20
|
+
let holeX = $state(60);
|
|
21
|
+
let holeY = $state(20);
|
|
22
|
+
let isDragging = $state(false);
|
|
23
|
+
let dragOffset = $state({ x: 0, y: 0 });
|
|
24
|
+
let containerEl: HTMLDivElement | undefined = $state();
|
|
25
|
+
let inputEl: HTMLInputElement | undefined = $state();
|
|
26
|
+
let isFocused = $state(false);
|
|
27
|
+
|
|
28
|
+
let holeCenterX = $derived(holeX + holeSize / 2);
|
|
29
|
+
let holeCenterY = $derived(holeY + holeSize / 2);
|
|
30
|
+
|
|
31
|
+
function onHolePointerDown(e: PointerEvent) {
|
|
32
|
+
isDragging = true;
|
|
33
|
+
const rect = (e.target as HTMLElement).getBoundingClientRect();
|
|
34
|
+
dragOffset = {
|
|
35
|
+
x: e.clientX - rect.left - holeSize / 2,
|
|
36
|
+
y: e.clientY - rect.top - holeSize / 2
|
|
37
|
+
};
|
|
38
|
+
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function onPointerMove(e: PointerEvent) {
|
|
42
|
+
if (!isDragging || !containerEl) return;
|
|
43
|
+
const container = containerEl.getBoundingClientRect();
|
|
44
|
+
holeX = Math.max(0, Math.min(e.clientX - container.left - dragOffset.x - holeSize / 2,
|
|
45
|
+
container.width - holeSize));
|
|
46
|
+
holeY = Math.max(0, Math.min(e.clientY - container.top - dragOffset.y - holeSize / 2,
|
|
47
|
+
container.height - holeSize));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function onPointerUp() {
|
|
51
|
+
isDragging = false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function focusInput() {
|
|
55
|
+
inputEl?.focus();
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<div class={cn('space-y-3 max-w-sm', className)} {...restProps}>
|
|
60
|
+
<!-- Password field container -->
|
|
61
|
+
<div
|
|
62
|
+
bind:this={containerEl}
|
|
63
|
+
class="relative rounded-xl border overflow-hidden cursor-text"
|
|
64
|
+
role="textbox"
|
|
65
|
+
tabindex="-1"
|
|
66
|
+
style:border-color={isFocused ? accentColor : 'var(--border)'}
|
|
67
|
+
style:box-shadow={isFocused ? `0 0 0 2px ${accentColor}26` : 'none'}
|
|
68
|
+
onclick={focusInput}
|
|
69
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') focusInput(); }}
|
|
70
|
+
onpointermove={onPointerMove}
|
|
71
|
+
onpointerup={onPointerUp}
|
|
72
|
+
>
|
|
73
|
+
<!-- Real input -->
|
|
74
|
+
<input
|
|
75
|
+
bind:this={inputEl}
|
|
76
|
+
type="text"
|
|
77
|
+
bind:value
|
|
78
|
+
{placeholder}
|
|
79
|
+
onfocus={() => isFocused = true}
|
|
80
|
+
onblur={() => isFocused = false}
|
|
81
|
+
class="w-full px-4 py-3 bg-(--card) font-mono text-base text-transparent caret-transparent outline-none selection:bg-transparent"
|
|
82
|
+
style="color: transparent;"
|
|
83
|
+
autocomplete="off"
|
|
84
|
+
spellcheck="false"
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
<!-- Visible text layer underneath the overlay -->
|
|
88
|
+
<div
|
|
89
|
+
class="absolute inset-0 flex items-center px-4 pointer-events-none font-mono text-base"
|
|
90
|
+
>
|
|
91
|
+
<span class="text-(--foreground)">
|
|
92
|
+
{#if value}
|
|
93
|
+
{value}
|
|
94
|
+
{:else}
|
|
95
|
+
<span class="text-(--muted-foreground)">{placeholder}</span>
|
|
96
|
+
{/if}
|
|
97
|
+
</span>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<!-- Overlay with circular hole cut out via CSS mask -->
|
|
101
|
+
<div
|
|
102
|
+
class="absolute inset-0 pointer-events-none bg-(--card)"
|
|
103
|
+
style:-webkit-mask-image="radial-gradient(circle {holeSize / 2}px at {holeCenterX}px {holeCenterY}px, transparent {holeSize / 2}px, black {holeSize / 2}px)"
|
|
104
|
+
style:mask-image="radial-gradient(circle {holeSize / 2}px at {holeCenterX}px {holeCenterY}px, transparent {holeSize / 2}px, black {holeSize / 2}px)"
|
|
105
|
+
>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
109
|
+
<!-- Peephole handle (draggable) -->
|
|
110
|
+
<div
|
|
111
|
+
role="slider"
|
|
112
|
+
aria-label="Peephole position"
|
|
113
|
+
aria-valuenow={Math.round(holeX)}
|
|
114
|
+
tabindex="0"
|
|
115
|
+
class="absolute rounded-full border-2 cursor-grab active:cursor-grabbing z-10"
|
|
116
|
+
style:width="{holeSize}px"
|
|
117
|
+
style:height="{holeSize}px"
|
|
118
|
+
style:left="{holeX}px"
|
|
119
|
+
style:top="{holeY}px"
|
|
120
|
+
style:border-color={isDragging ? accentColor : `${accentColor}66`}
|
|
121
|
+
style:box-shadow={isDragging ? `0 0 16px ${accentColor}66` : `0 0 8px ${accentColor}33`}
|
|
122
|
+
style:background="transparent"
|
|
123
|
+
onpointerdown={onHolePointerDown}
|
|
124
|
+
>
|
|
125
|
+
<div class="absolute -top-1 -left-1 w-2 h-2 border-t-2 border-l-2 rounded-tl" style:border-color={accentColor}></div>
|
|
126
|
+
<div class="absolute -top-1 -right-1 w-2 h-2 border-t-2 border-r-2 rounded-tr" style:border-color={accentColor}></div>
|
|
127
|
+
<div class="absolute -bottom-1 -left-1 w-2 h-2 border-b-2 border-l-2 rounded-bl" style:border-color={accentColor}></div>
|
|
128
|
+
<div class="absolute -bottom-1 -right-1 w-2 h-2 border-b-2 border-r-2 rounded-br" style:border-color={accentColor}></div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<!-- Character count -->
|
|
133
|
+
<div class="flex items-center justify-between text-xs font-mono text-(--muted-foreground)">
|
|
134
|
+
<span>{value.length} chars (probably)</span>
|
|
135
|
+
<span class="text-gigo-magenta/60">⬡ drag peephole to peek</span>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<p class="text-[10px] font-mono text-(--muted-foreground) text-center">
|
|
139
|
+
Your password is hidden. Drag the peephole to see what you typed. Security! 🔒
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value?: string;
|
|
3
|
+
placeholder?: string;
|
|
4
|
+
holeSize?: number;
|
|
5
|
+
accentColor?: string;
|
|
6
|
+
class?: string;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
};
|
|
9
|
+
declare const PasswordPeekhole: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
10
|
+
type PasswordPeekhole = ReturnType<typeof PasswordPeekhole>;
|
|
11
|
+
export default PasswordPeekhole;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils/cn.js';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
targetMs = 5000,
|
|
6
|
+
goBackwards = true,
|
|
7
|
+
stall = true,
|
|
8
|
+
lieAboutCompletion = true,
|
|
9
|
+
onComplete,
|
|
10
|
+
class: className,
|
|
11
|
+
...restProps
|
|
12
|
+
}: {
|
|
13
|
+
targetMs?: number;
|
|
14
|
+
goBackwards?: boolean;
|
|
15
|
+
stall?: boolean;
|
|
16
|
+
lieAboutCompletion?: boolean;
|
|
17
|
+
onComplete?: () => void;
|
|
18
|
+
class?: string;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
} = $props();
|
|
21
|
+
|
|
22
|
+
let progress = $state(0);
|
|
23
|
+
let displayProgress = $state(0);
|
|
24
|
+
let statusText = $state('Loading...');
|
|
25
|
+
let isComplete = $state(false);
|
|
26
|
+
let fakePercent = $state(0);
|
|
27
|
+
let barColor = $state('#e040fb');
|
|
28
|
+
|
|
29
|
+
const STATUS_MESSAGES = [
|
|
30
|
+
'Loading...', 'Almost there...', 'Just a moment...', 'Reticulating splines...',
|
|
31
|
+
'Downloading more RAM...', 'Consulting the oracle...', 'Please wait...',
|
|
32
|
+
'Undoing progress...', 'Going backwards...', 'Starting over...',
|
|
33
|
+
'99% complete (lie)', 'Any second now...', 'Oops, lost some progress...',
|
|
34
|
+
'Compiling excuses...', 'Buffering your patience...',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
$effect(() => {
|
|
38
|
+
let startTime = performance.now();
|
|
39
|
+
let running = true;
|
|
40
|
+
|
|
41
|
+
function tick() {
|
|
42
|
+
if (!running) return;
|
|
43
|
+
const elapsed = performance.now() - startTime;
|
|
44
|
+
let rawProgress = Math.min((elapsed / targetMs) * 100, 100);
|
|
45
|
+
|
|
46
|
+
if (goBackwards && rawProgress > 30 && rawProgress < 70 && Math.random() > 0.95) {
|
|
47
|
+
startTime += 800;
|
|
48
|
+
rawProgress = Math.max(0, rawProgress - 15);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (stall && rawProgress > 50 && rawProgress < 80 && Math.random() > 0.9) {
|
|
52
|
+
startTime += 500;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
progress = rawProgress;
|
|
56
|
+
displayProgress = Math.max(0, Math.min(100, rawProgress + (Math.random() - 0.5) * 8));
|
|
57
|
+
|
|
58
|
+
if (lieAboutCompletion) {
|
|
59
|
+
if (rawProgress < 20) fakePercent = rawProgress * 3;
|
|
60
|
+
else if (rawProgress < 80) fakePercent = 60 + Math.random() * 35;
|
|
61
|
+
else fakePercent = 95 + Math.random() * 4.9;
|
|
62
|
+
} else {
|
|
63
|
+
fakePercent = displayProgress;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const msgIdx = Math.floor((rawProgress / 100) * STATUS_MESSAGES.length);
|
|
67
|
+
statusText = STATUS_MESSAGES[Math.min(msgIdx, STATUS_MESSAGES.length - 1)];
|
|
68
|
+
|
|
69
|
+
if (rawProgress < 30) barColor = '#e040fb';
|
|
70
|
+
else if (rawProgress < 60) barColor = '#18ffff';
|
|
71
|
+
else if (rawProgress < 90) barColor = '#76ff03';
|
|
72
|
+
else barColor = '#ff1744';
|
|
73
|
+
|
|
74
|
+
if (rawProgress >= 100 && !isComplete) {
|
|
75
|
+
isComplete = true;
|
|
76
|
+
statusText = lieAboutCompletion ? 'Done! (was it though?)' : 'Complete!';
|
|
77
|
+
onComplete?.();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!isComplete) requestAnimationFrame(tick);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
requestAnimationFrame(tick);
|
|
84
|
+
return () => { running = false; };
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function restart() {
|
|
88
|
+
progress = 0;
|
|
89
|
+
displayProgress = 0;
|
|
90
|
+
fakePercent = 0;
|
|
91
|
+
isComplete = false;
|
|
92
|
+
statusText = 'Loading...';
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<div class={cn('space-y-3 max-w-md', className)} {...restProps}>
|
|
97
|
+
<div class="flex items-center justify-between">
|
|
98
|
+
<span class="text-xs font-mono text-(--muted-foreground)">{statusText}</span>
|
|
99
|
+
<span class="text-xs font-mono font-bold" style:color={barColor}>
|
|
100
|
+
{Math.round(fakePercent)}%
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div class="relative h-4 w-full overflow-hidden rounded-full border border-(--border) bg-(--secondary)">
|
|
105
|
+
<div
|
|
106
|
+
class="absolute inset-y-0 left-0 rounded-full transition-all duration-300"
|
|
107
|
+
style:width="{Math.max(0, Math.min(100, displayProgress))}%"
|
|
108
|
+
style:background="linear-gradient(90deg, {barColor}, {barColor}99)"
|
|
109
|
+
style:box-shadow="0 0 12px {barColor}66"
|
|
110
|
+
></div>
|
|
111
|
+
{#if displayProgress > 10}
|
|
112
|
+
<div
|
|
113
|
+
class="absolute inset-y-0 left-0 rounded-full opacity-30"
|
|
114
|
+
style:width="{Math.max(0, Math.min(100, displayProgress))}%"
|
|
115
|
+
style:background="linear-gradient(90deg, transparent, white 50%, transparent)"
|
|
116
|
+
style:animation="gigo-shimmer 1.5s linear infinite"
|
|
117
|
+
></div>
|
|
118
|
+
{/if}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div class="flex items-center justify-between text-[10px] font-mono text-(--muted-foreground)">
|
|
122
|
+
<span>Real: {Math.round(progress)}%</span>
|
|
123
|
+
<span>Shown: {Math.round(displayProgress)}%</span>
|
|
124
|
+
<span>Claimed: {Math.round(fakePercent)}%</span>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{#if isComplete}
|
|
128
|
+
<button
|
|
129
|
+
class="w-full rounded-xl border border-gigo-magenta/30 bg-gigo-magenta/10 px-4 py-2 text-sm font-mono text-gigo-magenta hover:bg-gigo-magenta/20 transition-colors cursor-pointer"
|
|
130
|
+
onclick={restart}
|
|
131
|
+
>
|
|
132
|
+
Restart the suffering
|
|
133
|
+
</button>
|
|
134
|
+
{/if}
|
|
135
|
+
|
|
136
|
+
<p class="text-[10px] font-mono text-(--muted-foreground) text-center">
|
|
137
|
+
Three different progress values. None of them are honest.
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
targetMs?: number;
|
|
3
|
+
goBackwards?: boolean;
|
|
4
|
+
stall?: boolean;
|
|
5
|
+
lieAboutCompletion?: boolean;
|
|
6
|
+
onComplete?: () => void;
|
|
7
|
+
class?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
declare const ProgressDoom: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type ProgressDoom = ReturnType<typeof ProgressDoom>;
|
|
12
|
+
export default ProgressDoom;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils/cn.js';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
value = $bindable(''),
|
|
6
|
+
maxDigits = 10,
|
|
7
|
+
dialSize = 260,
|
|
8
|
+
class: className,
|
|
9
|
+
...restProps
|
|
10
|
+
}: {
|
|
11
|
+
value?: string;
|
|
12
|
+
maxDigits?: number;
|
|
13
|
+
dialSize?: number;
|
|
14
|
+
class?: string;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
} = $props();
|
|
17
|
+
|
|
18
|
+
let rotation = $state(0);
|
|
19
|
+
let isDragging = $state(false);
|
|
20
|
+
let dialEl: SVGSVGElement | undefined = $state();
|
|
21
|
+
let startAngle = $state(0);
|
|
22
|
+
let startRotation = $state(0);
|
|
23
|
+
let selectedDigit = $state<number | null>(null);
|
|
24
|
+
let isAnimatingReturn = $state(false);
|
|
25
|
+
|
|
26
|
+
let CENTER = $derived(dialSize / 2);
|
|
27
|
+
let HOLE_RADIUS = $derived(dialSize * 18 / 260);
|
|
28
|
+
let RING_RADIUS = $derived(dialSize * 95 / 260);
|
|
29
|
+
const STOPPER_ANGLE = 30;
|
|
30
|
+
|
|
31
|
+
// Digit positions (0-9) arranged clockwise starting from bottom-right
|
|
32
|
+
// Real rotary phones: 1 at ~330°, going clockwise to 0 at ~210°
|
|
33
|
+
const digitAngles = [
|
|
34
|
+
240, // 0
|
|
35
|
+
330, // 1
|
|
36
|
+
310, // 2
|
|
37
|
+
290, // 3
|
|
38
|
+
270, // 4
|
|
39
|
+
250, // 5
|
|
40
|
+
230, // 6
|
|
41
|
+
210, // 7
|
|
42
|
+
190, // 8
|
|
43
|
+
170, // 9
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
function getDigitPos(angle: number) {
|
|
47
|
+
const rad = ((angle - 90) * Math.PI) / 180;
|
|
48
|
+
return {
|
|
49
|
+
x: CENTER + RING_RADIUS * Math.cos(rad),
|
|
50
|
+
y: CENTER + RING_RADIUS * Math.sin(rad)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getAngleFromCenter(clientX: number, clientY: number) {
|
|
55
|
+
if (!dialEl) return 0;
|
|
56
|
+
const rect = dialEl.getBoundingClientRect();
|
|
57
|
+
const cx = rect.left + rect.width / 2;
|
|
58
|
+
const cy = rect.top + rect.height / 2;
|
|
59
|
+
return Math.atan2(clientY - cy, clientX - cx) * (180 / Math.PI) + 90;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function onPointerDown(e: PointerEvent, digitIndex: number) {
|
|
63
|
+
if (isAnimatingReturn) return;
|
|
64
|
+
if (value.length >= maxDigits) return;
|
|
65
|
+
isDragging = true;
|
|
66
|
+
selectedDigit = digitIndex;
|
|
67
|
+
startAngle = getAngleFromCenter(e.clientX, e.clientY);
|
|
68
|
+
startRotation = rotation;
|
|
69
|
+
(e.target as HTMLElement)?.setPointerCapture?.(e.pointerId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function onPointerMove(e: PointerEvent) {
|
|
73
|
+
if (!isDragging || selectedDigit === null) return;
|
|
74
|
+
const currentAngle = getAngleFromCenter(e.clientX, e.clientY);
|
|
75
|
+
let delta = currentAngle - startAngle;
|
|
76
|
+
|
|
77
|
+
// Only allow clockwise rotation (positive delta)
|
|
78
|
+
if (delta < -180) delta += 360;
|
|
79
|
+
if (delta > 180) delta -= 360;
|
|
80
|
+
|
|
81
|
+
// Clamp to clockwise only
|
|
82
|
+
const newRotation = Math.max(0, Math.min(startRotation + delta, 360 - digitAngles[selectedDigit] + STOPPER_ANGLE));
|
|
83
|
+
rotation = Math.max(0, newRotation);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function onPointerUp() {
|
|
87
|
+
if (!isDragging || selectedDigit === null) return;
|
|
88
|
+
isDragging = false;
|
|
89
|
+
|
|
90
|
+
// Check if rotated far enough
|
|
91
|
+
const requiredRotation = 360 - digitAngles[selectedDigit];
|
|
92
|
+
if (rotation >= requiredRotation * 0.75) {
|
|
93
|
+
// Digit entered!
|
|
94
|
+
value += String(selectedDigit);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Animate return
|
|
98
|
+
isAnimatingReturn = true;
|
|
99
|
+
const returnStart = rotation;
|
|
100
|
+
const duration = 400 + rotation * 2; // longer return for bigger rotations
|
|
101
|
+
const startTime = performance.now();
|
|
102
|
+
|
|
103
|
+
function animateReturn(now: number) {
|
|
104
|
+
const elapsed = now - startTime;
|
|
105
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
106
|
+
// Ease out
|
|
107
|
+
const eased = 1 - Math.pow(1 - progress, 3);
|
|
108
|
+
rotation = returnStart * (1 - eased);
|
|
109
|
+
|
|
110
|
+
if (progress < 1) {
|
|
111
|
+
requestAnimationFrame(animateReturn);
|
|
112
|
+
} else {
|
|
113
|
+
rotation = 0;
|
|
114
|
+
isAnimatingReturn = false;
|
|
115
|
+
selectedDigit = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
requestAnimationFrame(animateReturn);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function removeLastDigit() {
|
|
122
|
+
value = value.slice(0, -1);
|
|
123
|
+
}
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<div class={cn('flex flex-col items-center gap-4', className)} {...restProps}>
|
|
127
|
+
<!-- Value display -->
|
|
128
|
+
<div class="flex items-center gap-2 rounded-xl border border-(--border) bg-(--secondary) px-4 py-2 min-w-50">
|
|
129
|
+
<span class="font-mono text-lg text-(--foreground) flex-1 text-center tracking-[0.3em]">
|
|
130
|
+
{#if value}
|
|
131
|
+
{value}
|
|
132
|
+
{:else}
|
|
133
|
+
<span class="text-(--muted-foreground) tracking-normal text-sm">Dial a number...</span>
|
|
134
|
+
{/if}
|
|
135
|
+
</span>
|
|
136
|
+
{#if value}
|
|
137
|
+
<button
|
|
138
|
+
class="text-xs text-(--muted-foreground) hover:text-gigo-red transition-colors"
|
|
139
|
+
onclick={removeLastDigit}
|
|
140
|
+
>⌫</button>
|
|
141
|
+
{/if}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<!-- Rotary dial -->
|
|
145
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
146
|
+
<svg
|
|
147
|
+
bind:this={dialEl}
|
|
148
|
+
width={dialSize}
|
|
149
|
+
height={dialSize}
|
|
150
|
+
viewBox="0 0 {dialSize} {dialSize}"
|
|
151
|
+
class="select-none touch-none"
|
|
152
|
+
role="group"
|
|
153
|
+
aria-label="Rotary dial"
|
|
154
|
+
onpointermove={onPointerMove}
|
|
155
|
+
onpointerup={onPointerUp}
|
|
156
|
+
onpointerleave={onPointerUp}
|
|
157
|
+
>
|
|
158
|
+
<!-- Outer ring -->
|
|
159
|
+
<circle cx={CENTER} cy={CENTER} r={dialSize / 2 - 4} fill="none" stroke="var(--border)" stroke-width="2" />
|
|
160
|
+
|
|
161
|
+
<circle cx={CENTER} cy={CENTER} r={dialSize / 2 - 8} fill="var(--card)" />
|
|
162
|
+
|
|
163
|
+
<!-- Center circle -->
|
|
164
|
+
<circle cx={CENTER} cy={CENTER} r="35" fill="var(--secondary)" stroke="var(--border)" stroke-width="1" />
|
|
165
|
+
|
|
166
|
+
<!-- Center label -->
|
|
167
|
+
<text x={CENTER} y={CENTER + 4} text-anchor="middle" font-size="10" font-family="monospace" fill="var(--muted-foreground)">GIGO</text>
|
|
168
|
+
|
|
169
|
+
<!-- Stopper -->
|
|
170
|
+
{#if true}
|
|
171
|
+
{@const stopPos = getDigitPos(STOPPER_ANGLE + 340)}
|
|
172
|
+
<rect
|
|
173
|
+
x={stopPos.x - 4}
|
|
174
|
+
y={stopPos.y - 12}
|
|
175
|
+
width="8"
|
|
176
|
+
height="24"
|
|
177
|
+
rx="3"
|
|
178
|
+
fill="var(--muted-foreground)"
|
|
179
|
+
opacity="0.3"
|
|
180
|
+
/>
|
|
181
|
+
{/if}
|
|
182
|
+
|
|
183
|
+
<!-- Rotating group -->
|
|
184
|
+
<g transform="rotate({rotation}, {CENTER}, {CENTER})">
|
|
185
|
+
{#each [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as digit}
|
|
186
|
+
{@const pos = getDigitPos(digitAngles[digit])}
|
|
187
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
188
|
+
<circle
|
|
189
|
+
cx={pos.x}
|
|
190
|
+
cy={pos.y}
|
|
191
|
+
r={HOLE_RADIUS}
|
|
192
|
+
fill={selectedDigit === digit ? 'rgba(224, 64, 251, 0.15)' : 'var(--secondary)'}
|
|
193
|
+
stroke={selectedDigit === digit ? '#e040fb' : 'var(--border)'}
|
|
194
|
+
stroke-width={selectedDigit === digit ? 2 : 1}
|
|
195
|
+
class="cursor-pointer"
|
|
196
|
+
role="button"
|
|
197
|
+
tabindex="0"
|
|
198
|
+
aria-label="Digit {digit}"
|
|
199
|
+
style="filter: {selectedDigit === digit ? 'drop-shadow(0 0 6px rgba(224, 64, 251, 0.4))' : 'none'}"
|
|
200
|
+
onpointerdown={(e) => onPointerDown(e, digit)}
|
|
201
|
+
/>
|
|
202
|
+
<text
|
|
203
|
+
x={pos.x}
|
|
204
|
+
y={pos.y + 5}
|
|
205
|
+
text-anchor="middle"
|
|
206
|
+
font-size="16"
|
|
207
|
+
font-weight="bold"
|
|
208
|
+
font-family="monospace"
|
|
209
|
+
fill={selectedDigit === digit ? '#e040fb' : 'var(--foreground)'}
|
|
210
|
+
class="pointer-events-none select-none"
|
|
211
|
+
>
|
|
212
|
+
{digit}
|
|
213
|
+
</text>
|
|
214
|
+
{/each}
|
|
215
|
+
</g>
|
|
216
|
+
</svg>
|
|
217
|
+
|
|
218
|
+
<p class="text-[10px] font-mono text-(--muted-foreground) text-center max-w-65">
|
|
219
|
+
Drag a digit clockwise to the stopper, then release. Just like grandma used to do.
|
|
220
|
+
</p>
|
|
221
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value?: string;
|
|
3
|
+
maxDigits?: number;
|
|
4
|
+
dialSize?: number;
|
|
5
|
+
class?: string;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
};
|
|
8
|
+
declare const RotaryDial: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
9
|
+
type RotaryDial = ReturnType<typeof RotaryDial>;
|
|
10
|
+
export default RotaryDial;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils/cn.js';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
value = $bindable(''),
|
|
6
|
+
digitCount = 10,
|
|
7
|
+
formatPattern = '(XXX) XXX-XXXX',
|
|
8
|
+
class: className,
|
|
9
|
+
...restProps
|
|
10
|
+
}: {
|
|
11
|
+
value?: string;
|
|
12
|
+
digitCount?: number;
|
|
13
|
+
formatPattern?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
} = $props();
|
|
17
|
+
|
|
18
|
+
let sliderValues = $state<number[]>([]);
|
|
19
|
+
|
|
20
|
+
$effect(() => {
|
|
21
|
+
if (sliderValues.length !== digitCount) {
|
|
22
|
+
sliderValues = Array(digitCount).fill(0);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Sync value from sliders
|
|
27
|
+
$effect(() => {
|
|
28
|
+
value = sliderValues.map(v => Math.round(v)).join('');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function formatPhoneDisplay(raw: string): string {
|
|
32
|
+
const digits = raw.padEnd(digitCount, '_');
|
|
33
|
+
let result = '';
|
|
34
|
+
let di = 0;
|
|
35
|
+
for (const ch of formatPattern) {
|
|
36
|
+
if (ch === 'X') {
|
|
37
|
+
result += digits[di] ?? '_';
|
|
38
|
+
di++;
|
|
39
|
+
} else {
|
|
40
|
+
result += ch;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (di < digits.length) {
|
|
44
|
+
result += digits.slice(di);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setSliderValue(index: number, newVal: number) {
|
|
50
|
+
sliderValues = sliderValues.map((v, i) => i === index ? newVal : v);
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<div class={cn('space-y-4 max-w-md', className)} {...restProps}>
|
|
55
|
+
<!-- Phone display -->
|
|
56
|
+
<div class="rounded-xl border border-(--border) bg-(--secondary) px-4 py-3 text-center">
|
|
57
|
+
<span class="font-mono text-xl tracking-[0.15em] text-(--foreground)">
|
|
58
|
+
{formatPhoneDisplay(value)}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<!-- Sliders grid -->
|
|
63
|
+
<div class="grid gap-2">
|
|
64
|
+
{#each sliderValues as sv, i}
|
|
65
|
+
<div class="flex items-center gap-3 rounded-lg border border-(--border) bg-(--card) px-3 py-2">
|
|
66
|
+
<span class="text-[10px] font-mono text-(--muted-foreground) min-w-14">
|
|
67
|
+
Digit {i + 1}
|
|
68
|
+
</span>
|
|
69
|
+
<input
|
|
70
|
+
type="range"
|
|
71
|
+
min="0"
|
|
72
|
+
max="9"
|
|
73
|
+
step="1"
|
|
74
|
+
value={sv}
|
|
75
|
+
oninput={(e) => setSliderValue(i, Number((e.target as HTMLInputElement).value))}
|
|
76
|
+
class="flex-1 h-2 rounded-full appearance-none cursor-pointer
|
|
77
|
+
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-gigo-magenta [&::-webkit-slider-thumb]:shadow-[0_0_8px_rgba(224,64,251,0.5)] [&::-webkit-slider-thumb]:cursor-pointer
|
|
78
|
+
[&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-gigo-magenta [&::-moz-range-thumb]:border-none [&::-moz-range-thumb]:cursor-pointer"
|
|
79
|
+
style="background: linear-gradient(to right, rgba(224,64,251,0.3) {sv * 11.11}%, var(--secondary) {sv * 11.11}%)"
|
|
80
|
+
/>
|
|
81
|
+
<span class="text-lg font-mono font-bold min-w-5 text-center"
|
|
82
|
+
style:color={sv > 0 ? '#e040fb' : 'var(--muted-foreground)'}>
|
|
83
|
+
{Math.round(sv)}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
{/each}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<p class="text-[10px] font-mono text-(--muted-foreground) text-center">
|
|
90
|
+
Slide each digit individually. The future of phone number entry is here.
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value?: string;
|
|
3
|
+
digitCount?: number;
|
|
4
|
+
formatPattern?: string;
|
|
5
|
+
class?: string;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
};
|
|
8
|
+
declare const SliderPhone: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
9
|
+
type SliderPhone = ReturnType<typeof SliderPhone>;
|
|
10
|
+
export default SliderPhone;
|