@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.
Files changed (96) hide show
  1. package/README.md +49 -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,164 @@
1
+ <script lang="ts">
2
+ import { cn, chaosClasses, chaosRandom, randomGarbageText } from '../../utils/cn.js';
3
+ import type { GigoCarouselProps } from '../../types/index.js';
4
+
5
+ let {
6
+ slides = [],
7
+ autoplay = false,
8
+ interval = 3000,
9
+ chaos = false,
10
+ chaosLevel = 5,
11
+ educational = false,
12
+ a11yWarning = false,
13
+ lyingNavigation = false,
14
+ reverseDirection = false,
15
+ randomJumps = false,
16
+ infiniteFakeSlides = false,
17
+ onslidechange,
18
+ class: className,
19
+ ...restProps
20
+ }: GigoCarouselProps = $props();
21
+
22
+ let currentIndex = $state(0);
23
+ let fakeSlideCount = $state(0);
24
+ let direction = $state<'left' | 'right'>('right');
25
+
26
+ const totalSlides = $derived(slides.length + (chaos && infiniteFakeSlides ? fakeSlideCount : 0));
27
+
28
+ $effect(() => {
29
+ if (!autoplay || slides.length === 0) return;
30
+ const timer = setInterval(() => { goNext(); }, interval);
31
+ return () => clearInterval(timer);
32
+ });
33
+
34
+ $effect(() => {
35
+ if (!chaos || !randomJumps || slides.length === 0) return;
36
+ const timer = setInterval(() => {
37
+ currentIndex = chaosRandom(slides.length);
38
+ onslidechange?.(currentIndex);
39
+ }, 4000);
40
+ return () => clearInterval(timer);
41
+ });
42
+
43
+ $effect(() => {
44
+ if (!chaos || !infiniteFakeSlides) return;
45
+ const timer = setInterval(() => { fakeSlideCount++; }, 5000);
46
+ return () => clearInterval(timer);
47
+ });
48
+
49
+ function goNext() {
50
+ if (slides.length === 0) return;
51
+ direction = 'right';
52
+ if (chaos && reverseDirection) {
53
+ currentIndex = (currentIndex - 1 + slides.length) % slides.length;
54
+ } else {
55
+ currentIndex = (currentIndex + 1) % slides.length;
56
+ }
57
+ onslidechange?.(currentIndex);
58
+ }
59
+
60
+ function goPrev() {
61
+ if (slides.length === 0) return;
62
+ direction = 'left';
63
+ if (chaos && reverseDirection) {
64
+ currentIndex = (currentIndex + 1) % slides.length;
65
+ } else {
66
+ currentIndex = (currentIndex - 1 + slides.length) % slides.length;
67
+ }
68
+ onslidechange?.(currentIndex);
69
+ }
70
+
71
+ function handlePrevClick() {
72
+ if (chaos && lyingNavigation) { goNext(); } else { goPrev(); }
73
+ }
74
+
75
+ function handleNextClick() {
76
+ if (chaos && lyingNavigation) { goPrev(); } else { goNext(); }
77
+ }
78
+
79
+ let containerClass = $derived(
80
+ cn(
81
+ 'relative w-full overflow-hidden rounded-xl border border-(--border) bg-(--surface-1)',
82
+ chaos && chaosClasses(chaosLevel),
83
+ className
84
+ )
85
+ );
86
+ </script>
87
+
88
+ <div class={containerClass} {...restProps}>
89
+ <!-- Slide area -->
90
+ <div class="relative h-64 w-full overflow-hidden">
91
+ {#each slides as slide, i}
92
+ <div
93
+ class={cn(
94
+ 'absolute inset-0 flex items-center justify-center p-8 transition-all duration-500 ease-out',
95
+ i === currentIndex
96
+ ? 'opacity-100 translate-x-0 scale-100'
97
+ : i < currentIndex || (currentIndex === 0 && i === slides.length - 1 && direction === 'left')
98
+ ? 'opacity-0 -translate-x-full scale-95 pointer-events-none'
99
+ : 'opacity-0 translate-x-full scale-95 pointer-events-none'
100
+ )}
101
+ >
102
+ {#if slide.image}
103
+ <img src={slide.image} alt={slide.content} class="max-h-full max-w-full object-contain rounded-lg" />
104
+ {:else}
105
+ <p class="text-lg text-(--foreground) text-center leading-relaxed">{slide.content}</p>
106
+ {/if}
107
+ </div>
108
+ {/each}
109
+
110
+ {#if chaos && infiniteFakeSlides && fakeSlideCount > 0}
111
+ <div class="absolute bottom-3 right-3 text-xs text-gigo-magenta font-mono animate-gigo-entrance">
112
+ +{fakeSlideCount} totally real slide(s)
113
+ </div>
114
+ {/if}
115
+
116
+ <!-- Gradient edges -->
117
+ <div class="absolute inset-y-0 left-0 w-12 bg-linear-to-r from-(--surface-1) to-transparent pointer-events-none"></div>
118
+ <div class="absolute inset-y-0 right-0 w-12 bg-linear-to-l from-(--surface-1) to-transparent pointer-events-none"></div>
119
+ </div>
120
+
121
+ <!-- Navigation -->
122
+ <div class="flex items-center justify-between border-t border-(--border) p-3">
123
+ <button
124
+ class="group/nav flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium text-(--muted-foreground) hover:text-(--foreground) hover:bg-(--surface-2) transition-all duration-200"
125
+ onclick={handlePrevClick}
126
+ aria-label="Previous slide"
127
+ >
128
+ <svg class="h-4 w-4 transition-transform duration-200 group-hover/nav:-translate-x-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
129
+ Prev
130
+ </button>
131
+
132
+ <div class="flex gap-2 items-center">
133
+ {#each slides as _, i}
134
+ <button
135
+ class="relative h-2 rounded-full transition-all duration-300"
136
+ style:width={i === currentIndex ? '24px' : '8px'}
137
+ style:background={i === currentIndex ? 'linear-gradient(90deg, #e040fb, #18ffff)' : 'var(--surface-3)'}
138
+ onclick={() => {
139
+ currentIndex = chaos && lyingNavigation
140
+ ? (slides.length - 1 - i) % slides.length
141
+ : i;
142
+ onslidechange?.(currentIndex);
143
+ }}
144
+ aria-label="Go to slide {i + 1}"
145
+ ></button>
146
+ {/each}
147
+ </div>
148
+
149
+ <button
150
+ class="group/nav flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium text-(--muted-foreground) hover:text-(--foreground) hover:bg-(--surface-2) transition-all duration-200"
151
+ onclick={handleNextClick}
152
+ aria-label="Next slide"
153
+ >
154
+ Next
155
+ <svg class="h-4 w-4 transition-transform duration-200 group-hover/nav:translate-x-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
156
+ </button>
157
+ </div>
158
+
159
+ {#if educational && chaos}
160
+ <p class="border-t border-(--border) px-4 py-2.5 text-xs text-(--muted-foreground) italic font-mono">
161
+ chaos: {[lyingNavigation && 'lying', reverseDirection && 'reversed', randomJumps && 'jumping', infiniteFakeSlides && 'spawning'].filter(Boolean).join(' + ')}
162
+ </p>
163
+ {/if}
164
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { GigoCarouselProps } from '../../types/index.js';
2
+ declare const Carousel: import("svelte").Component<GigoCarouselProps, {}, "">;
3
+ type Carousel = ReturnType<typeof Carousel>;
4
+ export default Carousel;
@@ -0,0 +1,178 @@
1
+ <script lang="ts">
2
+ import { cn, chaosClasses, chaosRandom, chaosPickOne, randomGarbageText } from '../../utils/cn.js';
3
+ import type { GigoFormProps, FormField } from '../../types/index.js';
4
+
5
+ let {
6
+ fields = [],
7
+ submitLabel = 'Submit',
8
+ chaos = false,
9
+ chaosLevel = 5,
10
+ educational = false,
11
+ a11yWarning = false,
12
+ randomReset = false,
13
+ fakeValidation = false,
14
+ alwaysFails = false,
15
+ shuffleFields = false,
16
+ onsubmit,
17
+ class: className,
18
+ ...restProps
19
+ }: GigoFormProps = $props();
20
+
21
+ let formData = $state<Record<string, string>>({});
22
+ let displayFields = $state<FormField[]>([]);
23
+ let errors = $state<Record<string, string>>({});
24
+ let submitMessage = $state('');
25
+ let isSubmitting = $state(false);
26
+
27
+ // Initialize form data
28
+ $effect(() => {
29
+ const initial: Record<string, string> = {};
30
+ for (const field of fields) {
31
+ initial[field.id] = formData[field.id] ?? '';
32
+ }
33
+ formData = initial;
34
+ displayFields = [...fields];
35
+ });
36
+
37
+ // Random reset
38
+ $effect(() => {
39
+ if (!chaos || !randomReset) return;
40
+ const timer = setInterval(() => {
41
+ const keys = Object.keys(formData);
42
+ if (keys.length > 0) {
43
+ const key = chaosPickOne(keys);
44
+ formData[key] = '';
45
+ }
46
+ }, 4000);
47
+ return () => clearInterval(timer);
48
+ });
49
+
50
+ // Shuffle fields
51
+ $effect(() => {
52
+ if (!chaos || !shuffleFields) return;
53
+ const timer = setInterval(() => {
54
+ displayFields = [...displayFields].sort(() => Math.random() - 0.5);
55
+ }, 5000);
56
+ return () => clearInterval(timer);
57
+ });
58
+
59
+ const FAKE_ERRORS = [
60
+ 'Field is too honest',
61
+ 'Expected a lie here',
62
+ 'Must contain at least one regret',
63
+ 'This field has given up',
64
+ 'Exceeds maximum hope',
65
+ 'Not enough chaos',
66
+ 'Please try harder'
67
+ ];
68
+
69
+ function validate(): boolean {
70
+ errors = {};
71
+
72
+ if (chaos && fakeValidation) {
73
+ for (const field of fields) {
74
+ if (Math.random() > 0.5) {
75
+ errors[field.id] = chaosPickOne(FAKE_ERRORS);
76
+ }
77
+ }
78
+ return Object.keys(errors).length === 0;
79
+ }
80
+
81
+ // Real validation for non-chaos mode
82
+ for (const field of fields) {
83
+ if (field.required && !formData[field.id]?.trim()) {
84
+ errors[field.id] = `${field.label} is required`;
85
+ }
86
+ }
87
+ return Object.keys(errors).length === 0;
88
+ }
89
+
90
+ async function handleSubmit(e: SubmitEvent) {
91
+ e.preventDefault();
92
+ isSubmitting = true;
93
+ submitMessage = '';
94
+
95
+ if (chaos && alwaysFails) {
96
+ await new Promise((r) => setTimeout(r, 800));
97
+ submitMessage = '💀 Submission failed (as designed)';
98
+ isSubmitting = false;
99
+ return;
100
+ }
101
+
102
+ if (!validate()) {
103
+ isSubmitting = false;
104
+ return;
105
+ }
106
+
107
+ await new Promise((r) => setTimeout(r, 500));
108
+ onsubmit?.({ ...formData });
109
+ submitMessage = chaos ? randomGarbageText() : 'Submitted successfully!';
110
+ isSubmitting = false;
111
+ }
112
+
113
+ let formClass = $derived(
114
+ cn(
115
+ 'space-y-5 rounded-xl border border-(--border) bg-(--surface-1) p-6',
116
+ chaos && chaosClasses(chaosLevel),
117
+ className
118
+ )
119
+ );
120
+ </script>
121
+
122
+ <form class={formClass} onsubmit={handleSubmit} {...restProps}>
123
+ {#each displayFields as field, idx (field.id)}
124
+ <div
125
+ class="space-y-2 animate-gigo-entrance"
126
+ style:animation-delay="{idx * 0.05}s"
127
+ >
128
+ <label for={field.id} class="text-sm font-medium text-(--foreground)">
129
+ {field.label}
130
+ {#if field.required}
131
+ <span class="text-gigo-magenta">*</span>
132
+ {/if}
133
+ </label>
134
+ <div class="relative group">
135
+ <input
136
+ id={field.id}
137
+ type={field.type}
138
+ placeholder={field.placeholder ?? ''}
139
+ value={formData[field.id] ?? ''}
140
+ oninput={(e) => {
141
+ formData[field.id] = (e.target as HTMLInputElement).value;
142
+ }}
143
+ class={cn(
144
+ 'flex h-10 w-full rounded-lg border border-transparent bg-(--secondary) px-4 py-2 text-sm text-(--foreground) transition-all duration-300 ease-out placeholder:text-(--muted-foreground) focus:outline-none focus:border-(--primary) focus:shadow-(--glow-primary)',
145
+ errors[field.id] ? 'border-gigo-red shadow-(--glow-destructive)' : ''
146
+ )}
147
+ />
148
+ <div class="absolute bottom-0 left-1/2 h-0.5 w-0 bg-linear-to-r from-gigo-magenta via-gigo-cyan to-gigo-lime transition-all duration-300 ease-out rounded-full group-focus-within:w-full group-focus-within:-ml-[50%]"></div>
149
+ </div>
150
+ {#if errors[field.id]}
151
+ <p class="text-xs text-gigo-red font-mono animate-gigo-entrance">{errors[field.id]}</p>
152
+ {/if}
153
+ </div>
154
+ {/each}
155
+
156
+ <button
157
+ type="submit"
158
+ disabled={isSubmitting}
159
+ class="group/submit relative inline-flex h-10 w-full items-center justify-center overflow-hidden rounded-lg bg-(--primary) px-4 text-sm font-medium text-(--primary-foreground) transition-all duration-200 hover:scale-[1.01] hover:shadow-(--glow-primary) active:scale-[0.98] disabled:opacity-40 disabled:pointer-events-none"
160
+ >
161
+ {#if isSubmitting}
162
+ <svg class="mr-2 h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
163
+ Submitting...
164
+ {:else}
165
+ {submitLabel}
166
+ {/if}
167
+ </button>
168
+
169
+ {#if submitMessage}
170
+ <p class="text-center text-sm animate-gigo-entrance {chaos && alwaysFails ? 'text-gigo-red font-mono' : 'text-gigo-lime'}">{submitMessage}</p>
171
+ {/if}
172
+
173
+ {#if educational && chaos}
174
+ <p class="text-xs text-(--muted-foreground) italic font-mono">
175
+ chaos: {[randomReset && 'resetting', fakeValidation && 'lying', alwaysFails && 'failing', shuffleFields && 'shuffling'].filter(Boolean).join(' + ')}
176
+ </p>
177
+ {/if}
178
+ </form>
@@ -0,0 +1,4 @@
1
+ import type { GigoFormProps } from '../../types/index.js';
2
+ declare const Form: import("svelte").Component<GigoFormProps, {}, "">;
3
+ type Form = ReturnType<typeof Form>;
4
+ export default Form;
@@ -0,0 +1,135 @@
1
+ <script lang="ts">
2
+ import { cn, chaosClasses, randomGarbageText, chaosRandom, chaosPickOne } from '../../utils/cn.js';
3
+ import type { GigoInputProps, InputType } from '../../types/index.js';
4
+
5
+ let {
6
+ type = 'text',
7
+ value = $bindable(''),
8
+ placeholder = '',
9
+ disabled = false,
10
+ required = false,
11
+ name = '',
12
+ chaos = false,
13
+ chaosLevel = 5,
14
+ educational = false,
15
+ a11yWarning = false,
16
+ randomDelete = false,
17
+ escapeOnFocus = false,
18
+ fakeValidation = false,
19
+ randomizeType = false,
20
+ oninput,
21
+ onchange,
22
+ class: className,
23
+ ...restProps
24
+ }: GigoInputProps = $props();
25
+
26
+ let offsetX = $state(0);
27
+ let offsetY = $state(0);
28
+ let fakeError = $state('');
29
+ let currentType = $state<InputType>('text');
30
+ let isFocused = $state(false);
31
+
32
+ $effect(() => { currentType = type; });
33
+
34
+ const FAKE_ERRORS = [
35
+ 'Invalid soul detected',
36
+ 'Too much ambition in this field',
37
+ 'Password must contain a haiku',
38
+ 'Email not sad enough',
39
+ 'Enter a valid existential crisis',
40
+ 'Field must be left blank to continue',
41
+ 'Input exceeds hope threshold'
42
+ ];
43
+
44
+ const INPUT_TYPES: InputType[] = ['text', 'email', 'password', 'number', 'tel', 'url', 'search'];
45
+
46
+ $effect(() => {
47
+ if (!chaos || !randomDelete || !value) return;
48
+ const interval = setInterval(() => {
49
+ if (value.length > 0 && Math.random() > 0.5) {
50
+ const pos = chaosRandom(value.length);
51
+ value = value.slice(0, pos) + value.slice(pos + 1);
52
+ }
53
+ }, 2000);
54
+ return () => clearInterval(interval);
55
+ });
56
+
57
+ $effect(() => {
58
+ if (!chaos || !randomizeType) return;
59
+ const interval = setInterval(() => {
60
+ currentType = chaosPickOne(INPUT_TYPES);
61
+ }, 3000);
62
+ return () => clearInterval(interval);
63
+ });
64
+
65
+ function handleFocus() {
66
+ isFocused = true;
67
+ if (chaos && escapeOnFocus) {
68
+ offsetX = (Math.random() - 0.5) * 300;
69
+ offsetY = (Math.random() - 0.5) * 150;
70
+ }
71
+ if (chaos && fakeValidation) {
72
+ fakeError = chaosPickOne(FAKE_ERRORS);
73
+ }
74
+ }
75
+
76
+ function handleBlur() {
77
+ isFocused = false;
78
+ if (chaos && fakeValidation && Math.random() > 0.4) {
79
+ fakeError = chaosPickOne(FAKE_ERRORS);
80
+ }
81
+ }
82
+
83
+ let computedClass = $derived(
84
+ cn(
85
+ 'peer flex h-10 w-full rounded-lg bg-(--secondary) px-4 py-2 text-sm text-(--foreground) transition-all duration-300 ease-out',
86
+ 'placeholder:text-(--muted-foreground)',
87
+ 'border border-transparent',
88
+ 'focus:outline-none focus:border-(--primary) focus:shadow-(--glow-primary)',
89
+ 'disabled:cursor-not-allowed disabled:opacity-40',
90
+ fakeError && chaos ? 'border-gigo-red shadow-(--glow-destructive)' : '',
91
+ chaos && chaosClasses(chaosLevel),
92
+ className
93
+ )
94
+ );
95
+ </script>
96
+
97
+ <div
98
+ class="relative group"
99
+ style:transform="translate({offsetX}px, {offsetY}px)"
100
+ style:transition="transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)"
101
+ >
102
+ <input
103
+ type={chaos && randomizeType ? currentType : type}
104
+ class={computedClass}
105
+ bind:value
106
+ {placeholder}
107
+ {disabled}
108
+ {required}
109
+ {name}
110
+ onfocus={handleFocus}
111
+ onblur={handleBlur}
112
+ {oninput}
113
+ {onchange}
114
+ {...restProps}
115
+ />
116
+
117
+ <!-- Animated focus underline -->
118
+ <div
119
+ class="absolute bottom-0 left-1/2 h-0.5 bg-linear-to-r from-gigo-magenta via-gigo-cyan to-gigo-lime transition-all duration-300 ease-out rounded-full"
120
+ style:width={isFocused ? '100%' : '0%'}
121
+ style:margin-left={isFocused ? '-50%' : '0'}
122
+ ></div>
123
+
124
+ {#if fakeError && chaos}
125
+ <p class="mt-1.5 text-xs text-gigo-red font-mono animate-gigo-entrance" role="alert">
126
+ {fakeError}
127
+ </p>
128
+ {/if}
129
+
130
+ {#if educational && chaos}
131
+ <p class="mt-1.5 text-xs text-(--muted-foreground) italic font-mono">
132
+ chaos: {[randomDelete && 'deleting', escapeOnFocus && 'escaping', fakeValidation && 'lying', randomizeType && 'morphing'].filter(Boolean).join(' + ')}
133
+ </p>
134
+ {/if}
135
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { GigoInputProps } from '../../types/index.js';
2
+ declare const Input: import("svelte").Component<GigoInputProps, {}, "value">;
3
+ type Input = ReturnType<typeof Input>;
4
+ export default Input;
@@ -0,0 +1,173 @@
1
+ <script lang="ts">
2
+ import { cn, chaosClasses, chaosRandom } from '../../utils/cn.js';
3
+ import type { GigoModalProps } from '../../types/index.js';
4
+
5
+ let {
6
+ open = $bindable(false),
7
+ title = 'Modal',
8
+ chaos = false,
9
+ chaosLevel = 5,
10
+ educational = false,
11
+ a11yWarning = false,
12
+ resistClose = false,
13
+ escapeModal = false,
14
+ fakeCloseButtons = false,
15
+ spawnMoreModals = false,
16
+ onclose,
17
+ children,
18
+ footer,
19
+ class: className,
20
+ ...restProps
21
+ }: GigoModalProps = $props();
22
+
23
+ let closeAttempts = $state(0);
24
+ let modalOffsetX = $state(0);
25
+ let modalOffsetY = $state(0);
26
+ let spawnedModals = $state(0);
27
+ let showSpawned = $state(false);
28
+
29
+ function handleClose() {
30
+ if (chaos && resistClose) {
31
+ closeAttempts++;
32
+ if (closeAttempts < 3) return;
33
+ closeAttempts = 0;
34
+ }
35
+
36
+ if (chaos && spawnMoreModals && spawnedModals < 3) {
37
+ spawnedModals++;
38
+ showSpawned = true;
39
+ return;
40
+ }
41
+
42
+ open = false;
43
+ spawnedModals = 0;
44
+ showSpawned = false;
45
+ onclose?.();
46
+ }
47
+
48
+ function handleFakeClose() {
49
+ if (chaos && escapeModal) {
50
+ modalOffsetX = (Math.random() - 0.5) * 200;
51
+ modalOffsetY = (Math.random() - 0.5) * 200;
52
+ }
53
+ }
54
+
55
+ function handleBackdropClick() {
56
+ handleClose();
57
+ }
58
+
59
+ function handleKeydown(e: KeyboardEvent) {
60
+ if (e.key === 'Escape') {
61
+ if (chaos && escapeModal) {
62
+ modalOffsetX = (Math.random() - 0.5) * 300;
63
+ modalOffsetY = (Math.random() - 0.5) * 300;
64
+ return;
65
+ }
66
+ handleClose();
67
+ }
68
+ }
69
+
70
+ let overlayClass = $derived(
71
+ cn(
72
+ 'fixed inset-0 z-50 flex items-center justify-center transition-all duration-300',
73
+ !open && 'pointer-events-none opacity-0'
74
+ )
75
+ );
76
+
77
+ let panelClass = $derived(
78
+ cn(
79
+ 'relative w-full max-w-lg rounded-2xl border border-(--surface-border-hover) p-6 transition-all duration-300 ease-out',
80
+ 'bg-(--surface-1)/80 backdrop-blur-xl',
81
+ 'shadow-[0_0_40px_rgba(224,64,251,0.08),0_20px_60px_rgba(0,0,0,0.5)]',
82
+ chaos && chaosClasses(chaosLevel),
83
+ className
84
+ )
85
+ );
86
+ </script>
87
+
88
+ <svelte:window onkeydown={handleKeydown} />
89
+
90
+ {#if open}
91
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
92
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
93
+ <div class={overlayClass} onclick={handleBackdropClick}>
94
+ <!-- Backdrop with blur -->
95
+ <div class="absolute inset-0 bg-black/70 backdrop-blur-sm"></div>
96
+
97
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
98
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
99
+ <div
100
+ class={panelClass}
101
+ style:transform="translate({modalOffsetX}px, {modalOffsetY}px) scale(1)"
102
+ style:transition="transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)"
103
+ style:animation="gigo-entrance 0.3s ease-out"
104
+ role="dialog"
105
+ aria-modal="true"
106
+ aria-label={title}
107
+ onclick={(e) => e.stopPropagation()}
108
+ {...restProps}
109
+ >
110
+ <!-- Top glow line -->
111
+ <div class="absolute top-0 left-[10%] right-[10%] h-px bg-linear-to-r from-transparent via-gigo-magenta/50 to-transparent"></div>
112
+
113
+ <div class="flex items-center justify-between mb-4">
114
+ <h2 class="text-lg font-semibold text-(--foreground)">{title}</h2>
115
+ <button
116
+ class="group/close rounded-lg p-1.5 text-(--muted-foreground) hover:text-(--foreground) hover:bg-(--surface-2) transition-all duration-200"
117
+ onclick={handleClose}
118
+ aria-label="Close"
119
+ >
120
+ <svg class="h-4 w-4 transition-transform duration-200 group-hover/close:rotate-90" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
121
+ <path d="M18 6L6 18M6 6l12 12" />
122
+ </svg>
123
+ </button>
124
+ </div>
125
+
126
+ <div class="text-(--foreground)">
127
+ {#if children}
128
+ {@render children()}
129
+ {/if}
130
+ </div>
131
+
132
+ {#if footer}
133
+ <div class="mt-6 flex justify-end gap-3">
134
+ {@render footer()}
135
+ </div>
136
+ {/if}
137
+
138
+ {#if chaos && fakeCloseButtons}
139
+ <div class="mt-4 flex gap-2">
140
+ {#each Array(3) as _, i}
141
+ <button
142
+ class="rounded-lg bg-(--surface-2) px-3 py-1.5 text-sm text-(--foreground) hover:bg-(--surface-3) transition-colors duration-200"
143
+ onclick={handleFakeClose}
144
+ >
145
+ {['Close', 'OK', 'Dismiss', 'Cancel', 'Done'][i % 5]}
146
+ </button>
147
+ {/each}
148
+ </div>
149
+ {/if}
150
+
151
+ {#if educational && chaos && resistClose && closeAttempts > 0}
152
+ <p class="mt-3 text-xs text-gigo-magenta font-mono animate-gigo-entrance">
153
+ Close attempt {closeAttempts}/3 — this modal resists closure!
154
+ </p>
155
+ {/if}
156
+ </div>
157
+ </div>
158
+
159
+ {#if showSpawned && chaos}
160
+ {#each Array(spawnedModals) as _, i}
161
+ <div
162
+ class="fixed rounded-2xl border border-(--surface-border-hover) bg-(--surface-1)/90 backdrop-blur-xl p-5 shadow-[0_0_30px_rgba(224,64,251,0.1)]"
163
+ style:z-index={60 + i}
164
+ style:top="{20 + i * 30}%"
165
+ style:left="{20 + i * 15}%"
166
+ style:animation="gigo-entrance 0.3s ease-out {i * 0.1}s both"
167
+ >
168
+ <p class="text-sm font-semibold text-(--foreground)">Surprise Modal #{i + 1}</p>
169
+ <p class="text-xs text-(--muted-foreground) mt-1">You wanted to close? Here's another!</p>
170
+ </div>
171
+ {/each}
172
+ {/if}
173
+ {/if}
@@ -0,0 +1,4 @@
1
+ import type { GigoModalProps } from '../../types/index.js';
2
+ declare const Modal: import("svelte").Component<GigoModalProps, {}, "open">;
3
+ type Modal = ReturnType<typeof Modal>;
4
+ export default Modal;