573 lines
16 KiB
Svelte
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>
|