원클릭으로
north-frontend
Frontend development guide for the North app crate — Tailwind v4, a11y, component architecture, and styling conventions.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Frontend development guide for the North app crate — Tailwind v4, a11y, component architecture, and styling conventions.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
North-specific conventions for writing Playwright e2e tests — auth fixture, API helper, selectors, and known pitfalls. Use when writing or editing tests in e2e/.
Bump the base Docker image version
Bump app version and prepare a release
| name | north-frontend |
| description | Frontend development guide for the North app crate — Tailwind v4, a11y, component architecture, and styling conventions. |
| trigger | When writing or modifying files in crates/app/, crates/ui/, crates/stores/, crates/repositories/, or style/main.css. Activated when working on Leptos views, components, containers, pages, stores, repositories, or CSS. |
This skill covers styling, accessibility, component architecture, and Tailwind v4 conventions for the North app crate. Follow these rules when creating or modifying frontend code.
containers/) — Complex widgets connected to stores and repositories. Use controllers (view-model pattern) to orchestrate stores, repos, and domain models, preparing callbacks and reactive data for views. Prefer passing destructured controller fields/methods as props into views so views can be rendered independently (e.g. in a storybook) without a controller. Controller state is in-memory — resets when the container unmounts. Files: controller.rs (view-model), view.rs (pure UI), container.rs (wires controllers to views), mod.rs (re-exports), components/ (optional subdirectory for sub-components extracted from the view). Allowed deps: stores, repositories, controllers.components/) — Presentational components not connected to stores or repositories. Compose atoms and HTML with clean, well-typed props interfaces (React-style). Allowed deps: props only (signals, memos, callbacks), atoms.atoms/) — Basic building blocks: buttons, inputs, typography, badges. Multi-dimension enum props with fn classes(self) -> &'static str. See docs/UI_KIT.md for the full catalog. Allowed deps: leptos only, no dto/store imports.One component per file. Never place multiple #[component] definitions in a single file. Extract each component into its own file — containers use {container}/components/ for sub-components, top-level components use components/ with one file per component and a mod.rs re-export.
Before writing any view code, check crates/app/src/atoms/ and docs/UI_KIT.md for existing atoms that cover your need. Use atoms instead of hand-rolling Tailwind classes. Only fall back to raw HTML+Tailwind when no atom exists yet.
Before writing a new container, component, or atom, read 2–3 existing ones from the corresponding directory for conventions and patterns.
// View props — always reactive types
pub fn InboxView(
active_task_ids: Memo<Vec<i64>>, // Derived filtered data
is_loaded: Signal<bool>, // Store-derived state
is_form_open: ReadSignal<bool>, // Local UI state
set_form_open: WriteSignal<bool>, // Local UI state writer
on_task_click: Callback<i64>, // Event handler
on_reorder: Callback<(i64, String, Option<Option<i64>>)>,
) -> impl IntoView
Rules:
Memo<T>, Signal<T>, ReadSignal<T>, WriteSignal<T>, and Callback<T> — never raw data or stores.on_click=Callback::new(move |id| ctrl.do_thing(id)) — no intermediate variables.#[prop(into)] for flexible signal acceptance. Use #[prop(optional)] for optional callbacks.icon_only: bool prop for compact rendering in task card action bars.Page → Store → Repository → Server Function
↓
View (pure rendering)
provide_context() directly in containers. Consume via expect_context::<T>() or typed helpers (use_app_store()).Domain models live in repositories/src/models/. They convert API DTOs into frontend-friendly structs — unfolding compressed fields (e.g. raw RRULE strings → a Recurrence struct) and adding methods that centralize entity logic in one place.
When needed: Writing controllers or store logic that works with data coming from the backend. If you find yourself parsing or interpreting DTO fields in multiple places, that logic belongs in a domain model.
Pattern: From<Dto> converts the DTO, optionally unfolding fields into richer types. Methods on the model keep domain logic co-located with the entity.
Check repositories/src/models/ when working with frontend data models or creating new ones.
When a Callback prop's behavior depends on reactive state (toggles, settings, filters), wrap it in Signal::derive → Signal<Callback<T, R>>. The derive tracks the dependencies; the inner callback captures their snapshot as plain values. This eliminates the need for a separate "trigger" signal to force re-evaluation. Consumers call signal.get() inside a Memo to subscribe, then callback.run(...).
refetch() or create its own Resource on mount.AppLayout is purely structural (auth guard, context providers, sidebar + main shell). It does NOT pre-fetch data.Resource for SSR-compatible async data. Use LocalResource only for browser-only APIs.The project uses a three-layer theme pattern in style/main.css:
:root { --bg-primary: #F9F8F6; } ← Semantic CSS variables (light)
.dark { --bg-primary: #1C1D2B; } ← Dark overrides
@theme { --color-bg-primary: var(--bg-primary); } ← Tailwind utility bridge
Rules:
bg-bg-primary, text-text-secondary, border-border.dark: prefix utilities — dark mode is handled by CSS variable switching.:root, .dark, AND @theme in style/main.css.@utility directive (gets variant support for free).| Token | Usage |
|---|---|
bg-primary | Main background |
bg-secondary | Sidebar, cards |
bg-tertiary | Hover backgrounds, code blocks |
bg-input | Form input backgrounds |
text-primary | Primary text |
text-secondary | Secondary/muted text |
text-tertiary | Placeholder, disabled text |
accent / accent-hover | Primary action color |
danger / danger-hover | Destructive actions |
warning | Warning indicators |
success | Success indicators |
on-accent | Text on accent background |
flex items-center justify-between gap-2space-y-4 or flex flex-col gap-4px-3 py-1.5 (buttons), p-1 (icon buttons), p-4 (sections)max-w-md, max-w-lg, max-w-2xl for content areastext-xs (11px meta), text-sm (13px default), text-base, text-lg, text-2xl (headings)font-medium (labels), font-semibold (headings, emphasis)text-text-primary (main), text-text-secondary (labels), text-text-tertiary (hints)font-sans (Inter), font-mono (JetBrains Mono, for code)Every button and clickable element MUST have cursor-pointer and visible hover feedback. Tailwind v4 defaults buttons to cursor: default, so this is never optional. No exceptions — if it's clickable, it gets cursor-pointer + a hover state change + transition-colors.
Every interactive element MUST have visible state transitions:
// Standard hover + transition
class="hover:bg-bg-tertiary transition-colors"
// Text hover (secondary → primary)
class="text-text-secondary hover:text-text-primary transition-colors"
// Icon button hover
class="text-text-tertiary hover:text-text-secondary hover:bg-bg-input transition-colors"
Always include transition-colors (or transition-opacity, transition-all) on elements with hover/focus state changes.
sm:, md:, lg:) for responsive overrides.@container / @sm:, @md:) for reusable components that need to adapt to parent size rather than viewport.Use semantic HTML. Always.
// CORRECT: native button
<button on:click=handler>"Delete"</button>
// WRONG: div pretending to be a button
<div on:click=handler>"Delete"</div>
<button><a href="..."><div> or <span> with on:click for interactive elements.cursor-pointer on all clickable non-link elements:
// Buttons that perform actions
class="cursor-pointer ..."
// Clickable cards, rows, toggles
class="cursor-pointer ..."
Tailwind v4 defaults buttons to cursor: default (browser behavior). You MUST add cursor-pointer explicitly.
The project uses a global focus-visible style in main.css:
*:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 4px;
}
Rules:
outline (not ring) for focus indicators — ring uses box-shadow which is invisible in Windows High Contrast Mode..no-focus-ring class exists for special cases — use sparingly.focus-visible:.Every interactive element needs BOTH hover and focus-visible states:
class="hover:bg-bg-tertiary focus-visible:bg-bg-tertiary transition-colors cursor-pointer"
Every button with only an icon MUST have an accessible name:
// Option 1: aria-label (preferred for brevity)
<button aria-label="Delete task" class="p-1 cursor-pointer ...">
<Icon kind=IconKind::Trash class="w-4 h-4" />
</button>
// Option 2: sr-only text
<button class="p-1 cursor-pointer ...">
<Icon kind=IconKind::Trash class="w-4 h-4" />
<span class="sr-only">"Delete task"</span>
</button>
Decorative icons alongside text get aria-hidden="true" (already handled by the Icon component for most cases).
Dropdowns / Menus:
<button aria-expanded=is_open aria-haspopup="menu">"Options"</button>
// When open:
<div role="menu">
<button role="menuitem">"Edit"</button>
<button role="menuitem">"Delete"</button>
</div>
Modals:
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
<h2 id="modal-title">"Edit Task"</h2>
// ...
</div>
Checkboxes (custom):
<button role="checkbox" aria-checked=is_checked aria-label="Complete task">
When content changes without a page reload (optimistic updates, completions, errors), announce it:
// Non-urgent status updates
<div aria-live="polite">{status_message}</div>
// Errors and critical alerts
<div aria-live="assertive" role="alert">{error_message}</div>
polite for task completions, save confirmations, filter results.assertive only for errors and critical alerts.aria-live container must exist in the DOM BEFORE the content changes.// Always associate labels with inputs
<label for="task-title" class="text-sm font-medium text-text-secondary">
"Task Title"
</label>
<input id="task-title" type="text" ... />
// When no visible label, use aria-label
<input type="search" aria-label="Search tasks" placeholder="Search..." ... />
// Error messages: link with aria-describedby
<input id="email" aria-invalid="true" aria-describedby="email-error" ... />
<span id="email-error" role="alert" class="text-sm text-danger">
"Invalid email"
</span>
Never rely on placeholder alone as a label. Placeholders disappear on input and have poor contrast.
// Native disabled (removes from tab order)
<button disabled class="disabled:opacity-50 disabled:cursor-not-allowed">
// aria-disabled (stays focusable — good for submit buttons that trigger validation)
<button aria-disabled=is_disabled
class="aria-disabled:opacity-50 aria-disabled:cursor-not-allowed"
on:click=move |_| {
if is_disabled.get() { return; }
// proceed
}
>
Respect user preferences:
class="transition-transform motion-reduce:transition-none"
app/src/atoms/)Semantic, multi-dimension prop components in crates/app/src/atoms/. Generic UI primitives (Icon, Modal, Popover, etc.) remain in north-ui. See docs/UI_KIT.md for full catalog and design rationale.
Rules:
<Text variant=TextVariant::HeadingLg> over <h1 class="text-2xl font-semibold ...">use crate::atoms::{Text, TextVariant, TextColor, TextTag};tag prop when needed// Primary action
class="px-4 py-1.5 text-sm font-medium bg-accent text-on-accent \
rounded cursor-pointer hover:bg-accent-hover \
focus-visible:bg-accent-hover transition-colors"
// Secondary / ghost
class="px-3 py-1.5 text-sm text-text-secondary cursor-pointer \
hover:text-text-primary hover:bg-bg-tertiary \
focus-visible:bg-bg-tertiary transition-colors"
// Icon button
class="p-1 rounded text-text-tertiary cursor-pointer \
hover:text-text-secondary hover:bg-bg-input \
focus-visible:bg-bg-input transition-colors"
// Danger
class="px-3 py-1.5 text-sm text-danger cursor-pointer \
hover:bg-bg-tertiary focus-visible:bg-bg-tertiary \
transition-colors"
class="w-full bg-bg-input border border-border rounded px-3 py-1.5 \
text-sm text-text-primary placeholder:text-text-tertiary \
focus:outline-none focus:border-accent transition-colors"
class="fixed inset-0 z-50 bg-backdrop flex items-center justify-center"
class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm \
text-text-primary cursor-pointer \
hover:bg-bg-tertiary transition-colors"
// Container: show children on hover via group
class="group ..."
// Action buttons: hidden until parent hover
class="opacity-0 group-hover:opacity-100 transition-opacity"
Before submitting frontend code, verify:
<Text> atom for all text — never raw <span class="text-sm ..."> for new code<button> or <a>, never <div>/<span> with on:clickcursor-pointertransition-colors (or appropriate transition)aria-label or sr-only textbg-bg-primary), never raw values or dark: prefixesaria-label)role="dialog" and aria-modal="true":root, .dark, @theme)