responsive ui + wallet redesign + 401 fix + round cards
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
# Component Composition
|
||||
|
||||
## Contents
|
||||
|
||||
- Items always inside their Group component
|
||||
- Callouts use Alert
|
||||
- Empty states use Empty component
|
||||
- Toast notifications use svelte-sonner
|
||||
- Choosing between overlay components
|
||||
- Dialog, Sheet, and Drawer always need a Title
|
||||
- Card structure
|
||||
- Button has no isPending or isLoading prop
|
||||
- Tabs.Trigger must be inside Tabs.List
|
||||
- Avatar always needs Avatar.Fallback
|
||||
- Use Separator instead of raw hr or border divs
|
||||
- Use Skeleton for loading placeholders
|
||||
- Use Badge instead of custom styled spans
|
||||
|
||||
---
|
||||
|
||||
## Items always inside their Group component
|
||||
|
||||
Never render items directly inside the content container.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
</script>
|
||||
|
||||
<Select.Content>
|
||||
<Select.Item value="apple">Apple</Select.Item>
|
||||
<Select.Item value="banana">Banana</Select.Item>
|
||||
</Select.Content>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
</script>
|
||||
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Item value="apple">Apple</Select.Item>
|
||||
<Select.Item value="banana">Banana</Select.Item>
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
```
|
||||
|
||||
This applies to all group-based components:
|
||||
|
||||
| Item | Group |
|
||||
| ------------------------------------------------------------- | -------------------- |
|
||||
| `Select.Item`, `Select.Label` | `Select.Group` |
|
||||
| `DropdownMenu.Item`, `DropdownMenu.Label`, `DropdownMenu.Sub` | `DropdownMenu.Group` |
|
||||
| `Menubar.Item` | `Menubar.Group` |
|
||||
| `ContextMenu.Item` | `ContextMenu.Group` |
|
||||
| `Command.Item` | `Command.Group` |
|
||||
|
||||
---
|
||||
|
||||
## Callouts use Alert
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as Alert from "$lib/components/ui/alert";
|
||||
</script>
|
||||
|
||||
<Alert.Root>
|
||||
<Alert.Title>Warning</Alert.Title>
|
||||
<Alert.Description>Something needs attention.</Alert.Description>
|
||||
</Alert.Root>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Empty states use Empty component
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as Empty from "$lib/components/ui/empty";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import FolderIcon from "@lucide/svelte/icons/folder";
|
||||
</script>
|
||||
|
||||
<Empty.Root>
|
||||
<Empty.Header>
|
||||
<Empty.Media variant="icon"><FolderIcon /></Empty.Media>
|
||||
<Empty.Title>No projects yet</Empty.Title>
|
||||
<Empty.Description
|
||||
>Get started by creating a new project.</Empty.Description
|
||||
>
|
||||
</Empty.Header>
|
||||
<Empty.Content>
|
||||
<Button>Create Project</Button>
|
||||
</Empty.Content>
|
||||
</Empty.Root>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Toast notifications use svelte-sonner
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { toast } from "svelte-sonner";
|
||||
</script>
|
||||
```
|
||||
|
||||
```ts
|
||||
toast.success("Changes saved.");
|
||||
toast.error("Something went wrong.");
|
||||
toast("File deleted.", {
|
||||
action: { label: "Undo", onClick: () => undoDelete() },
|
||||
});
|
||||
```
|
||||
|
||||
Mount the `Toaster` from your UI folder once in the app layout (see [Sonner](https://shadcn-svelte.com/docs/components/sonner)).
|
||||
|
||||
---
|
||||
|
||||
## Choosing between overlay components
|
||||
|
||||
| Use case | Component |
|
||||
| ---------------------------------- | ------------- |
|
||||
| Focused task that requires input | `Dialog` |
|
||||
| Destructive action confirmation | `AlertDialog` |
|
||||
| Side panel with details or filters | `Sheet` |
|
||||
| Mobile-first bottom panel | `Drawer` |
|
||||
| Quick info on hover | `HoverCard` |
|
||||
| Small contextual content on click | `Popover` |
|
||||
|
||||
---
|
||||
|
||||
## Dialog, Sheet, and Drawer always need a Title
|
||||
|
||||
`Dialog.Title`, `Sheet.Title`, `Drawer.Title` are required for accessibility. Use `class="sr-only"` if visually hidden.
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
</script>
|
||||
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Edit Profile</Dialog.Title>
|
||||
<Dialog.Description>Update your profile.</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
...
|
||||
</Dialog.Content>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Card structure
|
||||
|
||||
Use full composition — don't dump everything into `Card.Content`:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
</script>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Team Members</Card.Title>
|
||||
<Card.Description>Manage your team.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>...</Card.Content>
|
||||
<Card.Footer>
|
||||
<Button>Invite</Button>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Button has no isPending or isLoading prop
|
||||
|
||||
Compose with `Spinner` inside `Button` + `disabled`:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Spinner } from "$lib/components/ui/spinner";
|
||||
</script>
|
||||
|
||||
<Button disabled>
|
||||
<Spinner data-icon="inline-start" />
|
||||
Saving...
|
||||
</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tabs.Trigger must be inside Tabs.List
|
||||
|
||||
Never render `Tabs.Trigger` directly inside `Tabs.Root` — always wrap in `Tabs.List`:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as Tabs from "$lib/components/ui/tabs";
|
||||
let tab = $state("account");
|
||||
</script>
|
||||
|
||||
<Tabs.Root bind:value={tab}>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="account">Account</Tabs.Trigger>
|
||||
<Tabs.Trigger value="password">Password</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="account">...</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Avatar always needs Avatar.Fallback
|
||||
|
||||
Always include `Avatar.Fallback` for when the image fails to load:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as Avatar from "$lib/components/ui/avatar";
|
||||
</script>
|
||||
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src="/avatar.png" alt="User" />
|
||||
<Avatar.Fallback>JD</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use existing components instead of custom markup
|
||||
|
||||
| Instead of | Use |
|
||||
| ---------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| `<hr>` or `<div class="border-t">` | `<Separator />` (`import { Separator } from "$lib/components/ui/separator"`) |
|
||||
| `<div class="animate-pulse">` with styled divs | `<Skeleton class="h-4 w-3/4" />` (`import { Skeleton } from "$lib/components/ui/skeleton"`) |
|
||||
| `<span class="rounded-full bg-green-100 ...">` | `<Badge variant="secondary">` (`import { Badge } from "$lib/components/ui/badge"`) |
|
||||
@@ -0,0 +1,234 @@
|
||||
# Forms & Inputs
|
||||
|
||||
## Contents
|
||||
|
||||
- Forms use Field.FieldGroup + Field.Field
|
||||
- InputGroup requires InputGroup.Input/InputGroup.Textarea
|
||||
- Buttons inside inputs use InputGroup.Root + InputGroup.Addon
|
||||
- Option sets (2–7 choices) use ToggleGroup.Root + ToggleGroup.Item
|
||||
- Field.FieldSet + Field.FieldLegend for grouping related fields
|
||||
- Field validation and disabled states
|
||||
|
||||
---
|
||||
|
||||
## Forms use Field.FieldGroup + Field.Field
|
||||
|
||||
Always use `Field.FieldGroup` + `Field.Field` — never raw `div` with `space-y-*`:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as Field from "$lib/components/ui/field";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
</script>
|
||||
|
||||
<Field.FieldGroup>
|
||||
<Field.Field>
|
||||
<Field.FieldLabel for="email">Email</Field.FieldLabel>
|
||||
<Input id="email" type="email" />
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.FieldLabel for="password">Password</Field.FieldLabel>
|
||||
<Input id="password" type="password" />
|
||||
</Field.Field>
|
||||
</Field.FieldGroup>
|
||||
```
|
||||
|
||||
Use `Field` with `orientation="horizontal"` for settings pages. Use `Field.FieldLabel` with `class="sr-only"` for visually hidden labels.
|
||||
|
||||
**Choosing form controls:**
|
||||
|
||||
- Simple text input → `Input`
|
||||
- Dropdown with predefined options → `Select`
|
||||
- Searchable dropdown → `Combobox`
|
||||
- Native HTML select (no JS) → `native-select`
|
||||
- Boolean toggle → `Switch` (for settings) or `Checkbox` (for forms)
|
||||
- Single choice from few options → `RadioGroup`
|
||||
- Toggle between 2–5 options → `ToggleGroup.Root` + `ToggleGroup.Item`
|
||||
- OTP/verification code → `InputOTP`
|
||||
- Multi-line text → `Textarea`
|
||||
|
||||
---
|
||||
|
||||
## InputGroup requires InputGroup.Input/InputGroup.Textarea
|
||||
|
||||
Never use raw `Input` or `Textarea` inside an `InputGroup.Root`.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as InputGroup from "$lib/components/ui/input-group";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
</script>
|
||||
|
||||
<InputGroup.Root>
|
||||
<Input placeholder="Search..." />
|
||||
</InputGroup.Root>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as InputGroup from "$lib/components/ui/input-group";
|
||||
</script>
|
||||
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input placeholder="Search..." />
|
||||
</InputGroup.Root>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Buttons inside inputs use InputGroup.Root + InputGroup.Addon
|
||||
|
||||
Never place a `Button` directly inside or adjacent to an `Input` with custom positioning.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import SearchIcon from "@lucide/svelte/icons/search";
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<Input placeholder="Search..." class="pr-10" />
|
||||
<Button class="absolute top-0 right-0" size="icon">
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as InputGroup from "$lib/components/ui/input-group";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import SearchIcon from "@lucide/svelte/icons/search";
|
||||
</script>
|
||||
|
||||
<InputGroup.Root>
|
||||
<InputGroup.Input placeholder="Search..." />
|
||||
<InputGroup.Addon>
|
||||
<Button size="icon">
|
||||
<SearchIcon data-icon="inline-start" />
|
||||
</Button>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Option sets (2–7 choices) use ToggleGroup.Root + ToggleGroup.Item
|
||||
|
||||
Don't manually loop `Button` components with active state.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
let selected = $state("daily");
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2">
|
||||
{#each ["daily", "weekly", "monthly"] as option (option)}
|
||||
<Button
|
||||
variant={selected === option ? "default" : "outline"}
|
||||
onclick={() => (selected = option)}
|
||||
>
|
||||
{option}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as ToggleGroup from "$lib/components/ui/toggle-group";
|
||||
let selected = $state("daily");
|
||||
</script>
|
||||
|
||||
<ToggleGroup.Root bind:value={selected} spacing={2}>
|
||||
<ToggleGroup.Item value="daily">Daily</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="weekly">Weekly</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="monthly">Monthly</ToggleGroup.Item>
|
||||
</ToggleGroup.Root>
|
||||
```
|
||||
|
||||
Combine with `Field` for labelled toggle groups:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as Field from "$lib/components/ui/field";
|
||||
import * as ToggleGroup from "$lib/components/ui/toggle-group";
|
||||
</script>
|
||||
|
||||
<Field.Field orientation="horizontal">
|
||||
<Field.FieldTitle id="theme-label">Theme</Field.FieldTitle>
|
||||
<ToggleGroup.Root aria-labelledby="theme-label" spacing={2}>
|
||||
<ToggleGroup.Item value="light">Light</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="dark">Dark</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="system">System</ToggleGroup.Item>
|
||||
</ToggleGroup.Root>
|
||||
</Field.Field>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field.FieldSet + Field.FieldLegend for grouping related fields
|
||||
|
||||
Use `Field.FieldSet` + `Field.FieldLegend` for related checkboxes, radios, or switches — not `div` with a heading:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as Field from "$lib/components/ui/field";
|
||||
import { Checkbox } from "$lib/components/ui/checkbox";
|
||||
</script>
|
||||
|
||||
<Field.FieldSet>
|
||||
<Field.FieldLegend variant="label">Preferences</Field.FieldLegend>
|
||||
<Field.FieldDescription>Select all that apply.</Field.FieldDescription>
|
||||
<Field.FieldGroup class="gap-3">
|
||||
<Field.Field orientation="horizontal">
|
||||
<Checkbox id="dark" />
|
||||
<Field.FieldLabel for="dark" class="font-normal"
|
||||
>Dark mode</Field.FieldLabel
|
||||
>
|
||||
</Field.Field>
|
||||
</Field.FieldGroup>
|
||||
</Field.FieldSet>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field validation and disabled states
|
||||
|
||||
Both attributes are needed — `data-invalid`/`data-disabled` styles the field (label, description), while `aria-invalid`/`disabled` styles the control.
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as Field from "$lib/components/ui/field";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
</script>
|
||||
|
||||
<!-- Invalid. -->
|
||||
<Field.Field data-invalid>
|
||||
<Field.FieldLabel for="email">Email</Field.FieldLabel>
|
||||
<Input id="email" aria-invalid />
|
||||
<Field.FieldDescription>Invalid email address.</Field.FieldDescription>
|
||||
</Field.Field>
|
||||
|
||||
<!-- Disabled. -->
|
||||
<Field.Field data-disabled>
|
||||
<Field.FieldLabel for="email">Email</Field.FieldLabel>
|
||||
<Input id="email" disabled />
|
||||
</Field.Field>
|
||||
```
|
||||
|
||||
Works for all controls: `Input`, `Textarea`, `Select`, `Checkbox`, `RadioGroupItem`, `Switch`, `Slider`, `NativeSelect`, `InputOTP`.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Icons
|
||||
|
||||
**Always use the project's configured `iconLibrary` for imports.** Check the `iconLibrary` field in `components.json`: `lucide` → `@lucide/svelte`, `tabler` → `@tabler/icons-svelte`, etc. Never assume `@lucide/svelte`.
|
||||
|
||||
---
|
||||
|
||||
## Icons in Button use data-icon attribute
|
||||
|
||||
Add `data-icon="inline-start"` (prefix) or `data-icon="inline-end"` (suffix) to the icon. No sizing classes on the icon.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import SearchIcon from "@lucide/svelte/icons/search";
|
||||
</script>
|
||||
|
||||
<Button>
|
||||
<SearchIcon class="mr-2 size-4" />
|
||||
Search
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import SearchIcon from "@lucide/svelte/icons/search";
|
||||
import ArrowRightIcon from "@lucide/svelte/icons/arrow-right";
|
||||
</script>
|
||||
|
||||
<Button>
|
||||
<SearchIcon data-icon="inline-start" />
|
||||
Search
|
||||
</Button>
|
||||
|
||||
<Button>
|
||||
Next
|
||||
<ArrowRightIcon data-icon="inline-end" />
|
||||
</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## No sizing classes on icons inside components
|
||||
|
||||
Components handle icon sizing via CSS. Don't add `size-4`, `w-4 h-4`, or other sizing classes to icons inside `<Button>`, `DropdownMenu.Item`, `Alert.Root`, `Sidebar.*`, or other shadcn-svelte components — unless the user explicitly asks for custom icon sizes.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import SearchIcon from "@lucide/svelte/icons/search";
|
||||
</script>
|
||||
|
||||
<Button>
|
||||
<SearchIcon class="size-4" data-icon="inline-start" />
|
||||
Search
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import SearchIcon from "@lucide/svelte/icons/search";
|
||||
</script>
|
||||
|
||||
<Button>
|
||||
<SearchIcon data-icon="inline-start" />
|
||||
Search
|
||||
</Button>
|
||||
```
|
||||
|
||||
The same applies to icons inside `DropdownMenu.Item`, sidebar items, and other menu rows — no extra sizing classes on the icon component.
|
||||
|
||||
---
|
||||
|
||||
## Pass icons as components, not string keys
|
||||
|
||||
Use a component reference, not a string key to a lookup map.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```svelte
|
||||
<!-- String key lookup — avoid -->
|
||||
<DynamicIcon name="check" />
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import type { Component } from "svelte";
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
|
||||
let { Icon }: { Icon: Component } = $props();
|
||||
</script>
|
||||
|
||||
<Icon />
|
||||
|
||||
<!-- <StatusBadge Icon={CheckIcon} /> -->
|
||||
```
|
||||
@@ -0,0 +1,195 @@
|
||||
# Styling & Customization
|
||||
|
||||
See [customization.md](../customization.md) for theming, CSS variables, and adding custom colors.
|
||||
|
||||
## Contents
|
||||
|
||||
- Semantic colors
|
||||
- Built-in variants first
|
||||
- class for layout only
|
||||
- No space-x-_ / space-y-_
|
||||
- Prefer size-_ over w-_ h-\* when equal
|
||||
- Prefer truncate shorthand
|
||||
- No manual dark: color overrides
|
||||
- Use cn() for conditional classes
|
||||
- No manual z-index on overlay components
|
||||
|
||||
---
|
||||
|
||||
## Semantic colors
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```svelte
|
||||
<div class="bg-blue-500 text-white">
|
||||
<p class="text-gray-600">Secondary text</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```svelte
|
||||
<div class="bg-primary text-primary-foreground">
|
||||
<p class="text-muted-foreground">Secondary text</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## No raw color values for status/state indicators
|
||||
|
||||
For positive, negative, or status indicators, use Badge variants, semantic tokens like `text-destructive`, or define custom CSS variables — don't reach for raw Tailwind colors.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```svelte
|
||||
<span class="text-emerald-600">+20.1%</span>
|
||||
<span class="text-green-500">Active</span>
|
||||
<span class="text-red-600">-3.2%</span>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
</script>
|
||||
|
||||
<Badge variant="secondary">+20.1%</Badge>
|
||||
<Badge>Active</Badge>
|
||||
<span class="text-destructive">-3.2%</span>
|
||||
```
|
||||
|
||||
If you need a success/positive color that doesn't exist as a semantic token, use a Badge variant or ask the user about adding a custom CSS variable to the theme (see [customization.md](../customization.md)).
|
||||
|
||||
---
|
||||
|
||||
## Built-in variants first
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
</script>
|
||||
|
||||
<Button class="border-input hover:bg-accent border bg-transparent"
|
||||
>Click me</Button
|
||||
>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
</script>
|
||||
|
||||
<Button variant="outline">Click me</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## class for layout only
|
||||
|
||||
Use `class` for layout (e.g. `max-w-md`, `mx-auto`, `mt-4`), **not** for overriding component colors or typography. To change colors, use semantic tokens, built-in variants, or CSS variables.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
</script>
|
||||
|
||||
<Card.Root class="bg-blue-100 font-bold text-blue-900">
|
||||
<Card.Content>Dashboard</Card.Content>
|
||||
</Card.Root>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
</script>
|
||||
|
||||
<Card.Root class="mx-auto max-w-md">
|
||||
<Card.Content>Dashboard</Card.Content>
|
||||
</Card.Root>
|
||||
```
|
||||
|
||||
To customize a component's appearance, prefer these approaches in order:
|
||||
|
||||
1. **Built-in variants** — `variant="outline"`, `variant="destructive"`, etc.
|
||||
2. **Semantic color tokens** — `bg-primary`, `text-muted-foreground`.
|
||||
3. **CSS variables** — define custom colors in the global CSS file (see [customization.md](../customization.md)).
|
||||
|
||||
---
|
||||
|
||||
## No space-x-_ / space-y-_
|
||||
|
||||
Use `gap-*` instead. `space-y-4` → `flex flex-col gap-4`. `space-x-2` → `flex gap-2`.
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<Input />
|
||||
<Input />
|
||||
<Button>Submit</Button>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prefer size-_ over w-_ h-\* when equal
|
||||
|
||||
`size-10` not `w-10 h-10`. Applies to icons, avatars, skeletons, etc.
|
||||
|
||||
---
|
||||
|
||||
## Prefer truncate shorthand
|
||||
|
||||
`truncate` not `overflow-hidden text-ellipsis whitespace-nowrap`.
|
||||
|
||||
---
|
||||
|
||||
## No manual dark: color overrides
|
||||
|
||||
Use semantic tokens — they handle light/dark via CSS variables. `bg-background text-foreground` not `bg-white dark:bg-gray-950`.
|
||||
|
||||
---
|
||||
|
||||
## Use cn() for conditional classes
|
||||
|
||||
Use the `cn()` utility from the project for conditional or merged class names. Don't write manual ternaries in `class` strings.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
let isActive = $state(false);
|
||||
</script>
|
||||
|
||||
<div class={`flex items-center ${isActive ? "bg-primary text-primary-foreground" : "bg-muted"}`}>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
let isActive = $state(false);
|
||||
</script>
|
||||
|
||||
<div class={cn("flex items-center", isActive ? "bg-primary text-primary-foreground" : "bg-muted")}>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## No manual z-index on overlay components
|
||||
|
||||
`Dialog`, `Sheet`, `Drawer`, `AlertDialog`, `DropdownMenu`, `Popover`, `Tooltip`, `HoverCard` handle their own stacking. Never add `z-50` or `z-[999]`.
|
||||
Reference in New Issue
Block a user