svelte-best-practice
Use when designing, refactoring, or implementing new features in the svelte admin frontend.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Use when designing, refactoring, or implementing new features in the svelte admin frontend.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
Svelte 5 component, state (runes), and styling conventions for the admin_frontend. Use when designing, refactoring, or implementing features in the Svelte admin frontend — decomposing large .svelte feature pages, choosing Tailwind v4 utilities vs scoped/global styles, structuring .svelte.ts controllers and URL/session preferences, building shared admin UI (tables, dialogs, page headers, tabs, inline feedback), wiring {@html}/markdown previews safely, or reviewing an admin_frontend change before merge.
Use when answering questions, especially troubleshooting or design questions, to format responses using common tools.
SOC 職業分類に基づく
| name | svelte-best-practice |
| description | Use when designing, refactoring, or implementing new features in the svelte admin frontend. |
Applies to: admin frontend only. If the task is not related to the admin frontend, ignore this skill.
This document captures conventions for large Svelte 5 feature pages in admin_frontend, especially when a .svelte file grows past a few hundred lines. It is derived from refactoring guidance for feature pages such as Characters.
Prefer a “composition root” over a monolith. The route or top-level *Page.svelte should mostly:
If a single <script> block mixes unrelated lifecycles (e.g. browse list, edit form, photo crop, navigation guards), split by concern so each piece can be tested and changed independently.
For document / interval / teardown boilerplate, a small *.ts helper that returns an onMount cleanup (e.g. setupFeatureRuntime(...)) keeps the shell readable. $effect blocks that tie many shell locals together (e.g. bind:, one-off UI flags) often stay on the page unless you deliberately move them into .svelte.ts.
Move side-effect-free helpers to *.ts modules next to the feature (or under shared/ / $lib when reused):
formFromCharacter, saveBody, validation).prettyJson, empty defaults, merge helpers).This shrinks the Svelte file without changing behavior and makes unit testing straightforward.
When two blocks are structurally the same (e.g. “provider list → model list → ordered selected list with drag-and-drop”), do not copy-paste:
*.ts module.For two shapes inside one file (e.g. inline row + tooltip), Svelte 5 {#snippet} can dedupe markup without extracting a whole component.
Fixing a bug once beats forgetting the mirror copy.
{@html}Do not hand-roll a markdown-to-HTML pipeline in a page component for production previews:
{@html}.{@html} boundary as a security and maintenance boundary; centralize rendering in one preview component.Before adding any new npm dependency, verify the current latest stable version (do not rely on stale training data).
<style> — when to use whichThis project uses Tailwind CSS v4 with design tokens in global CSS (app.css). Default to utility classes for layout, spacing, typography, and colors.
Use component <style> (scoped) sparingly for:
Avoid large blocks of :global(...) rules in a feature page. If styles are global-by-necessity, they belong in:
app.css) when truly app-wide.Red flag: many :global selectors for “.field”, “.section-card”, etc. — that is usually a sign to introduce small presentational components (Field.svelte, SectionCard.svelte) with Tailwind on real elements instead of global class names.
When replacing layout <style> with utilities, default sm / md / lg / xl breakpoints may not match a legacy @media. Use arbitrary min-width (e.g. min-[1180px]:) when you need pixel-parity, or the layout will drift subtly.
For markdown/HTML previews, consider @tailwindcss/typography and a prose variant on the wrapper. That often replaces dozens of hand-written h1/p/ul rules.
If you skip the plugin, keep preview typography rules in the preview component’s scoped <style>, not in the parent page.
Split state by responsibility when a page gets heavy:
.svelte.ts): loading flags, list rows, API orchestration, save / delete.create*Preferences() in .svelte.ts); children receive plain props, not ad-hoc sessionStorage reads in leaves.When moving state out of the page:
bind: targets remain valid (writable state must live where Svelte expects).A scalable pattern:
features/<area>/
<Area>Page.svelte # thin shell
browse/ # list-only UI
view/ # read-only detail
edit/ # form sections, dialogs
state/ # *.svelte.ts controllers, guards
shared/ # helpers, *-classes.ts, *-a11y.ts (stable ids), lifecycle, widgets
Names are flexible; the idea is vertical slices by screen concern, not one mega-file. shared/ is not only Svelte widgets—pure helpers, design tokens / class maps, stable DOM ids, and mount helpers belong there too.
When decomposing a large page:
*.ts helpers (no UI change)..svelte.ts modules if the <script> is still huge.Each step should leave the app working.
:global style blocks in page components for patterns that could be components.aria-controls, the target stays in the DOM when “closed” (e.g. hidden), not {#if}-removed, unless you omit aria-controls when the target is unmounted.bind: and runes still line up after moving state.Short pitfalls seen on complex admin pages (e.g. heavy forms + .svelte.ts controllers). Prefer fixing the pattern once here rather than rediscovering it per feature.
Derived values from create*() factories. Expose $derived fields through getters on the returned object (get visibleRows() { return visibleRows }), not shorthand properties (return { visibleRows }—that can raise state_referenced_locally and capture a stale value). Getters also help when consumers are identity-sensitive (Set membership, reference equality) or need plain snapshots.
$effect that notifies parents. When an effect pushes DOM refs or derived snapshots upward (e.g. bind:this on <canvas> → onCanvasChange(el)), compare against a last-synced reference (or equivalent) before calling the parent so unrelated reactive churn or changing callback identities do not trigger redundant parent work.
Dynamic IDs for accessibility. For collapsibles and expandable regions, pair aria-controls with the controlled element’s id using a real expression (e.g. shared feature-a11y.ts constants used by both control and target). Centralize ids so strings cannot drift. If aria-controls points at an element, that element must exist in the DOM when the relationship matters—prefer hidden over {#if} for the region so the id is not removed while the button still references it.
bind: requires a concrete binding target. You often need parallel {#if} / {:else} branches that differ only by which property is bound (bind:selectedIds={form.llm_models} vs ...voice_models). That duplication is normal—the compiler needs a direct writable path. Abstracting it away usually means introducing an explicit writable adapter or wrapper component.
Shared visual tokens without new globals. When several sibling components must match the same Tailwind-heavy styling (section titles, hints, cards), a tiny *.ts module exporting class string constants avoids drift and reduces pressure to add feature-specific global utilities in app.css.
Plain *.ts importing factories. Mount helpers and similar should import type { … } from *.svelte.ts when they only need types, avoiding accidental circular runtime imports with state/ factories.
Cross-page rules.
createToastNotifier() ($lib/ui/). No page-local notify() + toast $state + setTimeout.InlineDestructiveAlert, InlineLoading, InlineEmptyState from $lib/ui/. No raw border-destructive/30 bg-destructive/10 blocks, no inline <p>Loading…</p>.AdminPageHeader. Every page renders through it; it owns the max-w-[1420px] left-aligned wrapper. No mx-auto page wrappers. Header tokens (kicker / title / intro) live only in $lib/styling/admin-tokens.ts — no feature-local *_HEADER_* copies.AdminPageHeader + AdminTabStrip (+ AdminRecordTabChip for "open record" tabs). Every tabbed page syncs ?tab= via createTabPreferences(...) in $lib/preferences/<feature>-preferences.svelte.ts. Tab descriptors declare kind: 'pane' | 'route'.SectionScrollNav with #hash (not ?tab=, not AdminTabStrip). Per-record dynamic subtab strips stay feature-local until a second consumer appears.AdminPageLinkAction (real <a href>) for header actions that navigate elsewhere — never <Button onclick={goto(...)}>. Middle-click / copy-link must work.<AdminPageHeader sticky> (publishes --admin-page-header-h). Second-level bars use <AdminPageStickyToolbar>; sticky table heads use <AdminTableShell stickyHead>. The page title must stay visible when sticky — don't hide it in favor of a lower bar.$lib/components/page/table/: AdminTableShell, AdminTableHeaderCell, AdminFilterBar, AdminMasterDetail, plus useTableSort / useTableFilters for state and URL sync. No bespoke sort-header or filter-bar markup. Virtualized log feeds are exempt.FormField (label+input) or class={ADMIN_INPUT} (bare). No raw h-10 rounded-md border border-input … soup.<SectionCard> (solid) and <SectionCardMuted> (translucent, nested).$lib/components/ui/dialog/. No Modal.svelte.$lib/catalog/. Unsaved-changes uses the canonical createUnsavedGuard (page-specific predicate passed in).*Panel.svelte (or *Tab.svelte) that: (a) has no <svelte:head> / AdminPageHeader / page wrapper; (b) accepts state and notifier as props; (c) namespaces any URL params it owns (no top-level ?tab= assumption); (d) renders self-contained section markup; (e) uses shared loading/error/empty primitives.docs/admin-ui.mdmintdocs/build/first-time-setup.mdx