@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,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,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,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,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}
|