Free SKILL.md scraped from GitHub. Clone the repo or copy the file directly into your Claude Code skills directory.
npx versuz@latest install cdn37421-typeless-custom-agents-skills-epicenter-uigit clone https://github.com/cdn37421/typeless-custom.gitcp typeless-custom/SKILL.MD ~/.claude/skills/cdn37421-typeless-custom-agents-skills-epicenter-ui/SKILL.md---
name: epicenter-ui
description: Epicenter UI component selection and composition patterns for Svelte apps using packages/ui. Use when choosing or reviewing @epicenter/ui components, loading states, empty states, skeletons, spinners, command empty states, action pending UI, table/list no-row states, button tooltips, wrapper minimization, or replacing ad hoc UI such as Loading... text, custom loading dots, raw animate-pulse placeholders, or one-off centered status markup.
metadata:
author: epicenter
version: '1.0'
---
# Epicenter UI
Use the local `@epicenter/ui` package before writing one-off UI. Most state surfaces already have a component with spacing, color, accessibility, and composition handled.
Related skills:
- Use `svelte` for branch mechanics: `{#if}`, `{#await}`, derived state, query state, and component lifecycle.
- Use `styling` for Tailwind details, wrapper element decisions, scroll traps, and disabled-state styling.
- Use this skill for local component choice and composition.
## Reference Repositories
- [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte): component structure and Svelte composition patterns
- [shadcn-svelte-extras](https://github.com/ieedan/shadcn-svelte-extras): chat components and extra UI patterns
- [TanStack Table](https://github.com/TanStack/table): headless table state, not table empty UI
- [Autumn](https://github.com/useautumn/autumn): billing and usage UI contexts where pending, progress, and empty states matter
## Loading State Choice
Pick the component by what the user is waiting for:
- Full surface pending: use `Empty.Root` with `Spinner` when the whole surface is replaced.
- Known progress: use `Progress`, not a spinner.
- Content shape is known: use `Skeleton`, not raw `animate-pulse` divs.
- Button action pending: disable the `Button` and put a small `Spinner` inside it.
- Chat assistant typing: use `Chat.BubbleMessage typing`. `LoadingDots` is only for chat bubbles.
- Command search with no matches: use `Command.Empty`.
- No rows, no files, no results, or failed surface: use `Empty.*`.
Do not show plain text such as `Loading...` by itself. Pair status text with an affordance, usually `Spinner`, and choose text that says what is happening: `Checking session`, `Loading tabs`, `Downloading model`.
## Composition And Wrappers
Collapse wrapper elements whenever a component can own the layout directly. `Empty.Root` already centers content, lays out a column, sets text alignment, and accepts `class`, so full-surface pending, empty, and error states usually do not need an outer `div`.
```svelte
<!-- Prefer this -->
<Empty.Root class="h-dvh flex-none border-0" aria-live="polite">
<Empty.Media>
<Spinner class="size-5 text-muted-foreground" />
</Empty.Media>
<Empty.Title>Checking session</Empty.Title>
</Empty.Root>
<!-- Avoid this -->
<div class="flex h-dvh items-center justify-center">
<Empty.Root class="border-0">
<Empty.Title>Checking session</Empty.Title>
</Empty.Root>
</div>
```
Add a wrapper only when it owns a real layout boundary that the component should not own: scroll containment, pane sizing, table cell structure, sticky headers, or sibling spacing.
## Empty States
Use the `Empty.*` compound component for an absent or failed surface:
```svelte
<script lang="ts">
import * as Empty from '@epicenter/ui/empty';
import FolderOpenIcon from '@lucide/svelte/icons/folder-open';
</script>
<Empty.Root class="py-8">
<Empty.Media variant="icon">
<FolderOpenIcon class="size-5" />
</Empty.Media>
<Empty.Title>No recordings yet</Empty.Title>
<Empty.Description>Record audio to see transcripts here.</Empty.Description>
</Empty.Root>
```
Use `Empty.Content` when the state has an action button. Keep the title short and let the description explain the next step.
## Pending Surfaces
When pending replaces a whole pane or page, use the same structure you would use for the error branch. This keeps layout and alignment stable:
```svelte
<script lang="ts">
import * as Empty from '@epicenter/ui/empty';
import { Spinner } from '@epicenter/ui/spinner';
</script>
<Empty.Root class="h-dvh border-0" aria-live="polite">
<Empty.Media>
<Spinner class="size-5 text-muted-foreground" />
</Empty.Media>
<Empty.Title>Checking session</Empty.Title>
</Empty.Root>
```
For inline pending inside an existing surface, keep the wrapper small and only use it when no existing element can take the layout classes:
```svelte
<div class="flex h-full items-center justify-center">
<Spinner class="size-5 text-muted-foreground" />
</div>
```
## Button Pending
```svelte
<script lang="ts">
import { Button } from '@epicenter/ui/button';
import { Spinner } from '@epicenter/ui/spinner';
</script>
<Button onclick={save} disabled={isSaving}>
{#if isSaving}
<Spinner class="size-3.5" />
<span>Saving</span>
{:else}
Save
{/if}
</Button>
```
Keep the label when the action needs context. Icon-only pending is fine for compact row actions where the button tooltip or surrounding label already names the action.
## Button Tooltips
`Button` has a built-in `tooltip` prop. Use it before hand-wrapping a button with `Tooltip.Root`, `Tooltip.Trigger`, and `Tooltip.Content`.
```svelte
<Button
size="icon"
variant="ghost"
tooltip="Delete recording"
onclick={deleteRecording}
>
<TrashIcon />
</Button>
```
The built-in tooltip expects a parent `Tooltip.Provider` somewhere above the button. Hand-roll tooltip composition only when the trigger is not a `Button`, the content needs custom markup, or the interaction is not a simple button tooltip.
## Table and List Empty States
TanStack Table is headless. It gives row state, sorting, filtering, and pagination, but it does not decide what empty UI should look like. When `table.getRowModel().rows.length === 0`, render `Empty.Root` in the table body or the surrounding list panel.
Use different copy for true empty data and filtered empty data:
```svelte
{#if rows.length === 0}
<Empty.Root class="min-h-64 border-0">
<Empty.Title>
{filter ? 'No results match your filters' : 'No recordings yet'}
</Empty.Title>
<Empty.Description>
{filter ? 'Try a different search term.' : 'Record audio to see transcripts here.'}
</Empty.Description>
</Empty.Root>
{/if}
```
## Avoid
- Plain `Loading...` text.
- Raw `Loader2Icon`, `LoaderCircleIcon`, or custom `animate-spin` outside `packages/ui`. Use `Spinner`.
- Raw `animate-pulse` placeholder blocks. Use `Skeleton`.
- Loading dots outside chat messages.
- Empty state copy inside an unstructured centered `div` when `Empty.*` would fit.
- Tooltip wrappers around `Button` when the `tooltip` prop is enough.
- Extra wrapper `div`s around `Empty.Root` just to center or stack content.
- Duplicating component internals from shadcn-svelte or extras instead of importing the local `@epicenter/ui` wrapper.
## Boundary With Svelte
Svelte decides which branch renders: `{#if}`, `{#await}`, query status, or derived state. Epicenter UI decides what the branch looks like: `Spinner`, `Skeleton`, `Progress`, `Empty`, `Command.Empty`, `Button tooltip`, or chat typing state.