Files
sui/src/routes/+page.svelte
2025-12-11 15:20:42 -08:00

573 lines
16 KiB
Svelte

<script lang="ts">
import {
CalendarDate,
today,
getLocalTimeZone,
type TimeDuration
} from '@internationalized/date';
import Button from '$lib/Button.svelte';
import ActionSelect from '$lib/ActionSelect.svelte';
import Checkbox, { type CheckboxState } from '$lib/Checkbox.svelte';
import Combobox from '$lib/Combobox.svelte';
import DateInput from '$lib/DateInput.svelte';
import Dialog from '$lib/Dialog.svelte';
import FramelessButton from '$lib/FramelessButton.svelte';
import InputGroup from '$lib/InputGroup.svelte';
import Link from '$lib/Link.svelte';
import PinInput from '$lib/PinInput.svelte';
import RadioGroup from '$lib/RadioGroup.svelte';
import StyledRawInput from '$lib/StyledRawInput.svelte';
import TextInput from '$lib/TextInput.svelte';
import TimeInput, { formatTime } from '$lib/TimeInput.svelte';
import ToggleGroup from '$lib/ToggleGroup.svelte';
import ToggleSelect from '$lib/ToggleSelect.svelte';
import { Toolbar } from '$lib/Toolbar';
import {
ArrowUUpLeft,
ArrowUUpRight,
DotsThreeOutlineVertical,
Plus,
TextB,
TextItalic,
TextStrikethrough,
TextUnderline
} from 'phosphor-svelte';
import { createLazyComponent, type ComboboxOption, type Option } from '$lib';
import Tabs from '$lib/Tabs.svelte';
import { Time } from '@internationalized/date';
import { onMount, type Component } from 'svelte';
import ErrorBox from '$lib/ErrorBox.svelte';
import TextareaInput from '$lib/TextareaInput.svelte';
import DurationInput, { formatDuration } from '$lib/DurationInput.svelte';
// Lazy-load heavy components
let PhoneInput = createLazyComponent(() => import('$lib/PhoneInput.svelte'));
let TimezoneInput = createLazyComponent(() => import('$lib/TimezoneInput.svelte'));
// Load heavy components on mount
onMount(() => {
PhoneInput.load();
TimezoneInput.load();
});
const comboboxOptions = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3', disabled: true }
];
let lazyOptions: ComboboxOption[] = $state([]);
let dateInputValue = $state<CalendarDate | undefined>(undefined);
let checkboxValue = $state<CheckboxState>('indeterminate');
let dialogOpen = $state(false);
let scrollableDialogOpen = $state(false);
let toggleOptions: Option[] = $state([
'item one',
'item two',
{ value: 'complex', label: 'Complex item' }
]);
let timeValue = $state<Time | null>(null);
let durationValue = $state<TimeDuration | null>(null);
const toolbar = new Toolbar();
const fontGroup = toolbar.group();
const boldToggle = fontGroup.toggle();
const boldStore = boldToggle.store;
</script>
<title>sui</title>
<h1 class="mb-4 text-3xl font-bold">sui — Opinionated Svelte 5 UI toolkit</h1>
<p class="mb-4">
Welcome to the Svelte UI toolkit! This is a collection of components and utilities designed to
help you build Svelte applications quickly and efficiently.
</p>
<h2 class="mb-2 text-2xl font-semibold">Component Library</h2>
<div class="component">
<p class="title">Button, Frameless Button, & Link</p>
<div class="flex gap-4">
<Button icon={{ component: Plus }} loading={false} onclick={() => alert('Button clicked!')}
>Click Me</Button
>
<Button icon={{ component: Plus }} loading={true} onclick={() => alert('Button clicked!')}
>Loading Button</Button
>
<FramelessButton icon={{ component: Plus }}>Click Me</FramelessButton>
<Link href="https://svelte.dev">Visit Svelte</Link>
<Button onclick={() => (dialogOpen = true)}>Open Dialog</Button>
<Button onclick={() => (scrollableDialogOpen = true)}>Open Scrollable Dialog</Button>
</div>
</div>
<div class="component">
<p class="title">Action Select</p>
<div class="flex gap-2">
<ActionSelect
class="basis-1/2"
stateless
label="Stateless Action"
options={[
{ label: 'Yeet' },
{ label: 'Yote' },
{ label: 'Yote and Yeet' },
{ label: 'Disabled Action', disabled: true }
]}
/>
<ActionSelect
class="basis-1/2"
label="Stateful Action"
value={{ label: 'Initial Action', onchoose: (value) => console.log('Chosen:', value) }}
options={[
{ label: 'Action 1', onchoose: (value) => console.log('Action 1 chosen:', value) },
{ label: 'Action 2', onchoose: (value) => console.log('Action 2 chosen:', value) },
{
label: 'Disabled Action',
disabled: true,
onchoose: (value) => console.log('Disabled action chosen:', value)
}
]}
/>
<ActionSelect
class="mt-7"
options={[{ label: 'Option 1' }, { label: 'Option 2' }, { label: 'Option 3' }]}
sameWidth={false}
frameless={true}
stateless={true}
>
<DotsThreeOutlineVertical class="text-sui-text" size="1.2em" />
</ActionSelect>
</div>
</div>
<div class="component">
<p class="title">Combobox</p>
<InputGroup>
<Combobox
name="example-combobox"
label="Select an option"
placeholder="Choose..."
options={comboboxOptions}
onchange={(e) => console.log('Selected:', e.value)}
onvalidate={(e) => console.log('Validation:', e.detail)}
/>
<Combobox
loading
label="Loading stateless combobox"
stateless
placeholder="Choose..."
options={[
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' }
]}
onchange={(e) => console.log('Selected:', e.value)}
onvalidate={(e) => console.log('Validation:', e.detail)}
/>
<Combobox
icon={{
component: Plus,
props: {
size: '1.2em'
}
}}
label="Lazy combobox"
placeholder="Choose..."
options={lazyOptions}
lazy
onlazy={() => {
setTimeout(() => {
lazyOptions = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' }
];
}, 2500);
}}
/>
<Combobox
label="Compact combobox"
placeholder="Choose..."
lazy
compact
options={comboboxOptions}
/>
</InputGroup>
</div>
<div class="component">
<p class="title">Checkbox</p>
<div class="flex items-center gap-4">
<Checkbox
name="example-checkbox"
bind:value={checkboxValue}
onchange={(value) => console.log('Checkbox value:', value)}
>
Agree to terms and conditions
</Checkbox>
<span>Checkbox is {checkboxValue}</span>
<Button onclick={() => (checkboxValue = 'indeterminate')}>Set Indeterminate</Button>
</div>
<Checkbox>Example checkbox</Checkbox>
</div>
<div class="component">
<p class="title">Dateinput</p>
<div class="flex items-start gap-4">
<DateInput bind:value={dateInputValue} />
<div class="shrink-0">
<Button
icon={{ component: Plus }}
onclick={() => {
dateInputValue = today(getLocalTimeZone());
console.log('Dateinput value set to:', dateInputValue);
}}
>
Set Current Date
</Button>
</div>
<span>Selected date is {dateInputValue}</span>
</div>
</div>
<div class="component">
<p class="title">Phone Input</p>
{#if PhoneInput.loading}
<div class="h-12 animate-pulse rounded bg-gray-200">Loading...</div>
{:else if PhoneInput.error}
<div class="text-red-500">Failed to load component</div>
{:else}
<PhoneInput.component label="Phone Number" name="phone" defaultISO="CA" />
{/if}
</div>
<div class="component">
<p class="title">Pin Input</p>
<PinInput label="Please enter your 6-digit PIN" length={6} />
</div>
<div class="component">
<p class="title">Radio Group</p>
<RadioGroup
name="example-radio-group"
label="Choose an option"
options={[
'male',
'female',
'other',
{ value: 'prefer_not_to_say', label: 'Prefer not to say' }
]}
/>
</div>
<div class="component">
<p class="title">Styled Raw Input, Text Input, Disabled Input, Compact Input</p>
<InputGroup>
<StyledRawInput placeholder="Type here..." class="basis-1/2" />
<TextInput label="Write something here" placeholder="Enter text..." class="basis-1/2" />
</InputGroup>
<InputGroup>
<TextInput label="Disabled input" placeholder="You can't enter text" disabled />
<TextInput label="Compact Input" placeholder="Small input field" compact />
</InputGroup>
</div>
<div class="component">
<p class="title">Multi-line input (textarea)</p>
<TextareaInput label="Write your message here" />
</div>
<div class="component">
<p class="title">Time & Duration Input</p>
<InputGroup class="gap-8">
<TimeInput label="Regular time input" name="example-time-input" />
<TimeInput label="Compact time" compact bind:value={timeValue} />
<DurationInput
label="Duration input"
name="example-duration-input"
precision={{ min: 'hours', max: 'seconds' }}
bind:value={durationValue}
/>
<DurationInput label="Compact duration" compact />
</InputGroup>
<InputGroup>
<p>Selected time is {formatTime(timeValue, 'undefined')} ({timeValue?.toString()})</p>
<Button
onclick={() => {
timeValue = new Time(15, 0);
}}
>
Set 3:00 PM
</Button>
<p>Precise duration is {formatDuration(durationValue)}</p>
</InputGroup>
</div>
<div class="component">
<p class="title">TimezoneInput</p>
{#if TimezoneInput.loading}
<div class="h-12 animate-pulse rounded bg-gray-200">Loading...</div>
{:else if TimezoneInput.error}
<div class="text-red-500">Failed to load component</div>
{:else}
<TimezoneInput.component name="example-timezone" label="Pick your timezone" />
{/if}
</div>
<div class="component">
<p class="title">Toggle</p>
<ToggleSelect name="example-toggle-select" class="mb-4">Toggle Me!</ToggleSelect>
<ToggleGroup label="Toggle Group" options={toggleOptions} />
<Button
onclick={() => {
toggleOptions.push({ value: 'new', label: 'New Option' });
}}>Add Option</Button
>
</div>
<div class="component">
<p class="title">Toolbar</p>
<div class="my-2">
<p>Bold is enabled: {$boldStore}</p>
</div>
<div
class="border-sui-text flex w-full min-w-max items-center gap-4 border-b
bg-white px-3 py-3 text-neutral-700 shadow-xs"
>
<div class="flex items-center gap-1">
<button type="button" class="item" title="Undo" aria-label="undo">
<ArrowUUpLeft size="1.25em" />
</button>
<button type="button" class="item" title="Redo" aria-label="redo">
<ArrowUUpRight size="1.25em" />
</button>
</div>
<div class="bg-sui-text/50 w-[1px] self-stretch"></div>
<div class="flex items-center gap-1">
<button
type="button"
class="item"
title="Toggle Bold"
aria-label="bold"
{@attach boldToggle.attachment}
>
<TextB size="1.25em" />
</button>
<button
type="button"
class="item"
title="Toggle Italic"
aria-label="italic"
{@attach fontGroup.toggle().attachment}
>
<TextItalic size="1.25em" />
</button>
<button
type="button"
class="item"
title="Toggle Underline"
aria-label="underline"
{@attach fontGroup.toggle().attachment}
>
<TextUnderline size="1.25em" />
</button>
<button
type="button"
class="item"
title="Toggle Strikethrough"
aria-label="strikethrough"
{...fontGroup.toggle().props}
>
<TextStrikethrough size="1.25em" />
</button>
</div>
</div>
</div>
<div class="component">
<p class="title">Tabs</p>
<Tabs
pages={[
{
title: 'Dashboard',
content: tab1
},
{
title: 'Activity',
content: tab2
},
{
title: 'Settings',
content: tab3
}
]}
/>
{#snippet tab1()}
<h3 class="mb-4 text-2xl font-bold">Dashboard Overview</h3>
<div class="grid grid-cols-2 gap-4">
<div class="rounded bg-blue-100 p-4">
<h4 class="font-semibold">Total Users</h4>
<p class="text-3xl font-bold text-blue-600">1,247</p>
</div>
<div class="rounded bg-green-100 p-4">
<h4 class="font-semibold">Revenue</h4>
<p class="text-3xl font-bold text-green-600">$12,450</p>
</div>
</div>
{/snippet}
{#snippet tab2()}
<h3 class="mb-3 text-xl font-semibold">Recent Activity</h3>
<ul class="space-y-2">
<li class="flex items-center gap-2">
<span class="h-2 w-2 rounded-full bg-green-500"></span>
<span>User John Doe logged in</span>
</li>
<li class="flex items-center gap-2">
<span class="h-2 w-2 rounded-full bg-blue-500"></span>
<span>New order #1234 received</span>
</li>
<li class="flex items-center gap-2">
<span class="h-2 w-2 rounded-full bg-yellow-500"></span>
<span>System backup completed</span>
</li>
</ul>
{/snippet}
{#snippet tab3()}
<h3 class="mb-3 text-xl font-semibold">Settings</h3>
<form class="space-y-3">
<div class="flex items-center gap-2">
<input type="checkbox" id="notifications" checked />
<label for="notifications">Enable notifications</label>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" id="darkmode" />
<label for="darkmode">Dark mode</label>
</div>
<button type="button" class="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Save Changes
</button>
</form>
{/snippet}
</div>
<!-- Regular Dialog Demo -->
<Dialog
bind:open={dialogOpen}
title="Dialog Title"
size="sm"
controls={{
ok: {
action: (dialog) => {
dialog.close();
alert('Dialog submitted!');
}
}
}}
onopen={(dialog) => {
dialog.error('Example error message!');
dialog.loading();
setTimeout(() => {
dialog.loaded();
}, 2000);
}}
>
<p>This is a dialog content area.</p>
</Dialog>
<!-- Scrollable Dialog Demo -->
<Dialog bind:open={scrollableDialogOpen} title="Scrollable Dialog" size="sm">
<div class="space-y-4">
<p>
Ullamco nulla sunt laboris esse commodo irure id pariatur est irure eiusmod. Cupidatat Lorem
ad deserunt non culpa aliqua qui qui ut reprehenderit minim consequat amet. Qui elit ipsum
dolor enim laboris. Exercitation sint esse dolore enim irure veniam esse incididunt fugiat.
</p>
<p>
In elit tempor quis enim id fugiat cillum consectetur minim sint ex. Minim reprehenderit culpa
sunt in reprehenderit. Amet in minim in nulla officia fugiat laborum velit dolor laborum
deserunt aliqua nostrud.
</p>
<p>
Ad dolor ad nisi est fugiat anim aute amet. Fugiat excepteur proident incididunt anim sunt.
Proident quis dolor ea voluptate esse commodo voluptate quis culpa cupidatat excepteur.
</p>
<p>
Cillum ut laboris laboris ea ex ex. Aliquip magna irure eiusmod qui eiusmod. Mollit id et
incididunt sint mollit anim cillum reprehenderit exercitation labore incididunt culpa. Officia
et ad occaecat quis ipsum. Culpa quis cupidatat reprehenderit reprehenderit incididunt
excepteur quis minim. Laboris cupidatat laborum est ipsum esse sint aliqua cillum laborum est
cillum dolore cupidatat pariatur. Dolor ipsum cillum enim esse consectetur dolor sunt magna.
</p>
<p>
Eu cillum reprehenderit Lorem duis sunt. Mollit laborum tempor magna dolor ad ipsum do fugiat
nisi quis culpa tempor veniam officia. Voluptate irure labore aliqua elit officia nulla dolor.
Lorem duis ea ea commodo deserunt minim enim. Excepteur non magna cupidatat ea eiusmod dolore
elit dolor veniam cupidatat. Amet voluptate culpa ut ex consequat culpa cillum. Exercitation
ex voluptate incididunt laboris qui sint id quis in aliqua excepteur incididunt.
</p>
</div>
</Dialog>
<style lang="postcss">
@reference '$lib/styles/tailwind.css';
.component .title {
@apply mb-2 text-lg font-semibold;
}
.component {
@apply mb-6 rounded-lg border p-4;
}
.item {
@apply flex items-center;
padding: theme('spacing.1');
border-radius: theme('borderRadius.md');
&:hover {
background-color: theme('colors.sui-secondary.100');
}
&[data-state='on'] {
background-color: theme('colors.sui-secondary.200');
color: theme('colors.sui-accent.900');
}
/* &:focus {
@apply ring-accent-400 ring-2;
} */
}
.separator {
width: 1px;
background-color: theme('colors.neutral.300');
align-self: stretch;
}
</style>