npm

Guides

Requirement templates

How you brief the agent decides how close the first pass lands. Each template below has a Copy button and a Download button. Grab the .md, drop it in your repo, fill in the angle-bracket blanks, and paste it to an agent that has loaded the VUI guide. Every template ends with happy and unhappy test scenarios, so the agent builds the tests alongside the feature.

Before you start

Point your agent at the shipped guide first. It encodes the rules these templates lean on. Copy node_modules/@viliha/vui-ui/CLAUDE.template.md into your repo as CLAUDE.md or AGENTS.md. With that in place, every template below can stay short: you name what and where, and the guide supplies the how: tokens, page types, RecordView, and accessibility.

The six things every good requirement names

What (the outcome) · Where (route / file / nav) · Reuse (which VUI pieces to build on) · Data (shape + source) · States (loading / empty / success / error) · Done-when (the acceptance checklist). Leave one out, and the agent has to guess.

1 · New component

For a reusable UI primitive that belongs in the library.

component.md
Build a reusable component: <Name>

Purpose:    <one line — what it renders / does>
Location:   packages/ui/src/<name>.tsx  (auto-exported as @viliha/vui-ui/<name>)
Props:      # the public API
  - <prop>: <type> — <meaning> (required? default?)
Variants:   <e.g. primary | secondary — or "none">
Sizes:      <sm | md | lg — or "none">
States:     <hover, focus, disabled, loading, empty, error — whichever apply>
Rendering:  <Server Component by default; "use client" only if it needs
             hooks / events / browser APIs — keep the boundary on the leaf>
Design:     theme tokens only (no hard-coded color / spacing / radius);
             match the look of the existing components.

Test scenarios (happy / unhappy) — generate the real cases from the props /
variants / states above; this is only the shape:
  TC-1  Renders with required props              -> matches design, tokens applied
  TC-2  Each variant / size                      -> correct styles
  TC-3  Interactive state (click / hover / focus) -> expected behaviour
  TC-4  Disabled (if applicable)                 -> no interaction, dimmed
  TC-5  Keyboard focus                           -> visible ring, operable
  TC-6  Loading / empty / error (if applicable)  -> correct state shown

Done when: typed (no `any`), tokenized, light + dark, accessible
(keyboard + visible focus + ARIA), the scenarios above pass, lint + types pass.

2 · New page

For a screen in the admin app. First decide the page type, since that choice drives everything else.

page.md
Add a page: <Title>

Page type:  <data table | record form | dashboard | settings | board>   # pick one
Route:      apps/backoffice/app/(app)/<route>/page.tsx
Nav:        add to nav-config.ts + mirror the color in route-meta.ts
             (sidebar + breadcrumbs + open-tabs all follow automatically).
             Pick the grouping shape:
               - titled Section  — a static band under a heading (always visible)
               - collapsible Group — a parent with `children` that hide/unhide
             (see docs /navigation). Default to an existing Section; only add a
             Group when nesting several related sub-pages.
Data:       <where rows/records come from — a mock module now, your API later>

# If it's a DATA TABLE / RECORD FORM, list the fields (become RecordField[]):
Fields:
  - <key>: <label> — <text | number | badge | select> (required? copyable?
            hideInTable? options=[…]?)
Add/Edit/View: <slide-over (default) | full-page route>
Info panel:    <per-field help text?  formDescription intro?>
Actions:       <row actions, bulk actions, import/export?>
Form footer:   <Cancel + Save (default) | extra actions: name them and say what
                each does, whether it validates, and whether it closes the form>
Form extras:   <content between fields (formSlots)?  any full-width field?>
Behaviour:     <defaults unless stated: name-click opens View, delete confirms,
                the saved row flashes, Save closes the form>

Test scenarios (happy / unhappy) — generate the real cases from the fields /
actions above; this is only the shape:
  TC-1  Loads with data                 -> rows / sections render
  TC-2  Empty data                      -> empty state + primary action
  TC-3  Loading                         -> loading state
  TC-4  Fetch error                     -> error state, no crash
  TC-5  Primary action (+ Add / create) -> opens form / navigates
  TC-6  Create with valid input         -> saved, row appears
  TC-7  Create with invalid input       -> inline validation, blocked

Done when: follows the page frame, uses RecordView/RecordForm (never a
hand-rolled table or form), the scenarios above pass, tokens, light + dark,
a11y, lint + types + build pass.

3 · New feature / functionality

For a capability that spans more than a single page: a command palette, a bulk importer, a notifications tray.

feature.md
Implement: <feature name>

Goal:        <the outcome, in one sentence>
Where:       <route(s) / component(s) / a provider in app/_components>
Reuse first: <which existing VUI pieces — RecordView, Dialog, Menu,
              CommandPalette, ChartContainer — build on these, don't reinvent>
Behavior:    <the rules — triggers, inputs, outputs, edge cases>
States:      loading / empty / success / error  (all required)
Data:        <local state | a store | sessionStorage | your API>
Out of scope: <what NOT to build, so it stays focused>

Test scenarios (happy / unhappy) — generate the real cases from the behavior /
states above; this is only the shape:
  TC-1  Trigger with valid input      -> expected outcome
  TC-2  Empty / no data               -> empty state
  TC-3  Failure (API / error)         -> error surfaced, no crash
  TC-4  Edge case <…>                 -> handled gracefully
  TC-5  Re-trigger / idempotency      -> no duplicate side effects

Done when: reuses existing components (no duplicate UI), tokens, a11y,
every state handled, the scenarios above pass, lint + types + build pass.

4 · New form

For an Add or Edit form: what the record is, which cards it splits into, and the fields in each. The layout questions are already answered by the design system, so the template only asks the ones you actually decide. See the Form layout page for what the settings do.

form.md
Build a form: <Add Order>

Record:     <what one row is — an Order, a Customer, a Claim>
Route:      apps/backoffice/app/(app)/<route>/
Opens as:   <slide-over (default) | full-page route>
Data:       <where records come from — a mock module now, your API later>

Layout:     # the form is rows; each row says which sections sit side by side.
            # Three to a row is the most that stays readable.
  Row 1:  <Customer>, <Delivery>
  Row 2:  <Items>, <Payment>, <Notes>
  Row 3:  <Terms>                      # one section on a row fills the row

            Inside every card it is always two columns, one field per row:
            [i] Label *  |  [ control ].  Nothing to decide there.

Sections:   # optional: a line under a card's title. Order and width come from
            # the rows above, so there is nothing else to set here.
  - <Customer>: <what this card is for>

Fields:     # each becomes one row in its card
  - <key>: <Label> — <group> — <text | number | date | select | checkbox | combobox>
      required?   <yes | no>
      tooltip:    <what to type here, in one line — becomes the [i] on the label>
      options:    <for select/combobox: the list, or the API that supplies it>
      validation: <min/max, pattern, email, phone — or "none">

Footer:     <Cancel + Save (default) | name any extra buttons and say what each
             does, whether it validates, and whether it closes the form>
Behaviour:  <defaults unless stated: Save closes the form, delete confirms,
             the saved row flashes, a failing field gets a red border with its
             message on the tooltip>

Test scenarios (happy / unhappy) — generate the real cases from the fields
above; this is only the shape:
  TC-1  Open the form                    -> sections render, first field focused
  TC-2  Save with valid input            -> record saved, form closes, row appears
  TC-3  Save with a required field empty -> inline error, Save blocked, form open
  TC-4  Invalid value (bad email, etc.)  -> inline error naming the problem
  TC-5  Cancel with unsaved edits        -> nothing saved
  TC-6  Save fails on the server         -> error shown, typed values kept
  TC-7  Narrow window                    -> cards collapse to one column

Done when: built from a `fields` array through RecordView/RecordForm (no
hand-rolled <form>), layout set by `formRows` only, every field with help text
has a tooltip, the scenarios above pass, and lint, types and build are clean.

5 · Multi-step workflow

For a guided, multi-screen flow. Describe it one step at a time, and the agent turns each step into a screen built from existing blocks.

workflow.md
Implement a workflow: <name>

Steps (in order):
  1. <step> — route <…>, inputs <…>, validation <…>, primary action <…>
  2. <step> — …
Entry point:       <where the user starts>
Success end state: <where they land + what record(s) get created/updated>
Back / cancel:     <how each step handles going back and cancelling>
Errors:            <inline validation + failure handling per step>
Data model:        <the record(s) + fields the flow produces>
Building blocks:   <RecordForm | Dialog | AuthCard | a stepper (Steps) — per step>

Test scenarios (happy / unhappy) — generate the real cases from the steps
above; this is only the shape:
  TC-1  Complete all steps with valid input -> success end state reached
  TC-2  Step N invalid input                -> inline validation, blocked
  TC-3  Back from step N                     -> previous step, input preserved
  TC-4  Cancel mid-flow                      -> returns to entry, nothing created
  TC-5  API failure at submit                -> error shown, can retry
  TC-6  Refresh mid-flow (if persisted)      -> progress restored

Done when: each step has loading/error/success, validation (Zod), the scenarios
above pass, tokens, light + dark, a11y, lint + types + build pass.

Worked example: customer signup

The same template, filled in. This is the level of detail to aim for.

customer-signup.md
Implement a workflow: Customer signup

Steps (in order):
  1. Account   — route /auth/signup: Name*, Email*, Password*
                 (Zod: valid email, password >= 8). Primary "Create account".
  2. Verify    — route /auth/verify: 6-digit code + "Resend". On success -> step 3.
  3. Company   — route /onboarding: full-page RecordForm, fields
                 Company name*, Domain, Country, Size. Primary "Finish".
  4. Done      — redirect to /dashboard with a welcome toast.
Entry point:       "Sign up" link on /auth/signin.
Success end state: a Customer + Organization record created; land on /dashboard.
Back / cancel:     Back returns to the previous step keeping input;
                   Cancel returns to /auth/signin.
Errors:            inline field errors; a failed verify keeps the code screen.
Data model:        Customer { name, email }, Organization { name, domain,
                   country, size }.
Building blocks:   AuthCard for steps 1-2 (copy app/_components/auth.tsx),
                   RecordForm formMode="page" for step 3.

Test scenarios (happy / unhappy):
  TC-1  Valid signup through all 3 steps    -> Customer + Organization created, land on /dashboard
  TC-2  Step 1 invalid email / short password -> inline errors, Next blocked
  TC-3  Step 2 wrong verify code             -> error, stays on code screen
  TC-4  Step 3 missing Company name          -> inline error, Finish blocked
  TC-5  Back from step 2 -> step 1           -> account input preserved
  TC-6  Cancel at any step                   -> returns to /auth/signin, nothing created

Done when: every step has loading/error/success, Zod validation, the scenarios
above pass, tokens, light + dark, a11y, lint + types + build pass.

6 · Full feature (CRUD entity)

The complete brief for a full resource: screens, fields, API contract, test matrix, business rules, and happy and unhappy test scenarios, all in one file. It's generalized from a real PRD: fill it in and an agent can design the UI/UX and implement it. Read it below, or copy or download the .md straight into your repo.

feature-requirement.md
# `<Entity>` — Feature Requirement

> **How to use.** Copy this file, rename it to your entity/feature, and fill every
> `<…>` placeholder. This one file is the complete brief an agent needs to
> **design the UI/UX** (using the VUI page types) **and implement the feature**
> (API + UI). Delete rows/sections that don't apply. If you keep a separate API
> contract, keep this reconciled with it — otherwise this file is the source of
> truth.

## 1 · Resource Information

| Property | Value |
|----------|-------|
| Base Path | `/api/v1/<entities>` |
| Archetype | `<A reference/global-catalog · B org-scoped · C child-of-parent · D other>` |
| ID Type | `<INT (positive) · UUID>` — invalid `{id}` → **400** |
| Org-scoped | `<No — global · Yes — requires x-organization-id header>` |
| Soft Delete / Restore | `<No — hard-delete (guarded if children linked → 409) · Yes — soft-delete + restore>` |
| Bulk API | `<Yes — 1–100 items/request, all-or-nothing · No>` |
| Import / Export | `<Yes · No>` |
| RBAC | `<Enforced · Contract-level 403 only (not enforced yet — don't assert in tests)>` |

## 2 · Screens & Breadcrumb

**Sidebar IA (drives breadcrumbs — add to `nav-config.ts`):** `<Home > Section > Group > Entities>`
**Screens:** List, Create, Edit `<, View?>`.

- **Page type — pick one per screen (see the docs "Page types" catalog):**
  - **List → Data table** (`RecordView` + a `fields` array). Search, pagination, row selection, bulk actions in the header.
  - **Create / Edit → Record form** (`RecordForm`, same fields both times; Edit pre-fills):
    `<slide-over overlay (default) · full-page route (long forms / own URL)>`.
  - **Multi-step?** Use the **Steps** wizard (`@viliha/vui-ui/steps`).
  - Overview screen → **Dashboard**; settings-style form → **Settings**; pipeline → **Board**.
- **Empty state:** `<"No <Entities> found" + one line of guidance + the primary "+ Add <Entity>" action>`.
- **Search:** across `<which fields>`. **Bulk:** `<select-all + bulk delete/edit; note any guard behaviour>`.
- **Create/Edit behaviour:** one form; unsaved-changes confirm on leave; the side **info panel auto-generates from each field's `description`** (no extra design). All spacing/color from theme tokens — never hand-style fields.

### Fields (Create/Edit) → becomes a `RecordField[]`

| Field | Required | Type | Notes (validation · unique · readonly · options · copyable · hideInTable) |
|-------|:--------:|------|---------------------------------------------------------------------------|
| `<Name>` | Yes | `<text · number · select · badge>` | `<e.g. unique (case-insensitive); shown as typed>` |
| `<Code>` | Yes | text | `<unique; auto-capitalized on save>` |
| `<Derived>` | — | number | `<read-only, compute-on-read; not entered by hand>` |

## 3 · API Contract

| Operation | Method | Endpoint | Success | Error status |
|-----------|--------|----------|--------:|--------------|
| Get Many | GET | `/api/v1/<entities>` | 200 | 400, 401, 403 |
| Create One | POST | `/api/v1/<entities>` | 201 | 400, 401, 403, 409 |
| Get One | GET | `/api/v1/<entities>/{id}` | 200 | 400, 401, 403, 404 |
| Update One | PATCH | `/api/v1/<entities>/{id}` | 200 | 400, 401, 403, 404, 409 |
| Delete One | DELETE | `/api/v1/<entities>/{id}` | 204 | 400, 401, 403, 404, 409† |
| Create Many | POST | `/api/v1/<entities>/bulk` | 201 | 400, 401, 403, 409 |
| Update Many | PATCH | `/api/v1/<entities>/bulk` | 200 | 400, 401, 403, 404, 409 |
| Delete Many | DELETE | `/api/v1/<entities>/bulk` | 204 | 400, 401, 403, 404, 409† |
| Export | GET | `/api/v1/<entities>/export` | 200 | 400, 401, 403 |
| Import | POST | `/api/v1/<entities>/import` | 202 | 400, 401, 403, 409 |

`† 409 = <in-use guard, e.g. linked children block delete>`. Bulk bodies: create `{ items }` ·
update `{ ids, data }` · delete `{ ids }`; all-or-nothing. Async ops return `202` + job id → poll.
Error body: `{ statusCode, code, message, details? }`.

## 4 · API Test Matrix

| Scenario | Get One | Get Many | Create | Update | Delete | Bulk |
|----------|:-------:|:--------:|:------:|:------:|:------:|:----:|
| Success | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Invalid path id → 400 | ✓ | – | – | ✓ | ✓ | ✓ |
| Not found → 404 | ✓ | – | – | ✓ | ✓ | ✓ |
| Permission → 403 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Validation → 400 | – | – | ✓ | ✓ | – | ✓ |
| Duplicate → 409 | – | – | ✓ | ✓ | – | ✓ |
| In use → 409 | – | – | – | – | ✓ | ✓ |

## 5 · Business Rules

| Rule | Description | Client message | Endpoint |
|------|-------------|----------------|----------|
| BR-1 | `<e.g. <Field> is required>` | `"<Field> is required."` | POST, PATCH |
| BR-2 | `<e.g. Code auto-capitalized on save>` | — | POST, PATCH |
| BR-3 | `<e.g. <Field> unique (case-insensitive)>` | `"<Field> must be unique."` | POST, PATCH |
| BR-… | `<e.g. cannot delete while children linked>` | `"<message>"` | DELETE |

- **Derived values** are compute-on-read, never stored/entered.
- **Concurrency:** `<last-save-wins · optimistic lock>`.
- Client messages here must match the form's validation/error text.

### Test Scenarios (Happy / Unhappy Path)

> Illustrative example only — the actual scenarios must be generated from the real operations,
> fields, and business rules above (§ 3–5), not copied verbatim from this example.

#### Create

##### Happy Path

| ID | Scenario | Expected |
|----|----------|----------|
| TC-CRT-001 | Create with valid payload | 201 Created |

##### Unhappy Path

| ID | Scenario | Expected |
|----|----------|----------|
| TC-CRT-002 | Missing required field | 400 Bad Request |
| TC-CRT-003 | Duplicate unique value | 409 Conflict |
| TC-CRT-004 | Invalid foreign key | 409 Conflict |

## 6 · Definition of Done

- **UI**: correct page type(s); the form is a `RecordForm` built from the fields table (required `*`,
  centered label/icon/control, colors from tokens — no per-field styling); breadcrumbs derive from
  `nav-config.ts`; light + dark; a11y (keyboard, focus, ARIA).
- **States**: loading, empty, success, error all handled; validation/error text matches § 5.
- **API**: wired (or mocked) per § 3, incl. bulk / import-export if specified; error body shape honoured.
- **Verify**: `check-types`, `lint`, `build` pass; tests for the matrix in § 4 where logic is testable.

7 · Worked example: Calendar page

A filled-in page brief for the appointments Calendar (Month/Week/Day, AM/PM hour grid, color labels, add dialog). Use it as the shape to aim for when you brief a rich screen.

calendar.md
Build a page: Calendar (appointments)

Route:        /calendar  (client page — put metadata in a sibling layout.tsx)
Page type:    #3 Dashboard-style frame (flex h-full flex-col -> action header
              with <Breadcrumbs/> -> content min-h-0 flex-1 overflow-y-auto).
Nav:          add to nav-config.ts so sidebar + breadcrumbs derive it.

Views (a segmented switcher, top-right):
  - Month  — 6-week grid, event chips per day, "+N more" overflow.
  - Week   — 7 day columns over a scrollable 24-hour grid.
  - Day    — single day column over the same 24-hour grid.
  Week + Day show AM/PM hour labels, a red current-time line, and auto-scroll
  the current time into view on open.

Event model:
  Ev { id, date (yyyy-MM-dd), start "HH:mm", end "HH:mm", title,
       color (label key), type, guests?, meet?, location?, description?, notify }
  In Week/Day, blocks are positioned by start and sized by duration;
  overlapping events split into side-by-side columns (lane assignment).

Color labels:  Google-style palette (Blueberry, Tomato, Tangerine, Banana,
               Sage, Peacock, Lavender, Grape, Graphite) as static Tailwind
               classes — same convention as TAB_COLORS. Chosen color drives the
               block/chip color.

Add appointment (Dialog):
  - Title (borderless, live color accent) + Event/Task/Appointment tabs.
  - Date + start–end time via the app Select (15-min slots, "9:00 AM" labels) —
    NOT native <input type="time"> (ugly clock/panel).
  - Color swatch picker.
  - "More options" progressive disclosure: guests, Google Meet toggle,
    location, description, notify Select.
  - Blue primary Save; Cancel. Enter in the title saves.

Building blocks:  Dialog, Input, Select, Checkbox, Button, Breadcrumbs, cn,
                  date-fns. No new dependency; no hand-rolled table/time picker.

Test scenarios (happy / unhappy):
  TC-1  Switch Month/Week/Day                 -> grid re-renders, heading updates
  TC-2  Add appointment with title + time     -> block/chip appears in the right slot + color
  TC-3  Save with empty title                 -> Save disabled, nothing added
  TC-4  End <= start                          -> end auto-corrected to start + 1h
  TC-5  Pick a color                          -> block renders in that color
  TC-6  Open Week/Day on today                -> current-time line shown, scrolled into view
  TC-7  Click an event                        -> removed (demo behavior)
  TC-8  "More options"                        -> guests/meet/location/description/notify revealed

Done when: three views work, AM/PM everywhere, current-time line + auto-scroll,
color labels, the Select-based time picker, the scenarios above pass, tokens,
light + dark, a11y, lint + types + build pass.

8 · Worked example: Chat (ChatGPT-style)

A filled-in brief for the Chat assistant: centered thread, auto-growing composer, and image/file attachments with preview and remove.

chat.md
Build a page: Chat (ChatGPT-style assistant)

Route:        /chat  (client page — metadata in a sibling layout.tsx)
Nav:          add to nav-config.ts under the "shadcn/ui" section.
Page frame:   flex h-full flex-col -> action header with <Breadcrumbs/> ->
              content min-h-0 flex-1 (two-pane).

Layout (clean, enterprise, ChatGPT-like):
  - Left (w-64, bg-muted): "New chat" button + scrollable conversation history.
  - Main: a centered (max-w-3xl) scrolling column of messages, then a composer.

Messages:     avatar (You / AI) + role label + text; assistant vs user differ by
              avatar color only. No colored bubbles — keep it clean.
Composer:     rounded-2xl bordered box holding an auto-growing <textarea>
              (Enter sends, Shift+Enter = newline), an attach (+) button (left),
              and a primary send icon button (right). focus-within highlights it.

Attachments (images + files):
  - Hidden <input type="file" multiple accept="image/*,.pdf,..."> triggered by +.
  - Pending attachments show as chips above the textarea (image -> thumbnail,
    else file icon + name + size) each with a remove (x).
  - On send, attachments attach to the user message: images render as
    thumbnails, files as downloadable chips.
  - Use URL.createObjectURL for previews (demo). ponytail: swap for real uploads.

Data model:
  Attachment { id, name, size, url, isImage }
  Msg        { id, role: "user" | "assistant", text, attachments: Attachment[] }
  Chat       { id, title, messages: Msg[] }   // title from first user message

Responses:    canned assistant echo so the UI feels live. ponytail: replace with
              a Claude API call (claude-opus-4-8 / claude-sonnet-5).
Building blocks:  Button, cn, Breadcrumbs, SetPageTitle. Radix icons only
                  (PlusIcon attach, PaperPlaneIcon send, Pencil1Icon new chat,
                  FileIcon, Cross2Icon). No chat/upload dependency.

Test scenarios (happy / unhappy):
  TC-1  Type + Enter                 -> user message + assistant reply appended, scrolls down
  TC-2  Shift+Enter                  -> newline, does NOT send
  TC-3  Attach an image              -> thumbnail chip in composer, then in the sent message
  TC-4  Attach a file, remove it     -> chip disappears, not sent
  TC-5  Send empty with no files     -> Send disabled, nothing added
  TC-6  New chat                     -> fresh conversation, prior one kept in history

Done when: centered thread, auto-grow composer, image + file attachments with
preview/remove, New chat history, canned reply, the scenarios above pass,
tokens, light + dark, a11y, lint + types + build pass.

9 · Worked example: Support & ticketing

A filled-in brief for the Support desk: a ticket queue with status/priority, a detail pane, and a reply thread.

support.md
Build a page: Support & ticketing (help-desk ticketing system)

Route:        /support  (client page — metadata in a sibling layout.tsx)
Nav:          add to nav-config.ts under the "shadcn/ui" section.
Page frame:   flex h-full flex-col -> action header with <Breadcrumbs/> ->
              content min-h-0 flex-1 (three-pane inside one card, p-4).

This is a ticketing system, NOT a chat. Three regions:
  - Left (w-80, border-r): ticket queue. Search Input (subject / ref /
    requester) + a status-filter Select over a scrollable list. Each row:
    subject, status badge, priority dot, ref (TCK-####), requester, updated.
    Active row tinted bg-accent.
  - Center: detail header (status badge, ref, subject) -> the original request
    -> a stacked ACTIVITY TIMELINE (avatar + author + role + time + text, each a
    bordered card, all left-aligned — not left/right chat bubbles) -> a reply
    textarea + "Send reply" button. Sending appends a comment and sets status
    -> Pending.
  - Right (w-60, border-l, hidden < lg): properties rail — editable status +
    priority Selects, requester, assignee, last updated.

Data model:
  Status   = "open" | "pending" | "resolved"
  Priority = "low" | "medium" | "high" | "urgent"
  Comment  { id, author, role: "agent" | "customer", text, time }
  Ticket   { id, ref, subject, requester, assignee, status, priority, updated,
             description, comments: Comment[] }

Colors:       status badges + priority dots use static Tailwind classes keyed by
              value (same convention as TAB_COLORS / calendar labels), light+dark.
Building blocks:  Input, Select, Button, cn, Breadcrumbs, SetPageTitle, date-fns.
                  The queue is buttons; for sorting/columns/bulk actions switch to
                  RecordView (see /docs/data-table). No table/chat dependency.

Test scenarios (happy / unhappy):
  TC-1  Select a ticket             -> detail + activity + properties switch, row active
  TC-2  Change status / priority    -> Select updates the ticket + badge/dot
  TC-3  Filter by status            -> queue narrows to that status
  TC-4  Send a reply                -> comment appended to timeline, status -> Pending
  TC-5  Reply empty                 -> Send disabled, nothing added
  TC-6  Search by ref/requester     -> queue filters; empty -> "No tickets found."

Done when: queue + detail + properties render, search & status filter work,
status/priority editable, reply appends to the timeline, badge/dot colors from
the maps, the scenarios above pass, light + dark, a11y, lint + types + build pass.

Specificity beats length

You don't need long prose; you need the blanks filled. A field list, a page type, and a done-when checklist get you a first pass that already looks like VUI. Vague requirements get you vague components.