@gigo-ui/components 1.0.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/README.md +42 -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,121 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils/cn.js';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
content = 'By using this software, you agree to surrender all your snacks, your firstborn WiFi password, and any hope of understanding what you just agreed to. This agreement is binding in all dimensions, including but not limited to: the physical realm, the digital void, and that weird dream you had last Tuesday. You may not reverse-engineer, disassemble, or even think too hard about this product without express written consent from our team of legally trained hamsters. The company reserves the right to change these terms at any time, for any reason, or for no reason at all — because chaos is the only true constant. Any disputes shall be settled by a coin flip performed by a blindfolded intern. By scrolling this far, you have already agreed twice.',
|
|
6
|
+
accepted = $bindable(false),
|
|
7
|
+
rotateText = true,
|
|
8
|
+
scrambleOnScroll = true,
|
|
9
|
+
sidewaysScroll = true,
|
|
10
|
+
class: className,
|
|
11
|
+
...restProps
|
|
12
|
+
}: {
|
|
13
|
+
content?: string;
|
|
14
|
+
accepted?: boolean;
|
|
15
|
+
rotateText?: boolean;
|
|
16
|
+
scrambleOnScroll?: boolean;
|
|
17
|
+
sidewaysScroll?: boolean;
|
|
18
|
+
class?: string;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
} = $props();
|
|
21
|
+
|
|
22
|
+
let scrollPos = $state(0);
|
|
23
|
+
let textRotation = $state(0);
|
|
24
|
+
let scrambledContent = $state('');
|
|
25
|
+
let containerEl: HTMLDivElement | undefined = $state();
|
|
26
|
+
let hasScrolledToEnd = $state(false);
|
|
27
|
+
let checkboxDisabled = $state(true);
|
|
28
|
+
|
|
29
|
+
$effect(() => {
|
|
30
|
+
scrambledContent = content;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function handleScroll(e: Event) {
|
|
34
|
+
const el = e.target as HTMLElement;
|
|
35
|
+
if (sidewaysScroll) {
|
|
36
|
+
scrollPos = el.scrollLeft;
|
|
37
|
+
const maxScroll = el.scrollWidth - el.clientWidth;
|
|
38
|
+
hasScrolledToEnd = maxScroll > 0 && el.scrollLeft >= maxScroll - 10;
|
|
39
|
+
} else {
|
|
40
|
+
scrollPos = el.scrollTop;
|
|
41
|
+
const maxScroll = el.scrollHeight - el.clientHeight;
|
|
42
|
+
hasScrolledToEnd = maxScroll > 0 && el.scrollTop >= maxScroll - 10;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (hasScrolledToEnd) checkboxDisabled = false;
|
|
46
|
+
|
|
47
|
+
if (rotateText) {
|
|
48
|
+
textRotation = Math.sin(scrollPos * 0.02) * 8;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (scrambleOnScroll && Math.random() > 0.7) {
|
|
52
|
+
const chars = content.split('');
|
|
53
|
+
const count = Math.floor(Math.random() * 5) + 1;
|
|
54
|
+
for (let i = 0; i < count; i++) {
|
|
55
|
+
const idx = Math.floor(Math.random() * chars.length);
|
|
56
|
+
if (chars[idx] !== ' ') {
|
|
57
|
+
chars[idx] = String.fromCharCode(33 + Math.floor(Math.random() * 94));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
scrambledContent = chars.join('');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handleCheckbox() {
|
|
65
|
+
if (checkboxDisabled) return;
|
|
66
|
+
if (Math.random() > 0.5) {
|
|
67
|
+
accepted = !accepted;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<div class={cn('space-y-3 max-w-md', className)} {...restProps}>
|
|
73
|
+
<div class="flex items-center justify-between">
|
|
74
|
+
<span class="text-sm font-bold text-(--foreground)">Terms & Conditions</span>
|
|
75
|
+
<span class="text-[10px] font-mono text-(--muted-foreground)">
|
|
76
|
+
{hasScrolledToEnd ? '✓ Scrolled to end' : '← Scroll to read all →'}
|
|
77
|
+
</span>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div
|
|
81
|
+
bind:this={containerEl}
|
|
82
|
+
class="rounded-xl border border-(--border) bg-(--card) p-4 text-sm text-(--muted-foreground) leading-relaxed font-mono"
|
|
83
|
+
style:max-height="160px"
|
|
84
|
+
style:overflow={sidewaysScroll ? 'auto hidden' : 'hidden auto'}
|
|
85
|
+
style:white-space={sidewaysScroll ? 'nowrap' : 'normal'}
|
|
86
|
+
onscroll={handleScroll}
|
|
87
|
+
>
|
|
88
|
+
<div style:transform="rotate({textRotation}deg)" style:transform-origin="center" style:transition="transform 0.15s">
|
|
89
|
+
{scrambledContent}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="flex items-center gap-3">
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
class="flex items-center justify-center w-5 h-5 rounded border cursor-pointer transition-all"
|
|
97
|
+
style:border-color={accepted ? '#76ff03' : checkboxDisabled ? 'var(--border)' : '#e040fb'}
|
|
98
|
+
style:background={accepted ? 'rgba(118, 255, 3, 0.15)' : 'transparent'}
|
|
99
|
+
style:opacity={checkboxDisabled ? 0.4 : 1}
|
|
100
|
+
onclick={handleCheckbox}
|
|
101
|
+
disabled={checkboxDisabled}
|
|
102
|
+
aria-checked={accepted}
|
|
103
|
+
role="checkbox"
|
|
104
|
+
>
|
|
105
|
+
{#if accepted}
|
|
106
|
+
<span class="text-xs text-gigo-lime">✓</span>
|
|
107
|
+
{/if}
|
|
108
|
+
</button>
|
|
109
|
+
<span class="text-xs text-(--muted-foreground)">
|
|
110
|
+
{checkboxDisabled
|
|
111
|
+
? 'Scroll to the end to enable'
|
|
112
|
+
: accepted
|
|
113
|
+
? 'Accepted (maybe — the checkbox has a 50% fail rate)'
|
|
114
|
+
: 'I agree to these incomprehensible terms'}
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<p class="text-[10px] font-mono text-(--muted-foreground) text-center">
|
|
119
|
+
Scrolls sideways. Text rotates as you scroll. Characters scramble. Checkbox works half the time.
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
content?: string;
|
|
3
|
+
accepted?: boolean;
|
|
4
|
+
rotateText?: boolean;
|
|
5
|
+
scrambleOnScroll?: boolean;
|
|
6
|
+
sidewaysScroll?: boolean;
|
|
7
|
+
class?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
declare const TermsSidescroll: import("svelte").Component<$$ComponentProps, {}, "accepted">;
|
|
11
|
+
type TermsSidescroll = ReturnType<typeof TermsSidescroll>;
|
|
12
|
+
export default TermsSidescroll;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils/cn.js';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
value = $bindable(50),
|
|
6
|
+
min = 0,
|
|
7
|
+
max = 100,
|
|
8
|
+
step = 1,
|
|
9
|
+
invertDirection = true,
|
|
10
|
+
jumpAround = true,
|
|
11
|
+
fakeLabels = true,
|
|
12
|
+
class: className,
|
|
13
|
+
...restProps
|
|
14
|
+
}: {
|
|
15
|
+
value?: number;
|
|
16
|
+
min?: number;
|
|
17
|
+
max?: number;
|
|
18
|
+
step?: number;
|
|
19
|
+
invertDirection?: boolean;
|
|
20
|
+
jumpAround?: boolean;
|
|
21
|
+
fakeLabels?: boolean;
|
|
22
|
+
class?: string;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
} = $props();
|
|
25
|
+
|
|
26
|
+
let displayValue = $state(50);
|
|
27
|
+
let labelText = $state('50%');
|
|
28
|
+
let isGrabbing = $state(false);
|
|
29
|
+
let shakeClass = $state('');
|
|
30
|
+
let jumpTimer = $state(0);
|
|
31
|
+
|
|
32
|
+
const FAKE_LABELS = [
|
|
33
|
+
'Quiet-ish', 'MAXIMUM', 'Whisper mode', '🔇 Muted (not really)',
|
|
34
|
+
'Ear damage', 'Perfect', 'Too much', 'Not enough',
|
|
35
|
+
'Your neighbors hate this', '???', 'Yes', 'No',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
$effect(() => {
|
|
39
|
+
displayValue = invertDirection ? max - value + min : value;
|
|
40
|
+
const pct = Math.round(((value - min) / (max - min)) * 100);
|
|
41
|
+
labelText = fakeLabels
|
|
42
|
+
? FAKE_LABELS[Math.floor((pct / 100) * FAKE_LABELS.length) % FAKE_LABELS.length]
|
|
43
|
+
: `${pct}%`;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
$effect(() => {
|
|
47
|
+
if (!jumpAround) return;
|
|
48
|
+
const timer = setInterval(() => {
|
|
49
|
+
if (!isGrabbing && Math.random() > 0.6) {
|
|
50
|
+
const offset = (Math.random() - 0.5) * 20;
|
|
51
|
+
value = Math.max(min, Math.min(max, value + offset));
|
|
52
|
+
shakeClass = 'animate-gigo-shake';
|
|
53
|
+
setTimeout(() => { shakeClass = ''; }, 300);
|
|
54
|
+
}
|
|
55
|
+
}, 3000);
|
|
56
|
+
return () => clearInterval(timer);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function handleInput(e: Event) {
|
|
60
|
+
const raw = Number((e.target as HTMLInputElement).value);
|
|
61
|
+
value = invertDirection ? max - raw + min : raw;
|
|
62
|
+
isGrabbing = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handleChange() {
|
|
66
|
+
isGrabbing = false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let sliderDisplayVal = $derived(invertDirection ? max - value + min : value);
|
|
70
|
+
let fillPct = $derived(((sliderDisplayVal - min) / (max - min)) * 100);
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<div class={cn('space-y-3 max-w-xs', shakeClass, className)} {...restProps}>
|
|
74
|
+
<div class="flex items-center justify-between">
|
|
75
|
+
<span class="text-xs font-mono text-(--muted-foreground)">Volume</span>
|
|
76
|
+
<span class="text-xs font-mono font-bold text-gigo-magenta">{labelText}</span>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="relative">
|
|
80
|
+
<input
|
|
81
|
+
type="range"
|
|
82
|
+
{min}
|
|
83
|
+
{max}
|
|
84
|
+
{step}
|
|
85
|
+
value={sliderDisplayVal}
|
|
86
|
+
oninput={handleInput}
|
|
87
|
+
onchange={handleChange}
|
|
88
|
+
class="w-full h-3 rounded-full appearance-none cursor-pointer
|
|
89
|
+
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-6 [&::-webkit-slider-thumb]:h-6 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-gigo-magenta [&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(224,64,251,0.5)] [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:active:cursor-grabbing
|
|
90
|
+
[&::-moz-range-thumb]:w-6 [&::-moz-range-thumb]:h-6 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-gigo-magenta [&::-moz-range-thumb]:border-none"
|
|
91
|
+
style="background: linear-gradient(to right, rgba(224,64,251,0.4) {fillPct}%, var(--secondary) {fillPct}%)"
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="flex justify-between text-[9px] font-mono text-(--muted-foreground)">
|
|
96
|
+
<span>{invertDirection ? '🔊 MAX' : '🔇 MIN'}</span>
|
|
97
|
+
<span>{invertDirection ? '🔇 MIN' : '🔊 MAX'}</span>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div class="flex items-center gap-2 rounded-lg border border-(--border) bg-(--card) px-3 py-2">
|
|
101
|
+
<span class="text-xs text-(--muted-foreground)">Actual value:</span>
|
|
102
|
+
<span class="font-mono text-sm font-bold text-gigo-cyan">{Math.round(value)}</span>
|
|
103
|
+
<div class="flex-1"></div>
|
|
104
|
+
{#each Array(5) as _, i}
|
|
105
|
+
<div
|
|
106
|
+
class="w-1 rounded-full transition-all duration-200"
|
|
107
|
+
style:height="{4 + i * 4}px"
|
|
108
|
+
style:background={i < Math.ceil((value / max) * 5) ? '#e040fb' : 'var(--secondary)'}
|
|
109
|
+
></div>
|
|
110
|
+
{/each}
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<p class="text-[10px] font-mono text-(--muted-foreground) text-center">
|
|
114
|
+
The slider is inverted. The labels lie. It jumps on its own. Enjoy.
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value?: number;
|
|
3
|
+
min?: number;
|
|
4
|
+
max?: number;
|
|
5
|
+
step?: number;
|
|
6
|
+
invertDirection?: boolean;
|
|
7
|
+
jumpAround?: boolean;
|
|
8
|
+
fakeLabels?: boolean;
|
|
9
|
+
class?: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
};
|
|
12
|
+
declare const VolumeSlider: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
13
|
+
type VolumeSlider = ReturnType<typeof VolumeSlider>;
|
|
14
|
+
export default VolumeSlider;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn, chaosClasses, randomGarbageText, chaosRandom } from '../../utils/cn.js';
|
|
3
|
+
import type { GigoButtonProps, ButtonVariant, ButtonSize } from '../../types/index.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
variant = 'default',
|
|
7
|
+
size = 'md',
|
|
8
|
+
disabled = false,
|
|
9
|
+
loading = false,
|
|
10
|
+
chaos = false,
|
|
11
|
+
chaosLevel = 5,
|
|
12
|
+
educational = false,
|
|
13
|
+
a11yWarning = false,
|
|
14
|
+
escapeOnHover = false,
|
|
15
|
+
randomizeText = false,
|
|
16
|
+
multiClickRequired = 1,
|
|
17
|
+
children,
|
|
18
|
+
onclick,
|
|
19
|
+
class: className,
|
|
20
|
+
...restProps
|
|
21
|
+
}: GigoButtonProps = $props();
|
|
22
|
+
|
|
23
|
+
let clickCount = $state(0);
|
|
24
|
+
let displayText = $state('');
|
|
25
|
+
let offsetX = $state(0);
|
|
26
|
+
let offsetY = $state(0);
|
|
27
|
+
let showWarning = $state(false);
|
|
28
|
+
let ripples = $state<Array<{ id: number; x: number; y: number; size: number }>>([]);
|
|
29
|
+
let nextRippleId = $state(0);
|
|
30
|
+
|
|
31
|
+
const variantClasses: Record<ButtonVariant, string> = {
|
|
32
|
+
default:
|
|
33
|
+
'bg-(--primary) text-(--primary-foreground) hover:shadow-(--glow-primary) hover:brightness-110',
|
|
34
|
+
destructive:
|
|
35
|
+
'bg-(--destructive) text-(--destructive-foreground) hover:shadow-(--glow-destructive) hover:brightness-110',
|
|
36
|
+
outline:
|
|
37
|
+
'border border-(--border) bg-transparent text-(--foreground) hover:bg-(--accent) hover:border-(--primary)/40',
|
|
38
|
+
secondary:
|
|
39
|
+
'bg-(--secondary) text-(--secondary-foreground) hover:bg-(--accent) hover:text-(--accent-foreground)',
|
|
40
|
+
ghost:
|
|
41
|
+
'text-(--foreground) hover:bg-(--accent)',
|
|
42
|
+
link:
|
|
43
|
+
'text-gigo-cyan underline-offset-4 hover:underline decoration-gigo-cyan/50',
|
|
44
|
+
'chaos-primary':
|
|
45
|
+
'bg-linear-to-r from-gigo-magenta via-gigo-cyan to-gigo-lime bg-[length:200%_auto] animate-gigo-gradient text-white font-semibold shadow-(--glow-primary)',
|
|
46
|
+
'chaos-destructive':
|
|
47
|
+
'bg-gigo-red text-white animate-gigo-shake shadow-(--glow-destructive)',
|
|
48
|
+
'chaos-nightmare':
|
|
49
|
+
'bg-linear-to-r from-gigo-magenta via-gigo-pink to-gigo-orange bg-[length:300%_auto] animate-gigo-gradient text-white font-bold animate-gigo-nightmare shadow-(--glow-primary)'
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const sizeClasses: Record<ButtonSize, string> = {
|
|
53
|
+
xs: 'h-7 px-2.5 text-xs rounded-md',
|
|
54
|
+
sm: 'h-8 px-3.5 text-sm rounded-md',
|
|
55
|
+
md: 'h-9 px-5 text-sm rounded-lg',
|
|
56
|
+
lg: 'h-10 px-6 text-base rounded-lg',
|
|
57
|
+
xl: 'h-12 px-8 text-lg rounded-xl',
|
|
58
|
+
'chaos-sm': 'h-6 px-1 text-[10px] rounded-none',
|
|
59
|
+
'chaos-lg': 'h-16 px-12 text-2xl rounded-full',
|
|
60
|
+
'chaos-xl': 'h-20 px-16 text-4xl rounded-3xl'
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
$effect(() => {
|
|
64
|
+
if (!chaos || !randomizeText) return;
|
|
65
|
+
const interval = setInterval(() => {
|
|
66
|
+
displayText = randomGarbageText();
|
|
67
|
+
}, 1200);
|
|
68
|
+
return () => clearInterval(interval);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function handleMouseEnter() {
|
|
72
|
+
if (!chaos || !escapeOnHover) return;
|
|
73
|
+
offsetX = (Math.random() - 0.5) * 200;
|
|
74
|
+
offsetY = (Math.random() - 0.5) * 200;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function spawnRipple(e: MouseEvent) {
|
|
78
|
+
const btn = e.currentTarget as HTMLElement;
|
|
79
|
+
const rect = btn.getBoundingClientRect();
|
|
80
|
+
const size = Math.max(rect.width, rect.height);
|
|
81
|
+
const id = nextRippleId++;
|
|
82
|
+
ripples = [...ripples, {
|
|
83
|
+
id,
|
|
84
|
+
x: e.clientX - rect.left - size / 2,
|
|
85
|
+
y: e.clientY - rect.top - size / 2,
|
|
86
|
+
size
|
|
87
|
+
}];
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
ripples = ripples.filter(r => r.id !== id);
|
|
90
|
+
}, 600);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function handleClick(e: MouseEvent) {
|
|
94
|
+
spawnRipple(e);
|
|
95
|
+
|
|
96
|
+
if (chaos && multiClickRequired > 1) {
|
|
97
|
+
clickCount++;
|
|
98
|
+
if (clickCount < multiClickRequired) {
|
|
99
|
+
showWarning = true;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
clickCount = 0;
|
|
103
|
+
showWarning = false;
|
|
104
|
+
}
|
|
105
|
+
onclick?.(e as MouseEvent & { currentTarget: EventTarget & HTMLButtonElement });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let computedClass = $derived(
|
|
109
|
+
cn(
|
|
110
|
+
'gigo-ripple relative inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-all duration-300 ease-out cursor-pointer select-none',
|
|
111
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--ring) focus-visible:ring-offset-2 focus-visible:ring-offset-(--background)',
|
|
112
|
+
'disabled:pointer-events-none disabled:opacity-40',
|
|
113
|
+
'active:scale-[0.97] hover:scale-[1.02]',
|
|
114
|
+
variantClasses[variant],
|
|
115
|
+
sizeClasses[size],
|
|
116
|
+
chaos && chaosClasses(chaosLevel),
|
|
117
|
+
className
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<button
|
|
123
|
+
class={computedClass}
|
|
124
|
+
{disabled}
|
|
125
|
+
aria-disabled={disabled || loading}
|
|
126
|
+
style:transform="translate({offsetX}px, {offsetY}px)"
|
|
127
|
+
style:transition="transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)"
|
|
128
|
+
onmouseenter={handleMouseEnter}
|
|
129
|
+
onclick={handleClick}
|
|
130
|
+
{...restProps}
|
|
131
|
+
>
|
|
132
|
+
{#each ripples as ripple (ripple.id)}
|
|
133
|
+
<span
|
|
134
|
+
class="gigo-ripple-circle"
|
|
135
|
+
style:width="{ripple.size}px"
|
|
136
|
+
style:height="{ripple.size}px"
|
|
137
|
+
style:left="{ripple.x}px"
|
|
138
|
+
style:top="{ripple.y}px"
|
|
139
|
+
></span>
|
|
140
|
+
{/each}
|
|
141
|
+
|
|
142
|
+
{#if loading}
|
|
143
|
+
<svg class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
|
|
144
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
145
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
|
146
|
+
</svg>
|
|
147
|
+
{/if}
|
|
148
|
+
|
|
149
|
+
{#if chaos && randomizeText && displayText}
|
|
150
|
+
{displayText}
|
|
151
|
+
{:else if children}
|
|
152
|
+
{@render children()}
|
|
153
|
+
{:else}
|
|
154
|
+
Button
|
|
155
|
+
{/if}
|
|
156
|
+
</button>
|
|
157
|
+
|
|
158
|
+
{#if showWarning && educational}
|
|
159
|
+
<span class="ml-2 text-xs text-gigo-magenta animate-gigo-pulse-glow font-mono">
|
|
160
|
+
{multiClickRequired - clickCount} more click(s)
|
|
161
|
+
</span>
|
|
162
|
+
{/if}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn, chaosClasses, chaosRandom } from '../../utils/cn.js';
|
|
3
|
+
import type { GigoCardProps } from '../../types/index.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
title = '',
|
|
7
|
+
description = '',
|
|
8
|
+
chaos = false,
|
|
9
|
+
chaosLevel = 5,
|
|
10
|
+
educational = false,
|
|
11
|
+
a11yWarning = false,
|
|
12
|
+
randomRotation = false,
|
|
13
|
+
resistClicks = false,
|
|
14
|
+
zIndexWar = false,
|
|
15
|
+
children,
|
|
16
|
+
header,
|
|
17
|
+
footer,
|
|
18
|
+
onclick,
|
|
19
|
+
class: className,
|
|
20
|
+
...restProps
|
|
21
|
+
}: GigoCardProps = $props();
|
|
22
|
+
|
|
23
|
+
let rotation = $state(0);
|
|
24
|
+
let clickCount = $state(0);
|
|
25
|
+
let currentZ = $state(1);
|
|
26
|
+
let tiltX = $state(0);
|
|
27
|
+
let tiltY = $state(0);
|
|
28
|
+
let spotlightX = $state(50);
|
|
29
|
+
let spotlightY = $state(50);
|
|
30
|
+
let isHovered = $state(false);
|
|
31
|
+
let cardEl: HTMLDivElement | undefined = $state();
|
|
32
|
+
|
|
33
|
+
$effect(() => {
|
|
34
|
+
if (!chaos || !randomRotation) return;
|
|
35
|
+
const interval = setInterval(() => {
|
|
36
|
+
rotation = (Math.random() - 0.5) * 20;
|
|
37
|
+
}, 2000);
|
|
38
|
+
return () => clearInterval(interval);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function handleMouseMove(e: MouseEvent) {
|
|
42
|
+
if (!cardEl) return;
|
|
43
|
+
const rect = cardEl.getBoundingClientRect();
|
|
44
|
+
const x = (e.clientX - rect.left) / rect.width;
|
|
45
|
+
const y = (e.clientY - rect.top) / rect.height;
|
|
46
|
+
tiltX = (y - 0.5) * -12;
|
|
47
|
+
tiltY = (x - 0.5) * 12;
|
|
48
|
+
spotlightX = x * 100;
|
|
49
|
+
spotlightY = y * 100;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleMouseEnter() { isHovered = true; }
|
|
53
|
+
function handleMouseLeave() {
|
|
54
|
+
isHovered = false;
|
|
55
|
+
tiltX = 0;
|
|
56
|
+
tiltY = 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleClick(e: MouseEvent) {
|
|
60
|
+
if (chaos && resistClicks) {
|
|
61
|
+
clickCount++;
|
|
62
|
+
if (clickCount < 3) return;
|
|
63
|
+
clickCount = 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (chaos && zIndexWar) {
|
|
67
|
+
currentZ = chaosRandom(100) + 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onclick?.(e as MouseEvent & { currentTarget: EventTarget & HTMLDivElement });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let cardClass = $derived(
|
|
74
|
+
cn(
|
|
75
|
+
'relative overflow-hidden rounded-xl border border-(--border) bg-(--surface-1) text-(--foreground) transition-all duration-300 ease-out',
|
|
76
|
+
'hover:border-(--surface-border-hover)',
|
|
77
|
+
chaos && chaosClasses(chaosLevel),
|
|
78
|
+
className
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
84
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
85
|
+
<div
|
|
86
|
+
class="group"
|
|
87
|
+
style:perspective="800px"
|
|
88
|
+
style:z-index={zIndexWar ? currentZ : undefined}
|
|
89
|
+
>
|
|
90
|
+
<div
|
|
91
|
+
bind:this={cardEl}
|
|
92
|
+
class={cardClass}
|
|
93
|
+
style:transform="rotate({rotation}deg) rotateX({tiltX}deg) rotateY({tiltY}deg)"
|
|
94
|
+
style:transition="transform 0.2s ease-out, border-color 0.3s, box-shadow 0.3s"
|
|
95
|
+
style:box-shadow={isHovered ? '0 0 30px rgba(224, 64, 251, 0.1), 0 20px 40px rgba(0, 0, 0, 0.3)' : '0 4px 20px rgba(0, 0, 0, 0.2)'}
|
|
96
|
+
onmousemove={handleMouseMove}
|
|
97
|
+
onmouseenter={handleMouseEnter}
|
|
98
|
+
onmouseleave={handleMouseLeave}
|
|
99
|
+
onclick={handleClick}
|
|
100
|
+
{...restProps}
|
|
101
|
+
>
|
|
102
|
+
<!-- Spotlight overlay -->
|
|
103
|
+
<div
|
|
104
|
+
class="pointer-events-none absolute inset-0 z-10 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
|
105
|
+
style:background="radial-gradient(circle at {spotlightX}% {spotlightY}%, rgba(224, 64, 251, 0.08), transparent 60%)"
|
|
106
|
+
></div>
|
|
107
|
+
|
|
108
|
+
{#if header}
|
|
109
|
+
<div class="relative z-20 border-b border-(--border) p-4">
|
|
110
|
+
{@render header()}
|
|
111
|
+
</div>
|
|
112
|
+
{/if}
|
|
113
|
+
|
|
114
|
+
<div class="relative z-20 p-6">
|
|
115
|
+
{#if title}
|
|
116
|
+
<h3 class="text-lg font-semibold leading-none tracking-tight">{title}</h3>
|
|
117
|
+
{/if}
|
|
118
|
+
{#if description}
|
|
119
|
+
<p class="mt-2 text-sm text-(--muted-foreground) leading-relaxed">{description}</p>
|
|
120
|
+
{/if}
|
|
121
|
+
|
|
122
|
+
{#if children}
|
|
123
|
+
<div class="mt-4">
|
|
124
|
+
{@render children()}
|
|
125
|
+
</div>
|
|
126
|
+
{/if}
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{#if footer}
|
|
130
|
+
<div class="relative z-20 border-t border-(--border) p-4">
|
|
131
|
+
{@render footer()}
|
|
132
|
+
</div>
|
|
133
|
+
{/if}
|
|
134
|
+
|
|
135
|
+
{#if educational && chaos && resistClicks && clickCount > 0}
|
|
136
|
+
<p class="relative z-20 px-6 pb-4 text-xs text-gigo-magenta font-mono animate-gigo-entrance">
|
|
137
|
+
Click {3 - clickCount} more time(s) to actually interact!
|
|
138
|
+
</p>
|
|
139
|
+
{/if}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|