Frontend Development — ayunis-core-frontend
Working Directory
All commands run from ayunis-core-frontend/:
cd ayunis-core-frontend
Before modifying any layer, read its SUMMARY.md in src/[layer]/SUMMARY.md. The top-level src/SUMMARY.md provides an overview.
Concise UI Copy
Give users enough information to choose confidently, with minimal repetition.
- Use short, specific action verbs for buttons: Archive, Restore, Delete. Include the target when the control would otherwise be ambiguous on its own.
- Let nearby titles identify the subject and descriptions explain the consequence. When an action row already says Archive organisation and explains its effect, its button can simply say Archive.
- Apply the same approach inside confirmation modals: keep the subject and consequences in the title and body, and use the action verb for the confirmation button. Avoid generic Yes, OK, or Confirm when a specific verb is clearer.
- Preserve information that changes the decision: what is affected, whether the action is reversible, and any required confirmation. Concision must not weaken destructive-action warnings or remove the resource name needed to confirm deletion.
- Prefer one useful explanation over repeated helper text. Keep implementation details out of product copy unless they help the user decide or recover from an error.
- Review the rendered copy in each affected locale. Short labels must remain clear in context; do not impose word limits that make translations unnatural.
Validation Sequence
Choose validation breadth using the repository's Proportional Workflow.
For Standard and High-Risk changes:
pnpm run build # Must succeed
pnpm run lint # Must pass
For Fast Path changes, run the narrowest relevant test when behavior or test
code changed, plus any lint/type check applicable to the touched files. Run the
build when imports, types, dependencies, bundling, or other compile-time
behavior could be affected.
Browser journey or system boundary changed? Load the e2e skill when
lower-level tests do not sufficiently prove the behavior. Add data-testids
needed by the journey to touched components in the same PR
(<feature>-<element>, kebab-case) — text selectors are banned because the UI
is i18n'd.
Would visual evidence materially help review? After the product PR exists,
load the pr-media skill and publish the smallest useful scene set to
pr-media/pr-<number>. Do not commit media scenes to the product branch.
Architecture (Feature-Sliced Design)
packages/ui/ # Framework-level UI primitives, tokens, and utilities
ayunis-core-frontend/src/
├── pages/ # Route components (compose widgets/features)
├── widgets/ # Reusable composites (used in ≥2 pages)
├── features/ # Self-contained business logic
└── shared/ # App-wide infrastructure and app-aware UI
Import rules: pages → widgets → features → shared
Layers only depend on layers to their right. Never import upward.
Check demonstrated conventions before deciding placement
The FSD rules above are the theory; this repo's actual conventions are the tie-breaker. Before deciding where a slice or component lives — feature vs widget vs page, which page a route maps to, whether something is "shared enough" to promote a layer — grep how comparable cases are already structured and follow that, rather than reasoning from FSD principles alone. Before adding any frontend file or introducing a new slice segment, inspect the target slice's existing directories and the two closest analogues.
- The abstract heuristics ("used in ≥2 pages → widget", "used from more than one slice → promote") are necessary but not sufficient. They routinely disagree with how the codebase actually draws its boundaries when viewed across the whole repo instead of a single branch.
- Concrete convention that has bitten before: pages map one slice per route — e.g. a list route and its detail route are separate page slices (
skills.index vs skill.$id), not one page reused across two routes. Check the route files and src/pages/ before assuming a shared placement.
- When a colocated placement (feature/page) and a "promote to shared/widget" placement both look defensible, the existing convention wins. Look at the
Reference Pages in the new-page skill and grep sibling slices before moving code.
- Do not double down on a theory-driven placement after pushback — re-check the convention first. See "Confirm placement before mutating a stacked PR chain" below.
Confirm placement before mutating a stacked PR chain
Do not execute structural moves and amend commits across a stacked-PR chain (e.g. gt modify a parent, then check out and amend the child) off a preliminary placement conclusion. Settle the placement against repo conventions first, then move — reverting a wrong move across stacked branches means restoring exact pre-session SHAs from the reflog.
Shared UI Package — Registry-Managed Primitives
Framework-level primitives live in the repository-root packages/ui/ workspace and are imported through @ayunis/ui subpaths. App-aware shared components remain in ayunis-core-frontend/src/shared/ui/.
- Run shadcn registry commands from
packages/ui/, whose components.json owns the aliases and registry configuration: cd ../packages/ui && pnpm dlx shadcn@latest add <component>.
- Do not patch
packages/ui/src/components/ for a feature-specific use case. Wrap or compose the primitive in the relevant frontend ui/ directory or src/shared/ui/ instead.
- Changes inside
packages/ui/ must remain application-independent and be intentional design-system work. If a primitive genuinely needs a new generic capability, confirm that scope with the user before changing it.
- Every newly introduced
packages/ui/src/components/<name>.tsx component must include a colocated <name>.stories.tsx. Cover its representative default state and meaningful generic variants, states, or interactions; compound components should be demonstrated as a usable composition.
- Import primitives from their public subpaths, such as
@ayunis/ui/components/button, and cn from @ayunis/ui/lib/cn. Do not reach into packages/ui/src/ from the frontend.
Reach for existing primitives and tokens
Compose the existing design system before hand-rolling layout, and use design tokens instead of raw Tailwind color scales:
- Look for a composite primitive first. For an icon-plus-label row (banners, result cards, list rows) use the
Item family — Item / ItemMedia variant="icon" / ItemContent — rather than assembling a bare flex container yourself. Check @ayunis/ui for a framework primitive and src/shared/ui/ for an app-aware component before adding your own.
- Use semantic color tokens, never hardcoded palette classes.
text-brand, text-muted-foreground, etc. — not text-amber-500, text-blue-600, or other raw Tailwind color scales, which break theming and dark mode.
Page module internals
Each page module can have these subdirectories:
src/pages/<page-name>/
├── ui/ # Components — component-local state/hooks and JSX
├── hooks/ # Standalone React behavior hooks, when the slice uses this segment
├── api/ # Mutation hooks (one per operation)
├── model/ # Types, constants, schemas
└── lib/ # Pure helpers and established reusable behavior hooks
Keep ui/ components focused on component logic. Calling hooks inside a component does not make ui/ the home for standalone use*.ts files. Put standalone interaction or presentation behavior hooks in the slice's established hooks/ or lib/ segment, choosing between them from the target slice and its two closest analogues; keep server operations in api/ and state/domain concerns in model/ when that is the slice convention. A component-specific hook may stay colocated only when comparable components in that slice already follow that pattern.
API Client
After backend API changes, regenerate the client:
pnpm run openapi:update # Regenerates src/shared/api/generated/
Never edit generated code manually — it will be overwritten.
Hook Pattern
One hook per operation, encapsulating mutation logic. Use showSuccess/showError from @/shared/lib/toast for user feedback, and extractErrorData from @/shared/api/extract-error-data for structured error handling.
For hooks that back a form (create/update dialogs), load the frontend-form-pattern skill — it covers form types, structure, and the full end-to-end validation pattern including backend DTO validation, field-level error display, and i18n.
Verifying in the Browser
When the change affects rendered UI or browser behavior, use your harness's browser tooling to check the affected page renders and the console is clean. A render failure shows the React dev-server error overlay — the element #webpack-dev-server-client-overlay must not exist. The frontend URL depends on the dev slot (see dev-environment); seeded login credentials are in seed-database.
Completion Checklist