@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.
Files changed (96) hide show
  1. package/README.md +42 -0
  2. package/dist/assets/favicon.svg +1 -0
  3. package/dist/components/chaos/CatchSubmit.svelte +144 -0
  4. package/dist/components/chaos/CatchSubmit.svelte.d.ts +11 -0
  5. package/dist/components/chaos/ChaosButton.svelte +132 -0
  6. package/dist/components/chaos/ChaosButton.svelte.d.ts +4 -0
  7. package/dist/components/chaos/ChaosForm.svelte +206 -0
  8. package/dist/components/chaos/ChaosForm.svelte.d.ts +10 -0
  9. package/dist/components/chaos/ColorPickerWrong.svelte +141 -0
  10. package/dist/components/chaos/ColorPickerWrong.svelte.d.ts +11 -0
  11. package/dist/components/chaos/DropdownCalc.svelte +195 -0
  12. package/dist/components/chaos/DropdownCalc.svelte.d.ts +8 -0
  13. package/dist/components/chaos/GhostCard.svelte +157 -0
  14. package/dist/components/chaos/GhostCard.svelte.d.ts +13 -0
  15. package/dist/components/chaos/GravityInput.svelte +161 -0
  16. package/dist/components/chaos/GravityInput.svelte.d.ts +13 -0
  17. package/dist/components/chaos/PasswordPeekhole.svelte +141 -0
  18. package/dist/components/chaos/PasswordPeekhole.svelte.d.ts +11 -0
  19. package/dist/components/chaos/ProgressDoom.svelte +139 -0
  20. package/dist/components/chaos/ProgressDoom.svelte.d.ts +12 -0
  21. package/dist/components/chaos/RotaryDial.svelte +221 -0
  22. package/dist/components/chaos/RotaryDial.svelte.d.ts +10 -0
  23. package/dist/components/chaos/SliderPhone.svelte +92 -0
  24. package/dist/components/chaos/SliderPhone.svelte.d.ts +10 -0
  25. package/dist/components/chaos/TermsSidescroll.svelte +121 -0
  26. package/dist/components/chaos/TermsSidescroll.svelte.d.ts +12 -0
  27. package/dist/components/chaos/VolumeSlider.svelte +116 -0
  28. package/dist/components/chaos/VolumeSlider.svelte.d.ts +14 -0
  29. package/dist/components/ui/Button.svelte +162 -0
  30. package/dist/components/ui/Button.svelte.d.ts +4 -0
  31. package/dist/components/ui/Card.svelte +141 -0
  32. package/dist/components/ui/Card.svelte.d.ts +4 -0
  33. package/dist/components/ui/Carousel.svelte +164 -0
  34. package/dist/components/ui/Carousel.svelte.d.ts +4 -0
  35. package/dist/components/ui/Form.svelte +178 -0
  36. package/dist/components/ui/Form.svelte.d.ts +4 -0
  37. package/dist/components/ui/Input.svelte +135 -0
  38. package/dist/components/ui/Input.svelte.d.ts +4 -0
  39. package/dist/components/ui/Modal.svelte +173 -0
  40. package/dist/components/ui/Modal.svelte.d.ts +4 -0
  41. package/dist/components/ui/Navigation.svelte +91 -0
  42. package/dist/components/ui/Navigation.svelte.d.ts +4 -0
  43. package/dist/docs/categories.d.ts +13 -0
  44. package/dist/docs/categories.js +17 -0
  45. package/dist/docs/component-data.d.ts +6 -0
  46. package/dist/docs/component-data.js +49 -0
  47. package/dist/docs/components/badui/catch-submit.d.ts +2 -0
  48. package/dist/docs/components/badui/catch-submit.js +49 -0
  49. package/dist/docs/components/badui/color-picker-wrong.d.ts +2 -0
  50. package/dist/docs/components/badui/color-picker-wrong.js +40 -0
  51. package/dist/docs/components/badui/dropdown-calc.d.ts +2 -0
  52. package/dist/docs/components/badui/dropdown-calc.js +28 -0
  53. package/dist/docs/components/badui/ghost-card.d.ts +2 -0
  54. package/dist/docs/components/badui/ghost-card.js +54 -0
  55. package/dist/docs/components/badui/gravity-input.d.ts +2 -0
  56. package/dist/docs/components/badui/gravity-input.js +64 -0
  57. package/dist/docs/components/badui/password-peekhole.d.ts +2 -0
  58. package/dist/docs/components/badui/password-peekhole.js +51 -0
  59. package/dist/docs/components/badui/progress-doom.d.ts +2 -0
  60. package/dist/docs/components/badui/progress-doom.js +48 -0
  61. package/dist/docs/components/badui/rotary-dial.d.ts +2 -0
  62. package/dist/docs/components/badui/rotary-dial.js +40 -0
  63. package/dist/docs/components/badui/slider-phone.d.ts +2 -0
  64. package/dist/docs/components/badui/slider-phone.js +40 -0
  65. package/dist/docs/components/badui/terms-sidescroll.d.ts +2 -0
  66. package/dist/docs/components/badui/terms-sidescroll.js +46 -0
  67. package/dist/docs/components/badui/volume-slider.d.ts +2 -0
  68. package/dist/docs/components/badui/volume-slider.js +52 -0
  69. package/dist/docs/components/chaos/chaos-button.d.ts +2 -0
  70. package/dist/docs/components/chaos/chaos-button.js +48 -0
  71. package/dist/docs/components/chaos/chaos-form.d.ts +2 -0
  72. package/dist/docs/components/chaos/chaos-form.js +68 -0
  73. package/dist/docs/components/standard/button.d.ts +2 -0
  74. package/dist/docs/components/standard/button.js +66 -0
  75. package/dist/docs/components/standard/card.d.ts +2 -0
  76. package/dist/docs/components/standard/card.js +55 -0
  77. package/dist/docs/components/standard/carousel.d.ts +2 -0
  78. package/dist/docs/components/standard/carousel.js +63 -0
  79. package/dist/docs/components/standard/form.d.ts +2 -0
  80. package/dist/docs/components/standard/form.js +57 -0
  81. package/dist/docs/components/standard/input.d.ts +2 -0
  82. package/dist/docs/components/standard/input.js +65 -0
  83. package/dist/docs/components/standard/modal.d.ts +2 -0
  84. package/dist/docs/components/standard/modal.js +63 -0
  85. package/dist/docs/components/standard/navigation.d.ts +2 -0
  86. package/dist/docs/components/standard/navigation.js +51 -0
  87. package/dist/docs/types.d.ts +16 -0
  88. package/dist/docs/types.js +1 -0
  89. package/dist/index.d.ts +22 -0
  90. package/dist/index.js +21 -0
  91. package/dist/styles/globals.css +569 -0
  92. package/dist/types/index.d.ts +177 -0
  93. package/dist/types/index.js +1 -0
  94. package/dist/utils/cn.d.ts +9 -0
  95. package/dist/utils/cn.js +90 -0
  96. package/package.json +61 -0
@@ -0,0 +1,141 @@
1
+ <script lang="ts">
2
+ import { cn } from '../../utils/cn.js';
3
+
4
+ let {
5
+ value = $bindable('#e040fb'),
6
+ offsetHue = true,
7
+ swapChannels = true,
8
+ delayUpdate = true,
9
+ class: className,
10
+ ...restProps
11
+ }: {
12
+ value?: string;
13
+ offsetHue?: boolean;
14
+ swapChannels?: boolean;
15
+ delayUpdate?: boolean;
16
+ class?: string;
17
+ [key: string]: unknown;
18
+ } = $props();
19
+
20
+ let pickerValue = $state('#e040fb');
21
+ let displayColor = $state('#e040fb');
22
+ let actualColor = $state('#e040fb');
23
+ let labelLie = $state('');
24
+
25
+ const COLOR_NAMES: Record<string, string> = {
26
+ red: 'Definitely Blue', blue: 'Clearly Red', green: 'Obviously Purple',
27
+ yellow: 'Totally Black', black: 'Pure White', white: 'Deep Black',
28
+ pink: 'Forest Green', orange: 'Ocean Blue', purple: 'Grass Green',
29
+ cyan: 'Warm Orange', magenta: 'Cool Teal', lime: 'Hot Pink',
30
+ };
31
+
32
+ function hexToRgb(hex: string): [number, number, number] {
33
+ const h = hex.replace('#', '');
34
+ return [
35
+ parseInt(h.substring(0, 2), 16),
36
+ parseInt(h.substring(2, 4), 16),
37
+ parseInt(h.substring(4, 6), 16),
38
+ ];
39
+ }
40
+
41
+ function rgbToHex(r: number, g: number, b: number): string {
42
+ return '#' + [r, g, b].map(c => Math.max(0, Math.min(255, Math.round(c))).toString(16).padStart(2, '0')).join('');
43
+ }
44
+
45
+ function corruptColor(hex: string): string {
46
+ let [r, g, b] = hexToRgb(hex);
47
+
48
+ if (swapChannels) {
49
+ [r, g, b] = [b, r, g];
50
+ }
51
+
52
+ if (offsetHue) {
53
+ r = (r + 128) % 256;
54
+ g = (g + 64) % 256;
55
+ b = (b + 192) % 256;
56
+ }
57
+
58
+ return rgbToHex(r, g, b);
59
+ }
60
+
61
+ function getClosestColorName(hex: string): string {
62
+ const [r, g, b] = hexToRgb(hex);
63
+ const names = Object.keys(COLOR_NAMES);
64
+ const hue = Math.atan2(Math.sqrt(3) * (g - b), 2 * r - g - b) * 180 / Math.PI;
65
+ const idx = Math.abs(Math.round(hue / 30)) % names.length;
66
+ return COLOR_NAMES[names[idx]] || 'Unknown Color';
67
+ }
68
+
69
+ function handlePick(e: Event) {
70
+ const raw = (e.target as HTMLInputElement).value;
71
+ pickerValue = raw;
72
+
73
+ const corrupted = corruptColor(raw);
74
+
75
+ if (delayUpdate) {
76
+ setTimeout(() => {
77
+ displayColor = corrupted;
78
+ actualColor = raw;
79
+ value = corrupted;
80
+ labelLie = getClosestColorName(raw);
81
+ }, 300 + Math.random() * 500);
82
+ } else {
83
+ displayColor = corrupted;
84
+ actualColor = raw;
85
+ value = corrupted;
86
+ labelLie = getClosestColorName(raw);
87
+ }
88
+ }
89
+ </script>
90
+
91
+ <div class={cn('space-y-4 max-w-xs', className)} {...restProps}>
92
+ <div class="flex items-center justify-between">
93
+ <span class="text-xs font-mono text-(--muted-foreground)">Pick a color</span>
94
+ {#if labelLie}
95
+ <span class="text-xs font-mono text-gigo-cyan">{labelLie}</span>
96
+ {/if}
97
+ </div>
98
+
99
+ <div class="flex items-center gap-4">
100
+ <input
101
+ type="color"
102
+ value={pickerValue}
103
+ oninput={handlePick}
104
+ class="w-16 h-16 rounded-xl border border-(--border) cursor-pointer bg-transparent
105
+ [&::-webkit-color-swatch-wrapper]:p-1 [&::-webkit-color-swatch]:rounded-lg [&::-webkit-color-swatch]:border-none
106
+ [&::-moz-color-swatch]:rounded-lg [&::-moz-color-swatch]:border-none"
107
+ />
108
+
109
+ <div class="flex-1 space-y-2">
110
+ <div class="flex items-center gap-2">
111
+ <div class="w-8 h-8 rounded-lg border border-(--border)" style:background={actualColor}></div>
112
+ <span class="text-[10px] font-mono text-(--muted-foreground)">You picked</span>
113
+ <span class="font-mono text-xs">{actualColor}</span>
114
+ </div>
115
+ <div class="flex items-center gap-2">
116
+ <div class="w-8 h-8 rounded-lg border border-(--border)" style:background={displayColor}></div>
117
+ <span class="text-[10px] font-mono text-(--muted-foreground)">You get</span>
118
+ <span class="font-mono text-xs text-gigo-magenta">{displayColor}</span>
119
+ </div>
120
+ </div>
121
+ </div>
122
+
123
+ <div
124
+ class="h-20 w-full rounded-xl border border-(--border) transition-colors duration-500"
125
+ style:background={displayColor}
126
+ >
127
+ <div class="flex h-full items-center justify-center">
128
+ <span
129
+ class="text-sm font-mono font-bold px-3 py-1 rounded-lg"
130
+ style:background="rgba(0,0,0,0.5)"
131
+ style:color="white"
132
+ >
133
+ {displayColor}
134
+ </span>
135
+ </div>
136
+ </div>
137
+
138
+ <p class="text-[10px] font-mono text-(--muted-foreground) text-center">
139
+ Every color you pick gets channel-swapped and hue-shifted. Good luck matching anything.
140
+ </p>
141
+ </div>
@@ -0,0 +1,11 @@
1
+ type $$ComponentProps = {
2
+ value?: string;
3
+ offsetHue?: boolean;
4
+ swapChannels?: boolean;
5
+ delayUpdate?: boolean;
6
+ class?: string;
7
+ [key: string]: unknown;
8
+ };
9
+ declare const ColorPickerWrong: import("svelte").Component<$$ComponentProps, {}, "value">;
10
+ type ColorPickerWrong = ReturnType<typeof ColorPickerWrong>;
11
+ export default ColorPickerWrong;
@@ -0,0 +1,195 @@
1
+ <script lang="ts">
2
+ import { cn } from '../../utils/cn.js';
3
+
4
+ let {
5
+ value = $bindable(''),
6
+ class: className,
7
+ ...restProps
8
+ }: {
9
+ value?: string;
10
+ class?: string;
11
+ [key: string]: unknown;
12
+ } = $props();
13
+
14
+ let expression = $state<string[]>([]);
15
+ let result = $state<string>('');
16
+ let showResult = $state(false);
17
+
18
+ const DIGIT_OPTIONS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
19
+ const OPERATOR_OPTIONS = ['+', '-', '×', '÷'];
20
+ const DECIMAL_OPTIONS = ['.'];
21
+
22
+ let digitDropdowns = $state<(string | null)[]>([null, null, null, null, null, null, null, null]);
23
+ let operatorDropdown = $state<string | null>(null);
24
+
25
+ function addDigit(index: number, val: string) {
26
+ digitDropdowns = digitDropdowns.map((d, i) => i === index ? val : d);
27
+ rebuildExpression();
28
+ }
29
+
30
+ function setOperator(val: string) {
31
+ operatorDropdown = val;
32
+ rebuildExpression();
33
+ }
34
+
35
+ function rebuildExpression() {
36
+ const parts: string[] = [];
37
+ // First number
38
+ const num1Digits = digitDropdowns.slice(0, 4).filter(d => d !== null);
39
+ if (num1Digits.length) parts.push(num1Digits.join(''));
40
+
41
+ // Operator
42
+ if (operatorDropdown) parts.push(operatorDropdown);
43
+
44
+ // Second number
45
+ const num2Digits = digitDropdowns.slice(4, 8).filter(d => d !== null);
46
+ if (num2Digits.length) parts.push(num2Digits.join(''));
47
+
48
+ expression = parts;
49
+ value = parts.join(' ');
50
+ showResult = false;
51
+ }
52
+
53
+ function calculate() {
54
+ const num1Str = digitDropdowns.slice(0, 4).filter(d => d !== null).join('');
55
+ const num2Str = digitDropdowns.slice(4, 8).filter(d => d !== null).join('');
56
+ const num1 = parseFloat(num1Str);
57
+ const num2 = parseFloat(num2Str);
58
+
59
+ if (isNaN(num1) || isNaN(num2) || !operatorDropdown) {
60
+ result = 'ERR: Incomplete';
61
+ showResult = true;
62
+ return;
63
+ }
64
+
65
+ let res: number;
66
+ switch (operatorDropdown) {
67
+ case '+': res = num1 + num2; break;
68
+ case '-': res = num1 - num2; break;
69
+ case '×': res = num1 * num2; break;
70
+ case '÷': res = num2 === 0 ? NaN : num1 / num2; break;
71
+ default: res = NaN;
72
+ }
73
+
74
+ result = isNaN(res) ? 'ERR: ÷ by 0' : String(Math.round(res * 10000) / 10000);
75
+ showResult = true;
76
+ value = `${num1} ${operatorDropdown} ${num2} = ${result}`;
77
+ }
78
+
79
+ function clearAll() {
80
+ digitDropdowns = [null, null, null, null, null, null, null, null];
81
+ operatorDropdown = null;
82
+ expression = [];
83
+ result = '';
84
+ showResult = false;
85
+ value = '';
86
+ }
87
+ </script>
88
+
89
+ <div class={cn('space-y-4 max-w-lg', className)} {...restProps}>
90
+ <!-- Display -->
91
+ <div class="rounded-xl border border-(--border) bg-(--secondary) px-4 py-3">
92
+ <div class="font-mono text-right text-sm text-(--muted-foreground) min-h-[1.2em]">
93
+ {expression.join(' ') || '_ ? _ = ?'}
94
+ </div>
95
+ {#if showResult}
96
+ <div class="font-mono text-right text-2xl font-bold mt-1"
97
+ style:color={result.startsWith('ERR') ? '#ff1744' : '#76ff03'}>
98
+ {result}
99
+ </div>
100
+ {/if}
101
+ </div>
102
+
103
+ <!-- Dropdown rows -->
104
+ <div class="space-y-3">
105
+ <!-- First number label -->
106
+ <div class="text-[10px] font-mono text-(--muted-foreground) uppercase tracking-wider">First Number (select each digit)</div>
107
+
108
+ <div class="flex gap-2 flex-wrap">
109
+ {#each [0, 1, 2, 3] as i}
110
+ <div class="flex flex-col items-center gap-1">
111
+ <span class="text-[9px] font-mono text-(--muted-foreground)">
112
+ {i === 0 ? 'Thou.' : i === 1 ? 'Hund.' : i === 2 ? 'Tens' : 'Ones'}
113
+ </span>
114
+ <select
115
+ class="w-16 h-9 rounded-lg border border-(--border) bg-(--card) text-center font-mono text-sm
116
+ text-(--foreground) cursor-pointer appearance-none
117
+ focus:outline-none focus:border-gigo-magenta focus:ring-1 focus:ring-gigo-magenta/30"
118
+ value={digitDropdowns[i] ?? ''}
119
+ onchange={(e) => addDigit(i, (e.target as HTMLSelectElement).value)}
120
+ >
121
+ <option value="" disabled selected>—</option>
122
+ {#each DIGIT_OPTIONS as d}
123
+ <option value={d}>{d}</option>
124
+ {/each}
125
+ </select>
126
+ </div>
127
+ {/each}
128
+ </div>
129
+
130
+ <!-- Operator -->
131
+ <div class="text-[10px] font-mono text-(--muted-foreground) uppercase tracking-wider">Operator</div>
132
+
133
+ <select
134
+ class="w-20 h-9 rounded-lg border border-(--border) bg-(--card) text-center font-mono text-lg
135
+ text-gigo-magenta cursor-pointer appearance-none
136
+ focus:outline-none focus:border-gigo-magenta focus:ring-1 focus:ring-gigo-magenta/30"
137
+ value={operatorDropdown ?? ''}
138
+ onchange={(e) => setOperator((e.target as HTMLSelectElement).value)}
139
+ >
140
+ <option value="" disabled selected>?</option>
141
+ {#each OPERATOR_OPTIONS as op}
142
+ <option value={op}>{op}</option>
143
+ {/each}
144
+ </select>
145
+
146
+ <!-- Second number -->
147
+ <div class="text-[10px] font-mono text-(--muted-foreground) uppercase tracking-wider">Second Number (select each digit)</div>
148
+
149
+ <div class="flex gap-2 flex-wrap">
150
+ {#each [4, 5, 6, 7] as i}
151
+ <div class="flex flex-col items-center gap-1">
152
+ <span class="text-[9px] font-mono text-(--muted-foreground)">
153
+ {i === 4 ? 'Thou.' : i === 5 ? 'Hund.' : i === 6 ? 'Tens' : 'Ones'}
154
+ </span>
155
+ <select
156
+ class="w-16 h-9 rounded-lg border border-(--border) bg-(--card) text-center font-mono text-sm
157
+ text-(--foreground) cursor-pointer appearance-none
158
+ focus:outline-none focus:border-gigo-magenta focus:ring-1 focus:ring-gigo-magenta/30"
159
+ value={digitDropdowns[i] ?? ''}
160
+ onchange={(e) => addDigit(i, (e.target as HTMLSelectElement).value)}
161
+ >
162
+ <option value="" disabled selected>—</option>
163
+ {#each DIGIT_OPTIONS as d}
164
+ <option value={d}>{d}</option>
165
+ {/each}
166
+ </select>
167
+ </div>
168
+ {/each}
169
+ </div>
170
+ </div>
171
+
172
+ <!-- Action buttons -->
173
+ <div class="flex gap-3">
174
+ <button
175
+ class="flex-1 px-4 py-2.5 rounded-xl font-mono text-sm font-bold
176
+ bg-gigo-magenta/15 border border-gigo-magenta/50 text-gigo-magenta
177
+ hover:bg-gigo-magenta/25 transition-colors cursor-pointer"
178
+ onclick={calculate}
179
+ >
180
+ = Calculate
181
+ </button>
182
+ <button
183
+ class="px-4 py-2.5 rounded-xl font-mono text-sm
184
+ bg-(--secondary) border border-(--border) text-(--muted-foreground)
185
+ hover:text-gigo-red hover:border-gigo-red/30 transition-colors cursor-pointer"
186
+ onclick={clearAll}
187
+ >
188
+ Clear
189
+ </button>
190
+ </div>
191
+
192
+ <p class="text-[10px] font-mono text-(--muted-foreground) text-center">
193
+ Every. Single. Digit. Is. A. Dropdown. You're welcome.
194
+ </p>
195
+ </div>
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ value?: string;
3
+ class?: string;
4
+ [key: string]: unknown;
5
+ };
6
+ declare const DropdownCalc: import("svelte").Component<$$ComponentProps, {}, "value">;
7
+ type DropdownCalc = ReturnType<typeof DropdownCalc>;
8
+ export default DropdownCalc;
@@ -0,0 +1,157 @@
1
+ <script lang="ts">
2
+ import { cn } from '../../utils/cn.js';
3
+
4
+ let {
5
+ title = 'Ghost Card',
6
+ description = '',
7
+ children,
8
+ disappearMinMs = 2000,
9
+ disappearMaxMs = 5000,
10
+ glitchChance = 0.6,
11
+ class: className,
12
+ ...restProps
13
+ }: {
14
+ title?: string;
15
+ description?: string;
16
+ children?: import('svelte').Snippet;
17
+ disappearMinMs?: number;
18
+ disappearMaxMs?: number;
19
+ glitchChance?: number;
20
+ class?: string;
21
+ [key: string]: unknown;
22
+ } = $props();
23
+
24
+ let visible = $state(true);
25
+ let glitching = $state(false);
26
+ let offsetX = $state(0);
27
+ let offsetY = $state(0);
28
+ let rotation = $state(0);
29
+ let scaleX = $state(1);
30
+ let clipPath = $state('none');
31
+ let hueShift = $state(0);
32
+ let containerEl: HTMLDivElement | undefined = $state();
33
+
34
+ let glitchContentOpacity = $state(1);
35
+ let glitchTitleStrike = $state(false);
36
+ let glitchTitleText = $state('');
37
+ let glitchScanlineTop = $state(50);
38
+
39
+ const GLITCH_CLIPS = [
40
+ 'polygon(0 0, 100% 0, 100% 45%, 0 45%)',
41
+ 'polygon(0 55%, 100% 55%, 100% 100%, 0 100%)',
42
+ 'polygon(0 0, 60% 0, 60% 100%, 0 100%)',
43
+ 'polygon(20% 10%, 80% 10%, 80% 90%, 20% 90%)',
44
+ 'inset(10% 0 10% 0)',
45
+ 'polygon(0 0, 100% 0, 100% 30%, 70% 30%, 70% 70%, 100% 70%, 100% 100%, 0 100%)',
46
+ ];
47
+
48
+ function corruptTitle(text: string): string {
49
+ return text.split('').map(c =>
50
+ Math.random() > 0.7 ? String.fromCharCode(c.charCodeAt(0) + Math.floor(Math.random() * 5)) : c
51
+ ).join('');
52
+ }
53
+
54
+ $effect(() => {
55
+ const disappear = () => {
56
+ const nextDisappear = disappearMinMs + Math.random() * (disappearMaxMs - disappearMinMs);
57
+
58
+ const timer = setTimeout(() => {
59
+ visible = false;
60
+
61
+ const reappearDelay = 500 + Math.random() * 2000;
62
+ setTimeout(() => {
63
+ offsetX = (Math.random() - 0.5) * 60;
64
+ offsetY = (Math.random() - 0.5) * 40;
65
+ rotation = (Math.random() - 0.5) * 12;
66
+
67
+ if (Math.random() < glitchChance) {
68
+ glitching = true;
69
+ scaleX = Math.random() > 0.5 ? -1 : 1;
70
+ clipPath = Math.random() > 0.3 ? GLITCH_CLIPS[Math.floor(Math.random() * GLITCH_CLIPS.length)] : 'none';
71
+ hueShift = Math.random() * 360;
72
+ glitchContentOpacity = Math.random() > 0.5 ? 0.6 : 1;
73
+ glitchTitleStrike = Math.random() > 0.5;
74
+ glitchTitleText = Math.random() > 0.6 ? corruptTitle(title) : '';
75
+ glitchScanlineTop = Math.random() * 100;
76
+
77
+ setTimeout(() => {
78
+ glitching = false;
79
+ scaleX = 1;
80
+ clipPath = 'none';
81
+ hueShift = 0;
82
+ glitchContentOpacity = 1;
83
+ glitchTitleStrike = false;
84
+ glitchTitleText = '';
85
+ }, 800 + Math.random() * 1500);
86
+ }
87
+
88
+ visible = true;
89
+ disappear();
90
+ }, reappearDelay);
91
+ }, nextDisappear);
92
+
93
+ return timer;
94
+ };
95
+
96
+ const t = disappear();
97
+ return () => clearTimeout(t);
98
+ });
99
+ </script>
100
+
101
+ <div
102
+ class="relative"
103
+ bind:this={containerEl}
104
+ >
105
+ <div
106
+ class={cn(
107
+ 'relative rounded-2xl border bg-(--card) p-6 transition-all duration-500',
108
+ visible ? 'opacity-100' : 'opacity-0 scale-75',
109
+ glitching ? 'border-gigo-red/50' : 'border-(--border)',
110
+ className
111
+ )}
112
+ style:transform="translate({offsetX}px, {offsetY}px) rotate({rotation}deg) scaleX({scaleX})"
113
+ style:clip-path={clipPath}
114
+ style:filter="hue-rotate({hueShift}deg)"
115
+ style:transition="opacity 0.3s, transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1), clip-path 0.2s, filter 0.3s"
116
+ {...restProps}
117
+ >
118
+ {#if glitching}
119
+ <div class="absolute inset-0 pointer-events-none overflow-hidden rounded-2xl z-10">
120
+ <div class="absolute inset-0 bg-gigo-red/5"></div>
121
+ <div
122
+ class="absolute w-full h-0.5 bg-gigo-cyan/30"
123
+ style:top="{glitchScanlineTop}%"
124
+ style:animation="gigo-shimmer 0.5s linear infinite"
125
+ ></div>
126
+ </div>
127
+ {/if}
128
+
129
+ <div class="relative z-0" style:opacity={glitchContentOpacity}>
130
+ {#if title}
131
+ <h3 class="text-lg font-semibold text-(--foreground)" class:line-through={glitchTitleStrike}>
132
+ {glitchTitleText || title}
133
+ </h3>
134
+ {/if}
135
+ {#if description}
136
+ <p class="mt-2 text-sm text-(--muted-foreground)">{description}</p>
137
+ {/if}
138
+ {#if children}
139
+ <div class="mt-4">
140
+ {@render children()}
141
+ </div>
142
+ {/if}
143
+ </div>
144
+
145
+ <!-- Ghost status indicator -->
146
+ <div class="absolute top-3 right-3 flex items-center gap-1.5">
147
+ <div
148
+ class="w-2 h-2 rounded-full transition-colors duration-300"
149
+ style:background={glitching ? '#ff1744' : visible ? '#76ff03' : '#7a7a8e'}
150
+ style:box-shadow={glitching ? '0 0 8px rgba(255, 23, 68, 0.6)' : 'none'}
151
+ ></div>
152
+ <span class="text-[10px] font-mono text-(--muted-foreground)">
153
+ {glitching ? 'CORRUPT' : visible ? 'STABLE' : 'GONE'}
154
+ </span>
155
+ </div>
156
+ </div>
157
+ </div>
@@ -0,0 +1,13 @@
1
+ type $$ComponentProps = {
2
+ title?: string;
3
+ description?: string;
4
+ children?: import('svelte').Snippet;
5
+ disappearMinMs?: number;
6
+ disappearMaxMs?: number;
7
+ glitchChance?: number;
8
+ class?: string;
9
+ [key: string]: unknown;
10
+ };
11
+ declare const GhostCard: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type GhostCard = ReturnType<typeof GhostCard>;
13
+ export default GhostCard;
@@ -0,0 +1,161 @@
1
+ <script lang="ts">
2
+ import { cn } from '../../utils/cn.js';
3
+
4
+ let {
5
+ value = $bindable(''),
6
+ placeholder = 'Catch the digits...',
7
+ spawnInterval = 400,
8
+ maxFalling = 15,
9
+ characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ@.!?#$%',
10
+ arenaHeight = 224,
11
+ class: className,
12
+ ...restProps
13
+ }: {
14
+ value?: string;
15
+ placeholder?: string;
16
+ spawnInterval?: number;
17
+ maxFalling?: number;
18
+ characters?: string;
19
+ arenaHeight?: number;
20
+ class?: string;
21
+ [key: string]: unknown;
22
+ } = $props();
23
+
24
+ interface FallingDigit {
25
+ id: number;
26
+ char: string;
27
+ x: number;
28
+ y: number;
29
+ speed: number;
30
+ caught: boolean;
31
+ }
32
+
33
+ let digits = $state<FallingDigit[]>([]);
34
+ let nextId = $state(0);
35
+ let containerEl: HTMLDivElement | undefined = $state();
36
+ let containerWidth = $state(400);
37
+ let containerHeight = $state(200);
38
+
39
+ $effect(() => {
40
+ const timer = setInterval(() => {
41
+ if (digits.length > maxFalling) return;
42
+ const id = nextId++;
43
+ const w = containerEl?.offsetWidth ?? 400;
44
+ digits = [...digits, {
45
+ id,
46
+ char: characters[Math.floor(Math.random() * characters.length)],
47
+ x: 20 + Math.random() * (w - 60),
48
+ y: -30,
49
+ speed: 0.6 + Math.random() * 1.5,
50
+ caught: false
51
+ }];
52
+ }, spawnInterval);
53
+ return () => clearInterval(timer);
54
+ });
55
+
56
+ // Gravity animation frame
57
+ $effect(() => {
58
+ let running = true;
59
+ const h = containerEl?.offsetHeight ?? 200;
60
+
61
+ function tick() {
62
+ if (!running) return;
63
+ digits = digits
64
+ .map(d => ({
65
+ ...d,
66
+ y: d.caught ? d.y : d.y + d.speed
67
+ }))
68
+ .filter(d => d.y < h + 40 || d.caught);
69
+ requestAnimationFrame(tick);
70
+ }
71
+ requestAnimationFrame(tick);
72
+ return () => { running = false; };
73
+ });
74
+
75
+ // Update container dimensions
76
+ $effect(() => {
77
+ if (!containerEl) return;
78
+ const obs = new ResizeObserver(entries => {
79
+ for (const e of entries) {
80
+ containerWidth = e.contentRect.width;
81
+ containerHeight = e.contentRect.height;
82
+ }
83
+ });
84
+ obs.observe(containerEl);
85
+ return () => obs.disconnect();
86
+ });
87
+
88
+ function catchDigit(digit: FallingDigit) {
89
+ if (digit.caught) return;
90
+ value += digit.char;
91
+ digits = digits.filter(d => d.id !== digit.id);
92
+ }
93
+
94
+ function removeLastChar() {
95
+ value = value.slice(0, -1);
96
+ }
97
+ </script>
98
+
99
+ <div class={cn('space-y-3', className)}>
100
+ <!-- Display current value -->
101
+ <div class="flex items-center gap-2 rounded-xl border border-(--border) bg-(--secondary) px-4 py-2.5 min-h-10.5">
102
+ <span class="text-sm text-(--foreground) font-mono flex-1 break-all">
103
+ {#if value}
104
+ {value}
105
+ {:else}
106
+ <span class="text-(--muted-foreground)">{placeholder}</span>
107
+ {/if}
108
+ </span>
109
+ {#if value}
110
+ <button
111
+ class="text-xs text-(--muted-foreground) hover:text-gigo-red transition-colors px-1.5 py-0.5 rounded"
112
+ onclick={removeLastChar}
113
+ >
114
+ &#x232b;
115
+ </button>
116
+ {/if}
117
+ </div>
118
+
119
+ <!-- Falling digit arena -->
120
+ <div
121
+ bind:this={containerEl}
122
+ class="relative w-full overflow-hidden rounded-2xl border border-(--border) bg-(--card)"
123
+ style:height="{arenaHeight}px"
124
+ {...restProps}
125
+ >
126
+ <!-- Background grid -->
127
+ <div class="absolute inset-0 opacity-[0.03]" style="background-image: linear-gradient(var(--muted-foreground) 1px, transparent 1px), linear-gradient(90deg, var(--muted-foreground) 1px, transparent 1px); background-size: 32px 32px;"></div>
128
+
129
+ <!-- Catch zone at bottom -->
130
+ <div class="absolute bottom-0 left-0 right-0 h-10 bg-linear-to-t from-gigo-magenta/10 to-transparent pointer-events-none flex items-end justify-center pb-1">
131
+ <span class="text-[10px] font-mono text-gigo-magenta/50 uppercase tracking-widest">catch zone</span>
132
+ </div>
133
+
134
+ <!-- Falling digits -->
135
+ {#each digits as digit (digit.id)}
136
+ <button
137
+ class="absolute flex items-center justify-center w-9 h-9 rounded-lg text-sm font-mono font-bold cursor-pointer transition-all duration-100 hover:scale-125 active:scale-90 select-none"
138
+ style:left="{digit.x}px"
139
+ style:top="{digit.y}px"
140
+ style:background={digit.y > (containerHeight - 60) ? 'rgba(224, 64, 251, 0.2)' : 'rgba(255, 255, 255, 0.05)'}
141
+ style:border="1px solid {digit.y > (containerHeight - 60) ? 'rgba(224, 64, 251, 0.5)' : 'rgba(255, 255, 255, 0.1)'}"
142
+ style:color={digit.y > (containerHeight - 60) ? '#e040fb' : 'var(--foreground)'}
143
+ style:box-shadow={digit.y > (containerHeight - 60) ? '0 0 12px rgba(224, 64, 251, 0.3)' : 'none'}
144
+ onclick={() => catchDigit(digit)}
145
+ >
146
+ {digit.char}
147
+ </button>
148
+ {/each}
149
+
150
+ <!-- Instructions -->
151
+ {#if digits.length === 0 && !value}
152
+ <div class="absolute inset-0 flex items-center justify-center text-sm text-(--muted-foreground)">
153
+ Digits spawning...
154
+ </div>
155
+ {/if}
156
+ </div>
157
+
158
+ <p class="text-[10px] font-mono text-(--muted-foreground) text-center">
159
+ Click falling characters to type. Good luck spelling anything useful.
160
+ </p>
161
+ </div>