feat: add design system components

This commit is contained in:
2026-08-22 14:02:06 +05:00
parent 3620c5fc30
commit 15e2e010ec
9 changed files with 308 additions and 0 deletions
+13
View File
@@ -160,6 +160,19 @@ button:disabled {
border-radius: var(--radius-m); border-radius: var(--radius-m);
} }
.control {
padding: var(--space-1) var(--space-2);
border: 1px solid var(--border);
border-radius: var(--radius-s);
background: var(--surface);
max-width: var(--control-max-width);
}
input.control[type='number'] {
font-family: var(--font-mono);
width: 100%;
}
.error-banner { .error-banner {
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
border: 1px solid var(--danger); border: 1px solid var(--danger);
+77
View File
@@ -0,0 +1,77 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
variant?: 'primary' | 'secondary';
type?: 'button' | 'submit';
disabled?: boolean;
busy?: boolean;
busyText?: string;
onclick?: () => void;
children: Snippet;
}
let {
variant = 'primary',
type = 'button',
disabled = false,
busy = false,
busyText = '',
onclick,
children
}: Props = $props();
</script>
<button
{type}
class={variant}
aria-busy={busy}
disabled={disabled || busy}
onclick={onclick}
>
{#if busy && busyText}
{busyText}
{:else}
{@render children()}
{/if}
</button>
<style>
button {
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-s);
border: 1px solid transparent;
font-weight: 600;
font-size: var(--text-m);
cursor: pointer;
transition:
background var(--transition-fast),
border-color var(--transition-fast),
color var(--transition-fast);
}
button.primary {
background: var(--accent);
color: var(--accent-contrast);
}
button.primary:hover:not(:disabled) {
background: var(--accent-hover);
}
button.secondary {
background: var(--surface);
color: var(--text);
border-color: var(--border);
}
button.secondary:hover:not(:disabled) {
border-color: var(--accent);
color: var(--accent);
}
button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
</style>
@@ -0,0 +1,42 @@
<script lang="ts">
interface Props {
id: string;
label: string;
checked?: boolean;
hint?: string;
}
let { id, label, checked = $bindable(false), hint }: Props = $props();
</script>
<div class="checkbox">
<input id={id} type="checkbox" bind:checked />
<label for={id}>{label}</label>
</div>
{#if hint}
<p class="hint text-caption text-muted">{hint}</p>
{/if}
<style>
.checkbox {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-3);
}
input[type='checkbox'] {
width: 1rem;
height: 1rem;
cursor: pointer;
}
label {
font-size: var(--text-m);
cursor: pointer;
}
.hint {
margin: calc(-1 * var(--space-3)) 0 var(--space-3);
}
</style>
@@ -0,0 +1,25 @@
<script lang="ts">
import Field from './Field.svelte';
interface Props {
id: string;
label: string;
value?: string;
hint?: string;
}
let { id, label, value = $bindable('#000000'), hint }: Props = $props();
</script>
<Field {id} {label} {hint}>
<input id={id} class="control swatch" type="color" bind:value />
</Field>
<style>
.swatch {
height: var(--control-height);
width: 3.5rem;
padding: 2px;
cursor: pointer;
}
</style>
@@ -0,0 +1,38 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
title: string;
hint?: string;
children?: Snippet;
}
let { title, hint, children }: Props = $props();
</script>
<div class="empty panel">
<p class="title">{title}</p>
{#if hint}
<p class="text-caption text-muted">{hint}</p>
{/if}
{#if children}
{@render children()}
{/if}
</div>
<style>
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-1);
min-height: 8rem;
padding: var(--space-4);
text-align: center;
}
.title {
font-weight: 600;
}
</style>
+29
View File
@@ -0,0 +1,29 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
id: string;
label: string;
hint?: string;
children: Snippet;
}
let { id, label, hint, children }: Props = $props();
</script>
<div class="field">
<label class="text-caption text-muted" for={id}>{label}</label>
{@render children()}
{#if hint}
<p class="hint text-caption text-muted">{hint}</p>
{/if}
</div>
<style>
.field {
display: flex;
flex-direction: column;
gap: var(--space-1);
margin-bottom: var(--space-3);
}
</style>
@@ -0,0 +1,21 @@
<script lang="ts">
import Field from './Field.svelte';
interface Props {
id: string;
label: string;
value?: string;
options: { value: string; label: string }[];
hint?: string;
}
let { id, label, value = $bindable(''), options, hint }: Props = $props();
</script>
<Field {id} {label} {hint}>
<select id={id} class="control" bind:value>
{#each options as option (option.value)}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</Field>
@@ -0,0 +1,43 @@
<script lang="ts">
import Field from './Field.svelte';
interface Props {
id: string;
label: string;
value?: number;
min?: number;
max?: number;
step?: number;
hint?: string;
}
let { id, label, value = $bindable(0), min, max, step, hint }: Props = $props();
</script>
<Field {id} {label} {hint}>
<div class="row">
<input id={id} type="range" min={min} max={max} step={step} bind:value />
<output>{value}</output>
</div>
</Field>
<style>
.row {
display: flex;
align-items: center;
gap: var(--space-2);
max-width: var(--control-max-width);
}
input[type='range'] {
flex: 1;
accent-color: var(--accent);
}
output {
min-width: 3ch;
text-align: right;
font-family: var(--font-mono);
font-size: var(--text-s);
}
</style>
@@ -0,0 +1,20 @@
<script lang="ts">
import Field from './Field.svelte';
interface Props {
id: string;
label: string;
value?: string | number;
type?: 'number' | 'text';
min?: number;
max?: number;
step?: number;
hint?: string;
}
let { id, label, value = $bindable(''), type = 'text', min, max, step, hint }: Props = $props();
</script>
<Field {id} {label} {hint}>
<input class="control" {id} {type} min={min} max={max} step={step} bind:value />
</Field>