# File types Package: @commenda/pathfinder@0.1.0 Page: /docs/assets/file-types ## Folders [#folders] ```tsx import { Icons } from '@commenda/pathfinder'; ``` `status` is required and accepts `open` or `closed`. Sizes are 16 (default), 24, and 40. Native `width` and `height` can change display dimensions while `size` selects the authored drawing. These are the illustrated document folders; outline interface glyphs remain under `Icons.Interface`. ## Usage [#usage] ```tsx import { FileTypeIcon, FileTypeHelper } from '@commenda/pathfinder/file-types'; ; ``` **ASSET-FILE-01:** Use 16 for rows/chips/inline links, 24 for lists/tables, and 40 for uploads/cards. All 13 families and 39 variants are local. Preserve outlined lettering and type colors; no font is required to render the badges. **ASSET-FILE-02:** Pair artwork with a filename. File kind is conveyed by badge lettering and filename, not color alone. The 16px JSON drawing exports at **17×16** because its badge extends outside the component; preserve that width rather than squeezing the lettering. `kind` accepts `pdf`, `doc`, `xls`, `csv`, `ppt`, `txt`, `image`, `json`, `xml`, `zip`, `generic`, `folder`, and `folder-open`. The filename helper normalizes extension case and uses the final filename segment. Extensionless names, dotfiles, and unfamiliar extensions return `generic`. It does not inspect file content or parse URLs. **ASSET-FILE-03:** The resolver follows the extension lists printed beneath the Figma component sets: | Kind | Extensions | | -------------- | ------------------------------------ | | PDF | pdf | | Document | docx, doc, rtf, odt | | Spreadsheet | xlsx, xls, xlsm, ods | | Delimited data | csv, tsv | | Presentation | pptx, ppt, key | | Text | txt, md, log | | Image | png, jpg, jpeg, gif, svg, webp, heic | | JSON | json | | XML | xml, html | | Archive | zip, rar, 7z, tar, gz | The source component descriptions are empty; the visible gallery annotations carry these rules. Full text and source IDs are retained in `figma-guidance.json`. --- # Flags Package: @commenda/pathfinder@0.1.0 Page: /docs/assets/flags ## Usage [#usage] ```tsx import { DZFlag } from '@commenda/pathfinder/flags'; ; ``` **ASSET-FLAG-01:** Sizes are 12, 16 (default), 20, and 24. Preserve the round crop and inside 0.5px Soft Pearl 300 hairline at every size; do not recolor the flag. **ASSET-FLAG-02:** Flags represent countries or territories, not languages. Display a country name when identification matters. Figma includes 256 codes, including `GE-AB`, `GE-OS`, and `XK`; codes are not constrained to two characters. For dynamic country data, explicitly import the registry-backed component. It includes the flag collection; use named imports when codes are known in advance. ```tsx import { CountryFlag } from '@commenda/pathfinder/flags/dynamic'; ; ``` Codes are trimmed and case-normalized. Unknown codes render a neutral circle without inventing a country identity. Provide your own localized label, such as “Country unavailable.” --- # Icons Package: @commenda/pathfinder@0.1.0 Page: /docs/assets/icons ## Usage [#usage] ```tsx import { SearchMdIcon } from '@commenda/pathfinder/icons'; ; ``` **ASSET-ICON-01:** Sizes are 12, 16 (default), 20, and 24. Use 16 beside Label/13 and small/medium controls; 20 for large controls; 12 for badges/dense tables; 24 for featured/empty states. **ASSET-ICON-02:** The 16px drawing uses an effective 1.5px stroke. The 12/20/24 variants reuse the 24px SVG where a rendered comparison verifies matching geometry. Width and height change at rendering time; redundant source copies are removed. Outlined geometry does not accept a stroke-weight override. **ASSET-ICON-03:** Default is Soft Pearl 900; `quiet` uses 800, `strong` 1000, `brand` Blue 700, and `destructive` Red 700. `inherit` takes the parent's color. Brand-colored Commenda artwork retains its own colors and has no tone prop. Figma contains 1,186 families: 1,173 Untitled UI icons, five Commenda marks, and **eight** Pathfinder additions. Its introduction still says seven additions; `stop-fill` is the extra family. The [asset reference](/assets.txt) lists every named export and supported size; `assets.json` retains source-node provenance. --- # Visual assets Package: @commenda/pathfinder@0.1.0 Page: /docs/assets Browse the collections below, search by name, and select artwork to copy its import. The [machine-readable asset reference](/assets.txt) records the complete public surface and any explicit exclusions. ## Install and render [#install-and-render] Import `@commenda/pathfinder/styles.css` once. Each collection has its own entry point. Individual components also have direct imports, such as `@commenda/pathfinder/icons/SearchMdIcon`. No application Tailwind scan, SVG loader, remote asset service, or asset-copy step is required. Artwork renders as native inline SVG. Pathfinder generates and compiles the React components at package build time; consuming apps need no SVG loader. Monochrome paths use `currentColor`; multicolor artwork preserves its source colors. Simple glyphs are server components. Artwork with masks, clipping paths, or gradients uses a small client boundary with React `useId` so repeated instances remain unique through server rendering and hydration. Original embedded raster images are retained where they are part of a supplied logo. ### Category namespaces [#category-namespaces] ```tsx import { Icons } from '@commenda/pathfinder'; ``` The categories autocomplete in TypeScript and preserve each component's props. Country names use uppercase codes; territory hyphens are removed in JSX names, for example `Icons.Flags.GEAB`. Every component's namespace is listed in the [machine-readable asset reference](/assets.txt) and `assets.json`. Static member access is tree-shakeable. Passing whole categories around or indexing them dynamically may retain that category's artwork; use `CountryFlag` from `/flags/dynamic` when the country code comes from data. ### Folder and status rules [#folder-and-status-rules] **ASSET-API-01:** Use one `Icons.Folder` component with the required `status="open" | "closed"` prop. It renders the illustrated document-folder family. Choose `size={16 | 24 | 40}`; the default is 16. Do not use the former `Icons.FileTypes.Folder` or `Icons.FileTypes.FolderOpen` aliases. Outline interface-folder glyphs are a separate family under `Icons.Interface`. **ASSET-API-02:** `Icons.Status` is a component, not a category namespace. Its required `status` is one of `to-do`, `scheduled`, `in-progress`, `in-review`, `completed`, `canceled`, `blocking`, `blocked-by`, `failed`, or `needs-attention`. Always supply a translated `label`. Use `size={12 | 16 | 20 | 24}`; the default is 16. Replace `Icons.Status.Completed` with ``. Existing named `StatusIcon` imports continue to work. ### Native sizing [#native-sizing] **ASSET-SIZE-01:** `size` selects an authored optical variant. `width` and `height` control the rendered dimensions while the source `viewBox` preserves its proportions. You do not need another downloaded file for a different display size. ```tsx ``` Native SVG attributes such as `aria-label`, `aria-labelledby`, `aria-describedby`, `className`, `style`, and event handlers are forwarded to the SVG. Event handlers belong in client components. Prefer `label` for a simple accessible name; an explicit `aria-label` takes precedence. Refs and replacement children are not part of this API. ## Accessibility rules [#accessibility-rules] - **ASSET-A11Y-01:** Omit `label` for decorative artwork beside equivalent visible text. Meaningful standalone artwork needs an explicit, localized `label`. - **ASSET-A11Y-02:** Name an icon-only button on the button. The icon stays decorative; a tooltip alone is insufficient. - **ASSET-A11Y-03:** Status and progress information must have a readable name/count. Color is never the only signal. Assets do not create focus stops. - **ASSET-SOURCE-01:** Canonical source SVGs, original checksums, and verified shared-source mappings are retained locally. Never fetch temporary Figma URLs at runtime. Do not hand-edit generated components. ## AI and provenance [#ai-and-provenance] The same pages are available through the Markdown links, `/llms.txt`, and `/llms-full.txt`. Import `@commenda/pathfinder/assets.json` to inspect the versioned catalog without importing a runtime registry. It records availability, source node IDs, sizes, export names, upstream versions, and downloaded-source checksums. Shared source references record deduplication without losing the original Figma IDs. Activity’s tooltip and motion variants are explicitly excluded from the static API. Brand marks remain their owners' trademarks. See the package's third-party notices for source licenses. The package remains UNLICENSED. --- # Logos Package: @commenda/pathfinder@0.1.0 Page: /docs/assets/logos ## Usage [#usage] ```tsx import { QuickbooksLogo } from '@commenda/pathfinder/logos/integrations'; ; ``` **ASSET-LOGO-01:** Preserve original brand colors, proportions, and raster/vector artwork. Never trace or recolor logos. Integration tiles have a white round background and inside 0.5px Soft Pearl 300 border, with authored sizes 16, 24, 32, and 40. The API defaults to 40 for integration cards. **ASSET-LOGO-02:** Payment marks use their own rectangular 46×32 or square 32×32 geometry plus 24/16 variants. Software/sign-in marks are bare. Import these from `@commenda/pathfinder/logos/payments` and `@commenda/pathfinder/logos/software`, respectively. Do not wrap an integration logo to approximate them. Figma inventories 48 integrations, 50 payments, four software/sign-in marks, and an additional `commenda-lockup` at 106×18. The lockup has its own `@commenda/pathfinder/logos/commenda` entry point. The catalog preserves each mark’s width-to-height ratio; `size` selects the authored height. The 24px rectangular payment variant has a 34.5px Figma frame; Figma exports a 35×24 SVG viewport. The renderer preserves that export width so the artwork is not squeezed. --- # Status icons and progress Package: @commenda/pathfinder@0.1.0 Page: /docs/assets/status-icons ## Usage [#usage] ```tsx import { Icons } from '@commenda/pathfinder'; ``` **ASSET-STATUS-01:** Ten statuses use sizes 12, 16 (default), 20, and 24. `to-do` is backlog; `scheduled` is planned but not started. They share a glyph, so their labels carry the distinction. `in-progress` means started; `in-review` awaits a reviewer; `completed` is done; `canceled` will not be done. **ASSET-STATUS-02:** `blocking` holds up another item; `blocked-by` waits on another item. These relationships stay neutral. `failed` needs a redo; `needs-attention` needs someone to act. In progress uses Amber 600, review Blue 600, completed Green 700, failed Red 700, attention Orange 700, and neutral states Soft Pearl 700. **ASSET-PROGRESS-01:** The semantic `ProgressRing` / `Icons.Progress.Ring` accepts any measured numeric value with an optional `max` (100 by default), or `null` for indeterminate progress. Sizes remain 16 (default), 24 and 32. The source-aligned arc sweeps clockwise from 12 o'clock; track Soft Pearl 700 at 35% opacity. Provide a name and pair with a count; `valueText` supplies a human-readable value. Explicit complete/failed states and reduced-motion-aware animation follow the [Progress contract](/docs/components/progress). The nine static `Progress0Icon` through `Progress100Icon` artwork exports remain available for static specimens. The additional Figma `activity` family contains five row-indicator states with tooltip variants and a running-motion rule. Its ten source variants are retained, but are explicitly excluded from this static status API. Its tooltip and animation behavior require a dedicated component. ## Progress library [#progress-library] --- # Accordion Package: @commenda/pathfinder@0.1.0 Page: /docs/components/accordion Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=363-2 ```tsx import { Accordion, type AccordionItem } from '@commenda/pathfinder/accordion'; const filingSections: AccordionItem[] = [ { value: 'scope', title: 'What the annual report covers', content: 'We file the Delaware annual report, pay the franchise tax and send you the state’s confirmation.', }, { value: 'due', title: 'When it is due', description: 'Delaware, due 1 March', content: 'Complete your review by 15 February so there is time to prepare the filing.', }, { value: 'details', title: 'What we need from you', badgeConfig: { label: 'Action needed', tone: 'warning' }, content: ( ), }, ]; export const AccordionDefault = () => ( ); ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` Import `@commenda/pathfinder/styles.css` once after host CSS. Pathfinder ships compiled, prefixed Tailwind v4 CSS; the application needs no Tailwind scanning. Import from `@commenda/pathfinder/accordion`. Interactive callbacks belong in a client component in Next.js App Router. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `items` | `readonly AccordionItem[]` | required | Required; value, title, content; optional description, badgeConfig, disableConfig | | `variant` | `"card" \| "divided"` | optional | divided | | `mode` | `"multiple" \| "single"` | conditional | multiple | | `value` | `readonly string[] \| string \| null` | conditional | Array in multiple mode; string/null in single mode | | `defaultValue` | `readonly string[] \| string \| null` | optional | Array in multiple mode; string/null in single mode | | `onValueChange` | `((value: readonly string[]) => void) \| ((value: string \| null) => void)` | conditional | Required with value; follows mode type | | `headingLevel` | `2 \| 3 \| 4 \| 5 \| 6` | optional | 3; choose for the surrounding document | | `panelConfig` | `{ keepMounted?: boolean; }` | optional | true; retains embedded form values | | `className` | `string` | optional | Additional classes for the component root. | | `id` | `string` | optional | Stable HTML identifier for the component. | ### panelConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `keepMounted` | `boolean` | No | | Root IDs, className, appropriate ARIA attributes, and typed refs are supported. Controlled value requires onValueChange and cannot be combined with defaultValue. Item IDs/values must be unique and stable. ## Usage rules [#usage-rules] Multiple mode permits several open sections. Single mode opens at most one and allows closing it to `null`. All start closed unless defaultValue is supplied; open the first when it contains the main information. The entire header is one button. `badgeConfig` is read-only; do not put links, checkboxes, or action buttons in the title. `disableConfig` groups isDisabled/reason. Tab visits headers in document order; Enter/Space toggles. No arrow-key roving focus. The chevron turns 180° and content height animates over 200ms, disabled with reduced motion. Keep mounted panels preserve state while their content is hidden from focus. The application opens the section containing a form error before focusing that field. Do not nest accordions. A single Show more block belongs to a Collapsible pattern. ## Examples [#examples] ### Cards [#cards] ```tsx import { Accordion, type AccordionItem } from '@commenda/pathfinder/accordion'; const filingSections: AccordionItem[] = [ { value: 'scope', title: 'What the annual report covers', content: 'We file the Delaware annual report, pay the franchise tax and send you the state’s confirmation.', }, { value: 'due', title: 'When it is due', description: 'Delaware, due 1 March', content: 'Complete your review by 15 February so there is time to prepare the filing.', }, { value: 'details', title: 'What we need from you', badgeConfig: { label: 'Action needed', tone: 'warning' }, content: ( ), }, ]; export const AccordionCards = () => ( ({ ...item, title: ['Annual filing scope', 'Filing deadline', 'Company details'][index] ?? item.title, }))} /> ); ``` ### Single [#single] ```tsx import { Accordion, type AccordionItem } from '@commenda/pathfinder/accordion'; const filingSections: AccordionItem[] = [ { value: 'scope', title: 'What the annual report covers', content: 'We file the Delaware annual report, pay the franchise tax and send you the state’s confirmation.', }, { value: 'due', title: 'When it is due', description: 'Delaware, due 1 March', content: 'Complete your review by 15 February so there is time to prepare the filing.', }, { value: 'details', title: 'What we need from you', badgeConfig: { label: 'Action needed', tone: 'warning' }, content: ( ), }, ]; export const AccordionSingle = () => ( ({ ...item, title: ['Included in the report', 'Submission date', 'Required information'][ index ] ?? item.title, }))} /> ); ``` ### Disabled [#disabled] ```tsx import { Accordion } from '@commenda/pathfinder/accordion'; export const AccordionDisabled = () => ( ); ``` The header `badgeConfig` accepts `label`, `tone`, and `indicatorConfig`. Its size is fixed to the small Badge variant so callers cannot request a size the header silently overrides. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Accordion [#accordion] Source: [363:267](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=363-267), item 363:159, docs 364:217. - `ACCORDION-01` — Source: whole header toggles; Label/13 Strong title, optional Copy/12 description and read-only badge; trailing 16 chevron turns 180°. No interactive elements inside the toggle. - `ACCORDION-02` — Source/Pathfinder: multiple open is default; single mode is for mutually exclusive sections. All closed unless initialized. Preserve form state by keeping closed content mounted by default, without leaving it focusable. - `ACCORDION-03` — Source: no nesting. Show more for one long block is a Collapsible pattern. Divided header padding 10/8, 0.5 rule, hover darkens only chevron. Card header 14/16, radius 10, section gap 8. Height/chevron transitions 200ms; reduced motion immediate. - `ACCORDION-04` — Pathfinder: heading/button/panel relationships, native Tab order, Enter/Space toggle. App opens sections containing form errors before focusing the field. disableConfig groups disabled state and its reason. Single values are string/null; multiple values are readonly string arrays. --- # Action card Package: @commenda/pathfinder@0.1.0 Page: /docs/components/action-card Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=658-33976 ```tsx import { ActionCard } from '@commenda/pathfinder/action-card'; } actionConfig={{ label: 'Connect account', onAction: connectAccount, pendingDescription: 'Finish connecting in the window that opened.', }} dismissConfig={{ onDismiss: persistDismissal }} />; ``` ## Usage [#usage] ```tsx import { ActionCard } from '@commenda/pathfinder/action-card'; } actionConfig={{ label: 'Connect account', onAction: connectAccount, pendingDescription: 'Finish connecting in the window that opened.', }} dismissConfig={{ onDismiss: persistDismissal }} />; ``` ## Inline action [#inline-action] ```tsx import { ActionCard } from '@commenda/pathfinder/action-card'; } actionConfig={{ label: 'Connect account', onAction: connectAccount, pendingDescription: 'Finish connecting in the window that opened.', }} dismissConfig={{ onDismiss: persistDismissal }} />; ``` ## Failure and retry [#failure-and-retry] ```tsx // Return { status: 'failed', message: 'Please try again.' } from the handler to retain this card. import { ActionCard } from '@commenda/pathfinder/action-card'; } actionConfig={{ label: 'Connect account', onAction: connectAccount, pendingDescription: 'Finish connecting in the window that opened.', }} dismissConfig={{ onDismiss: persistDismissal }} />; ``` Reset restores the example. Simulate failure retains the card for retry; no backend request is made. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `title` | `string` | required | Required title and optional supporting text | | `description` | `string` | optional | Required title and optional supporting text | | `artwork` | `ReactElement>` | required | Decorative artwork rendered in the existing 40px Avatar | | `actionConfig` | `{ label: string; onAction: () => ActionResult \| Promise; pendingDescription?: string; }` | required | Handler returns `{ status: 'completed' }` or `{ status: 'failed', message }`, synchronously or asynchronously | | `dismissConfig` | `{ label?: string; onDismiss: () => void; }` | optional | Optional secondary action, defaults to Not now; persists permanent dismissal | | `state` | `"ready" \| "pending" \| "completed" \| "dismissed"` | optional | Host status; local pending/completion also tracked | | `placement` | `"docked" \| "inline"` | optional | Docked radius16/raised, inline radius10/flat | | `className` | `string` | optional | Layout override | ### actionConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | Yes | | | `onAction` | `() => ActionResult \| Promise` | Yes | | | `pendingDescription` | `string` | No | | ### dismissConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | No | | | `onDismiss` | `() => void` | Yes | | ## Design rules [#design-rules] - **ACTION-01:** Optional next action outside the blocking request flow. The frame is not a click target; only buttons activate. - **ACTION-02:** One primary action and at most one secondary dismissal. Loading retains the action label with a spinner. Explain external-window work in the pending description when relevant. - **ACTION-03:** Completion or dismissal removes the card. The host persists that outcome. No automatic agent turn, success toast, or promised follow-up. Historical cards remain usable until acted on or dismissed. Return completion only when the action succeeds, not merely when an external window opens. If a blocked popup fails, return a failure message. Use a new React key for a new action, and store its completion/dismissal independently from conversational requests. ## Integration and accessibility [#integration-and-accessibility] Buttons have explicit names and keyboard focus. No network transport or AI SDK provider is required. Requests, tool execution, permission checks, durable outcomes and composer coordination stay in the application. See [AI content integration](/docs/guides/ai-content) for migration mappings; application migration is outside this build. --- # Address input Package: @commenda/pathfinder@0.1.0 Page: /docs/components/address-input Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=286-2 ```tsx import { AddressInput } from '@commenda/pathfinder/address-input'; export const AddressInputDefault = () => ( ); ``` ## Installation [#installation] Import the component in your screen: ```tsx import { AddressInput, type AddressValue, } from '@commenda/pathfinder/address-input'; ``` Load the stylesheet once in your app root, after host styles: ```tsx import '@commenda/pathfinder/styles.css'; ``` Pathfinder ships precompiled CSS; no Tailwind setup is required. In Next.js App Router, put interactive state and callbacks in a client component. See [installation](/docs/getting-started/installation) for the workspace setup. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `label` | `string` | conditional | Accessible group name; one is required | | `aria-label` | `string` | conditional | Accessible group name; one is required | | `hideLabel` | `boolean` | optional | Visually hides the group label | | `value` | `AddressValue` | conditional | Controlled or uncontrolled complete address; defaults to empty fields | | `defaultValue` | `AddressValue` | optional | Controlled or uncontrolled complete address; defaults to empty fields | | `onValueChange` | `((value: AddressValue) => void)` | conditional | Complete value on every edit; required with `value` | | `countryConfig` | `AddressCountryConfig` | optional | Defaults to Kakashi's 199 named supported countries | | `regionConfig` | `{ options: readonly ChoiceOption[]; }` | optional | Overrides the country-based searchable state/region selector | | `suggestionsConfig` | `AddressSuggestionsConfig` | optional | Optional address lookup; manual entry always remains available | | `requiredFields` | `readonly ("country" \| "addressLine1" \| "city" \| "region" \| "postalCode")[]` | optional | Per-field native required validation; line 2 is always optional | | `fieldErrors` | `Partial>` | optional | Field-specific messages displayed immediately | | `disableConfig` | `{ isDisabled: boolean; reason?: string; }` | optional | Disables all fields and excludes them from native submission | | `readOnly` | `boolean` | optional | Preserves readable, copyable, submittable values | | `description` | `string` | optional | Group help text | | `id` | `string` | optional | Group ID, field-name prefix, and native form association | | `name` | `string` | optional | Group ID, field-name prefix, and native form association | | `form` | `string` | optional | Group ID, field-name prefix, and native form association | | `className` | `string` | optional | Root styling and additional accessible description | | `aria-describedby` | `string` | optional | Root styling and additional accessible description | ### value fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `country` | `string` | Yes | | | `addressLine1` | `string` | Yes | | | `addressLine2` | `string` | Yes | | | `city` | `string` | Yes | | | `region` | `string` | Yes | | | `postalCode` | `string` | Yes | | ### defaultValue fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `country` | `string` | Yes | | | `addressLine1` | `string` | Yes | | | `addressLine2` | `string` | Yes | | | `city` | `string` | Yes | | | `region` | `string` | Yes | | | `postalCode` | `string` | Yes | | ### countryConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `allowedCountries` | `readonly ("AD" \| "AE" \| "AF" \| "AG" \| "AL" \| "AM" \| "AO" \| "AR" \| "AT" \| "AU" \| "AZ" \| "BA" \| "BB" \| "BD" \| "BE" \| "BF" \| "BG" \| "BH" \| "BI" \| "BJ" \| "BN" \| "BO" \| "BR" \| "BS" \| "BT" \| "BW" \| "BY" \| "BZ" \| "CA" \| "CD" \| "CF" \| "CG" \| "CH" \| "CI" \| "CK" \| "CL" \| "CM" \| "CN" \| "CO" \| "CR" \| "CU" \| "CV" \| "CY" \| "CZ" \| "DE" \| "DJ" \| "DK" \| "DM" \| "DO" \| "DZ" \| "EC" \| "EE" \| "EG" \| "ER" \| "ES" \| "ET" \| "FI" \| "FJ" \| "FM" \| "FR" \| "GA" \| "GB" \| "GD" \| "GE" \| "GH" \| "GM" \| "GN" \| "GQ" \| "GR" \| "GT" \| "GW" \| "GY" \| "HN" \| "HR" \| "HT" \| "HU" \| "ID" \| "IE" \| "IL" \| "IN" \| "IQ" \| "IR" \| "IS" \| "IT" \| "JE" \| "JM" \| "JO" \| "JP" \| "KE" \| "KG" \| "KH" \| "KI" \| "KM" \| "KN" \| "KR" \| "KW" \| "KY" \| "KZ" \| "LA" \| "LB" \| "LC" \| "LI" \| "LK" \| "LR" \| "LS" \| "LT" \| "LU" \| "LV" \| "LY" \| "MA" \| "MC" \| "MD" \| "ME" \| "MG" \| "MH" \| "MK" \| "ML" \| "MM" \| "MN" \| "MR" \| "MT" \| "MU" \| "MV" \| "MW" \| "MX" \| "MY" \| "MZ" \| "NA" \| "NE" \| "NG" \| "NI" \| "NL" \| "NO" \| "NP" \| "NR" \| "NZ" \| "OM" \| "PA" \| "PE" \| "PG" \| "PH" \| "PK" \| "PL" \| "PR" \| "PT" \| "PW" \| "PY" \| "QA" \| "RO" \| "RS" \| "RU" \| "RW" \| "SA" \| "SB" \| "SC" \| "SD" \| "SE" \| "SG" \| "SI" \| "SK" \| "SL" \| "SM" \| "SN" \| "SO" \| "SR" \| "SS" \| "ST" \| "SV" \| "SY" \| "SZ" \| "TD" \| "TG" \| "TH" \| "TJ" \| "TL" \| "TM" \| "TN" \| "TO" \| "TR" \| "TT" \| "TV" \| "TW" \| "TZ" \| "UA" \| "UG" \| "US" \| "UY" \| "UZ" \| "VA" \| "VC" \| "VE" \| "VG" \| "VN" \| "VU" \| "WS" \| "YE" \| "ZA" \| "ZM" \| "ZW")[]` | No | | | `excludedCountries` | `readonly ("AD" \| "AE" \| "AF" \| "AG" \| "AL" \| "AM" \| "AO" \| "AR" \| "AT" \| "AU" \| "AZ" \| "BA" \| "BB" \| "BD" \| "BE" \| "BF" \| "BG" \| "BH" \| "BI" \| "BJ" \| "BN" \| "BO" \| "BR" \| "BS" \| "BT" \| "BW" \| "BY" \| "BZ" \| "CA" \| "CD" \| "CF" \| "CG" \| "CH" \| "CI" \| "CK" \| "CL" \| "CM" \| "CN" \| "CO" \| "CR" \| "CU" \| "CV" \| "CY" \| "CZ" \| "DE" \| "DJ" \| "DK" \| "DM" \| "DO" \| "DZ" \| "EC" \| "EE" \| "EG" \| "ER" \| "ES" \| "ET" \| "FI" \| "FJ" \| "FM" \| "FR" \| "GA" \| "GB" \| "GD" \| "GE" \| "GH" \| "GM" \| "GN" \| "GQ" \| "GR" \| "GT" \| "GW" \| "GY" \| "HN" \| "HR" \| "HT" \| "HU" \| "ID" \| "IE" \| "IL" \| "IN" \| "IQ" \| "IR" \| "IS" \| "IT" \| "JE" \| "JM" \| "JO" \| "JP" \| "KE" \| "KG" \| "KH" \| "KI" \| "KM" \| "KN" \| "KR" \| "KW" \| "KY" \| "KZ" \| "LA" \| "LB" \| "LC" \| "LI" \| "LK" \| "LR" \| "LS" \| "LT" \| "LU" \| "LV" \| "LY" \| "MA" \| "MC" \| "MD" \| "ME" \| "MG" \| "MH" \| "MK" \| "ML" \| "MM" \| "MN" \| "MR" \| "MT" \| "MU" \| "MV" \| "MW" \| "MX" \| "MY" \| "MZ" \| "NA" \| "NE" \| "NG" \| "NI" \| "NL" \| "NO" \| "NP" \| "NR" \| "NZ" \| "OM" \| "PA" \| "PE" \| "PG" \| "PH" \| "PK" \| "PL" \| "PR" \| "PT" \| "PW" \| "PY" \| "QA" \| "RO" \| "RS" \| "RU" \| "RW" \| "SA" \| "SB" \| "SC" \| "SD" \| "SE" \| "SG" \| "SI" \| "SK" \| "SL" \| "SM" \| "SN" \| "SO" \| "SR" \| "SS" \| "ST" \| "SV" \| "SY" \| "SZ" \| "TD" \| "TG" \| "TH" \| "TJ" \| "TL" \| "TM" \| "TN" \| "TO" \| "TR" \| "TT" \| "TV" \| "TW" \| "TZ" \| "UA" \| "UG" \| "US" \| "UY" \| "UZ" \| "VA" \| "VC" \| "VE" \| "VG" \| "VN" \| "VU" \| "WS" \| "YE" \| "ZA" \| "ZM" \| "ZW")[]` | No | | ### regionConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `options` | `readonly ChoiceOption[]` | Yes | | ### suggestionsConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | `readonly AddressSuggestion[]` | Yes | | | `isLoading` | `boolean` | Yes | | | `error` | `string` | No | | | `resultsQuery` | `string` | Yes | Query that produced the supplied items; stale results cannot be selected. | | `onQueryChange` | `(query: string) => void` | Yes | | ### disableConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isDisabled` | `boolean` | Yes | | | `reason` | `string` | No | | `AddressValue` contains six strings: `country` (ISO code), `addressLine1`, `addressLine2`, `city`, `region`, and `postalCode`. A `name="address"` produces fields named `address.country`, `address.addressLine1`, and so on. Uncontrolled values restore their defaults on form reset. ## Countries [#countries] ```tsx import { AddressInput } from '@commenda/pathfinder/address-input'; export const AddressInputCountries = () => ( ); ``` Pathfinder's available country codes and labels are bundled at build time. United States, Canada, India, United Kingdom, Singapore, United Arab Emirates, and Ireland appear first, followed by other countries sorted alphabetically by label. `None`, `Other`, and entries without a label are omitted. Empty `allowedCountries` means all available countries; exclusions always apply. Filtering preserves the canonical order. Pathfinder owns the supported countries, labels, priority order, and region exclusions in `packages/pathfinder/data/address-countries.json`. Run `pnpm --filter @commenda/pathfinder generate:address-countries` after changing this file. Regional metadata comes from Pathfinder's pinned `country-state-city` build dependency. Generation does not read another workspace package. ## Address suggestions [#address-suggestions] ```tsx import { useState } from 'react'; import { AddressInput, type AddressValue, } from '@commenda/pathfinder/address-input'; export const AddressInputSuggestions = () => { const [query, setQuery] = useState(''); const address: AddressValue = { country: 'US', addressLine1: '350 Fifth Avenue', addressLine2: '', city: 'New York', region: 'NY', postalCode: '10118', }; return ( ); }; ``` The example uses local demonstration data. Production applications supply `suggestionsConfig` with `items`, `isLoading`, `resultsQuery`, `onQueryChange`, and optional `error`. Each suggestion is `{ id, label, description?, address: AddressValue }`. The application owns requests, debouncing, cancellation, and credentials. Set `resultsQuery` to the query that produced the current items, not the latest input before its request finishes. Stale results are hidden until their query matches the typed street address. Selecting a suggestion fills all six fields. Suggestions outside the allowed country list cannot be selected. Search failure never prevents manual entry. ## Validation [#validation] ```tsx import { AddressInput } from '@commenda/pathfinder/address-input'; export const AddressInputStates = () => ( ); ``` Country changes preserve all entered fields for review. State and region options default to the pinned country-state-city metadata, including Pathfinder's US territory exclusions. Countries with no metadata allow free-text entry. Applications can override the options with `regionConfig`; country-specific validation remains application-owned. Changing a country does not silently erase an address or select a region. ## Design and implementation rules [#design-and-implementation-rules] - **ADDRESS-001:** Figma 292:9094 and 293:3032 establish the compact field treatment. Keep the approved stable order: country, street, optional line 2, city, region, postal code. Narrow layouts stack the fields. - **ADDRESS-002:** Suggestions overlay the street field without moving the form. Manual editing remains available when search fails. - **ADDRESS-003:** Preserve Pathfinder country availability, labels, and priority/alphabetical ordering. There is no private runtime dependency. - **ADDRESS-004:** Preserve drafts on country changes; default regions follow the bundled metadata and application options override them. The application owns validation and requests. - **ADDRESS-005:** Line 2 cannot be required. Preserve native autocomplete and form association for each field. - **ADDRESS-006:** Only results matching the current street query can be selected. These rules also live alongside the component implementation and are available through the Markdown endpoint, `llms.txt`, and `llms-full.txt`. ### Shared rules [#shared-rules] Labels activate the first input. Descriptions, disabled reasons, and validation messages stay associated with the field. Error icons center on the first 18px text line, including wrapped messages. Controlled values require callbacks; uncontrolled form resets restore defaults. Public props expose application values, never private primitive or calendar objects. Styles ship precompiled for React 18/19 and Next.js consumers. --- # Alert Package: @commenda/pathfinder@0.1.0 Page: /docs/components/alert Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=299-2 ```tsx import { Alert } from '@commenda/pathfinder/alert'; ; ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` Import `@commenda/pathfinder/styles.css` once after host CSS. Styles are precompiled with Tailwind v4; consumers need no Tailwind scanning. Interactive callbacks belong in client components in Next.js App Router. ## Usage [#usage] ```tsx import { Alert } from '@commenda/pathfinder/alert'; ; ``` ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `title` | `string` | conditional | At least one required | | `description` | `string` | conditional | At least one required | | `tone` | `FeedbackTone` | optional | neutral | | `actionConfig` | `FeedbackAction` | optional | Secondary button | | `secondaryActionConfig` | `FeedbackAction` | optional | Optional link | | `dismissConfig` | `DismissConfig` | optional | Omit for a persistent callout | | `open` | `boolean` | conditional | Controlled / uncontrolled; true initially | | `defaultOpen` | `boolean` | optional | Controlled / uncontrolled; true initially | | `onOpenChange` | `((open: boolean) => void)` | conditional | Required with open; requests closure | | `announcement` | `FeedbackAnnouncement` | optional | auto maps tone to live-region priority | | `className` | `string` | optional | Additional classes for the component root. | ### dismissConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | No | | | `onDismiss` | `(() => void)` | No | | Never auto-dismiss. Title says what happened; description says what to do. Keep copy concise without truncating essential instructions. Field errors remain on fields; an alert may summarize them. onOpenChange requests closure immediately; onDismiss runs after the 200 ms collapse only when the owner allows closure. The application owns focus restoration after removing a focused alert. ### Actions [#actions] `FeedbackAction` requires a label and exactly one of `onClick` (command) or `href` (native navigation). Commands support `disabled`, `disabledReason`, `loadingConfig`, and `leadingIcon`; navigation supports `target`, `rel`, and `leadingIcon`. The application owns async work and retry state. ## Examples [#examples] ### Tones [#tones] ```tsx <> {(['neutral', 'info', 'success', 'warning', 'error'] as const).map((tone) => ( ))} ; ``` ### Actions [#actions-1] ```tsx ; ``` ### Dismissible [#dismissible] ```tsx ; ``` Read the [design guidance](/docs/components/alert#design-rules) before using these components. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Alert and Banner — 299:2 [#alert-and-banner--2992] - **ALERT-01:** Persistent, contextual feedback beside the form, table or figure it concerns. Never auto-dismiss. No dismissConfig produces a callout that stays until the owner removes it. Field errors remain on fields; form alerts summarize them. - **ALERT-02:** Title states what happened without a period; description states the next step. Aim for two lines, but never clip essential text. At most a secondary small Button for recovery plus a link for more information. Passing confirmations use Toast. - **ALERT-03:** Neutral is a state, info is news, success is completion, warning is an approaching issue, error is a current problem. Auto announcement maps warnings/errors to alert and info/success to status; neutral stays nonurgent. The explicit announcement prop can override this for the actual event. - **ALERT-04:** Tinted background and 0.5 px border, radius 8, padding 12 vertical/16 start/12 end, gap 10; icon 16, Label/13 Strong title, Copy/13 body, gap 2. Close uses plain IconButton xs on tinted surfaces. - **BANNER-01:** Whole-account or product context only; place beneath the breadcrumb across the content card. One at a time, one concise message, one link. The application owns placement and prioritization. Wrap on narrow screens rather than truncate. - **BANNER-02:** Announcement is the sixth banner tone from node 917:314: blue 100/background, blue 300 bottom border, centered blue 900 copy, no icon or title, optional secondary small Button. - **FEEDBACK-DISMISS-01:** Dismissal fades and collapses space over 200 ms, ease-out. onOpenChange requests closure immediately; onDismiss follows the collapse after an accepted close. A controlled owner can reject the request. The owner restores focus if needed. - **FEEDBACK-ACTION-01:** Commands use Button; navigation uses native anchors. Require a label and exactly one of onClick or href. Components choose action appearance; callers own loading, disabled reasons and async behavior. --- # Approval card Package: @commenda/pathfinder@0.1.0 Page: /docs/components/approval-card Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=607-4660 ```tsx import { ApprovalCard } from '@commenda/pathfinder/approval-card'; temporarilyHide(request.id)} />; ``` ## Usage [#usage] ```tsx import { ApprovalCard } from '@commenda/pathfinder/approval-card'; temporarilyHide(request.id)} />; ``` ## Inline approval [#inline-approval] ```tsx import { ApprovalCard } from '@commenda/pathfinder/approval-card'; temporarilyHide(request.id)} />; ``` ## Failure and retry [#failure-and-retry] ```tsx // Return { status: 'failed', message: 'Please try again.' } from the handler to retain this card. import { ApprovalCard } from '@commenda/pathfinder/approval-card'; temporarilyHide(request.id)} />; ``` ## Loading [#loading] ```tsx import { ApprovalCard } from '@commenda/pathfinder/approval-card'; temporarilyHide(request.id)} />; ``` Reset restores the example. Simulate failure retains the card for retry; no backend request is made. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `consequence` | `string` | required | Required concrete effect of approval; explain reversibility in `description` | | `approveConfig` | `{ label: string; onApprove: RequestHandler<{ alwaysAllow: boolean; }>; destructive?: boolean; }` | required | Explicit verb, handler receiving `{ alwaysAllow: boolean }`, optional destructive styling | | `onDeny` | `RequestHandler` | required | Separate denial callback; returns accepted receipt or failure | | `denyLabel` | `string` | optional | Defaults to Deny | | `detailsConfig` | `{ label?: string; onOpen: () => void; }` | optional | Optional host-owned detail view | | `alwaysAllowConfig` | `{ label: string; value: boolean; onValueChange: (value: boolean) => void; }` | optional | Explicit controlled permission, omitted unless supported by the host | | `title` | `string` | required | | | `description` | `string` | optional | Supporting guidance associated with the field. | | `placement` | `"docked" \| "inline"` | optional | | | `state` | `RequestState` | optional | | | `onDismiss` | `(() => void)` | optional | | | `className` | `string` | optional | Additional classes for the component root. | ### approveConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | Yes | | | `onApprove` | `RequestHandler<{ alwaysAllow: boolean; }>` | Yes | | | `destructive` | `boolean` | No | | ### detailsConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | No | | | `onOpen` | `() => void` | Yes | | ### alwaysAllowConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | Yes | | | `value` | `boolean` | Yes | | | `onValueChange` | `(value: boolean) => void` | Yes | | ### state fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `"ready" \| "pending" \| "loading"` | Yes | | #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `"resolved"` | Yes | | | `recap` | `ResponseRecapProps` | Yes | | ### Shared request props [#shared-request-props] | Prop | Type | Behavior | | ------------- | ---------------------- | ------------------------------------------------------------------------ | | `title` | `string` | Required card title. | | `description` | `string` | Optional supporting text. | | `placement` | `'docked' \| 'inline'` | Docked by default; parent controls width and placement | | `state` | `RequestState` | Ready, loading skeleton, externally pending, or resolved with a recap | | `onDismiss` | `() => void` | Temporary dismissal via Not now or scoped Escape; not a submitted answer | | `className` | `string` | Layout override | `RequestHandler` returns `{ status: 'accepted', recap: ResponseRecapProps }` or `{ status: 'failed', message: string }`, directly or as a Promise. A thrown error shows a retry message and preserves the draft. The host persists accepted responses; the component displays the returned receipt. `state={{ status: 'resolved', recap }}` restores history. Use a stable request ID as the React key, and change the key when the request changes. External pending is a rendering state; authorization and idempotency remain server responsibilities. **REQUEST-01:** Docked tray uses pearl100, a raised shadow, radius16 and a white radius10 inset with 16px padding. Header40, footer at least44; controls wrap on narrow screens. **REQUEST-02:** The parent places the request 12px above Composer at the same width. Inline requests are flat and fill their message column. Escape only temporarily dismisses; no global listener is installed. **REQUEST-03:** Reading content does not animate. Existing Button and Popover motion respects reduced motion. Pending retains the draft and exposes a polite status. Acceptance never precedes the host result. ## Design rules [#design-rules] - **APPROVAL-01:** Show the action, its consequence, and reversibility before requesting consent. No numeric/global approval shortcut. - **APPROVAL-02:** Deny and approval are distinct explicit outcomes. Escape and Not now temporarily dismiss; they never deny or approve. - **APPROVAL-03:** Always allow starts from the host's actual permission state. Accepted approval and denial receipts use `kind: 'approval'` with actor and timestamp. The server owns authorization and must validate the request again before executing it. A typed composer reply while approval is active follows the Figma denial path. Implement that mapping in the application orchestrator, not as an arbitrary keyboard handler on ApprovalCard. Do not translate temporary dismissal, network failure, or a closed browser tab into consent. ## Integration and accessibility [#integration-and-accessibility] Buttons have explicit names and keyboard focus. No network transport or AI SDK provider is required. Requests, tool execution, permission checks, durable outcomes and composer coordination stay in the application. See [AI content integration](/docs/guides/ai-content) for migration mappings; application migration is outside this build. --- # Attachment group Package: @commenda/pathfinder@0.1.0 Page: /docs/components/attachment-group Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=569-26690 ```tsx import { AttachmentGroup } from '@commenda/pathfinder/attachment-group'; ; ``` ## Usage [#usage] ```tsx import { AttachmentGroup } from '@commenda/pathfinder/attachment-group'; ({ content: })} />; ``` The group measures actual pill widths and reserves room for +N within the last permitted row. It recomputes after container resize, font loading, or item changes. IDs must be unique and stable. Input order is preserved; grouping files into folder records is application-owned. ## Sent-message alignment [#sent-message-alignment] `align="end"` aligns toward the inline end and suppresses all removal controls, including overflow checkboxes. Preview actions continue to work. Place the group 6px above the message bubble in the conversation composition. ```tsx ; ``` ## Overflow behavior [#overflow-behavior] +N opens a named Popover listing hidden attachments. Uncheck an item to request removal in an editable group; select its pill to preview or retry. This uses a Popover with Checkbox rather than the action-only Dropdown so selection and preview remain distinct controls. The app updates `items` after removal; if it rejects the request, the attachment remains selected. The initial server render provides +N access; measurement takes place before the client paints. Hidden measurement labels are excluded from the accessibility tree and contain no focusable controls. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `items` | `readonly AttachmentItem[]` | required | Required; empty arrays render nothing | | `align` | `"start" \| "end"` | optional | Start default; end is a non-removable sent group | | `maxRows` | `1 \| 2 \| 3` | optional | Default 3; +N stays inside this row budget | | `onPreview` | `((item: AttachmentItem) => void)` | optional | Same lifecycle callbacks as Attachment | | `onRetry` | `((item: AttachmentItem) => void)` | optional | Same lifecycle callbacks as Attachment | | `onRemove` | `((item: AttachmentItem) => void)` | optional | Same lifecycle callbacks as Attachment | | `getPreview` | `((item: AttachmentItem) => AttachmentProps["previewConfig"])` | optional | Optional per-item hover/focus preview | | `label` | `string` | optional | Accessible group name; defaults to Attachments | | `className` | `string` | optional | Outer layout | ## Design rules [#design-rules] - **ATTACH-GROUP-01:** Figma 569:26690. Wrap with a 6px gap, cap at three rows, and expose every remaining item through +N. Never clip a fourth row of keyboard-focusable items. - **ATTACH-GROUP-02:** Composer alignment enables removal when supplied; sent alignment removes editing controls but retains preview. - Retain failed items and their retry action. Width calculations use original labels and update when item state changes. ## Migration notes [#migration-notes] Existing `{ key, fileName, mimeType, fileRef }` arrays map to AttachmentItem without changing persisted data. Keep existing folder aggregation before the group. Overflow, measurement, and keyboard management are handled inside Pathfinder. --- # Attachment Package: @commenda/pathfinder@0.1.0 Page: /docs/components/attachment Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=569-26294 ```tsx import { Attachment } from '@commenda/pathfinder/attachment'; ; ``` ## Usage [#usage] ```tsx import { Attachment } from '@commenda/pathfinder/attachment'; ; ``` The application owns uploads, cancellation, retry, storage URLs, and viewer content. Callbacks receive the original item with its stable ID. Updating an item's `upload` value changes the pill without replacing its identity. ## Upload lifecycle [#upload-lifecycle] Use `upload: { status: 'uploading', progress: 42 }` for measured progress, `progress: null` for indeterminate progress, and `{ status: 'failed', error: 'Connection lost' }` for failure. Omit `upload`, or set its status to `ready`, after success. Failed uploads remain visible; clicking retries only when `onRetry` is supplied. Set the uploading state promptly when retry begins. ## Sent attachments [#sent-attachments] Omit `onRemove` after sending. Hover previews and the main preview action remain available. Images use the image file-type icon in the pill; the actual picture belongs in the preview. ```tsx ; ``` ## Preview composition [#preview-composition] `previewConfig={{ content: , isLoading }}` supplies the hover/focus preview. `onPreview` can open Pathfinder's Sheet or your existing viewer. No PDF viewer, fetcher, or file URL resolver is bundled. The example uses a Sheet with app-supplied content. ## Validate before upload [#validate-before-upload] ```tsx import { AttachmentValidation } from '@commenda/pathfinder/attachment'; const result = AttachmentValidation.validate( selectedFiles, { accept: ['.pdf', 'image/*'], maxBytes: 10_000_000, maxCount: 10 }, existingAttachments.length ); // Start uploads for result.accepted. // Summarize result.rejected in one application-owned Toast. ``` Validation preserves file objects and selection order. Rejections contain `file` and `reasons` (`type`, `size`, `count`). Extensions are case-insensitive, size limits are inclusive, and count includes existing attachments plus accepted candidates. File extensions can be checked when browsers omit MIME metadata. This is pre-upload feedback; the server must enforce its own upload constraints. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `item` | `AttachmentItem` | required | Required stable `id` and original `filename`; optional `mediaType`, `url`, and byte `size` | | `onPreview` | `((item: AttachmentItem) => void)` | optional | Ready pill activation; host opens its viewer | | `onRetry` | `((item: AttachmentItem) => void)` | optional | Failed pill activation; host performs retry | | `onRemove` | `((item: AttachmentItem) => void)` | optional | Separate remove control, including during uploads | | `previewConfig` | `{ content: ReactNode; isLoading?: boolean; }` | optional | Optional host-owned preview content | | `className` | `string` | optional | Outer pill styling | ### previewConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `content` | `ReactNode` | Yes | | | `isLoading` | `boolean` | No | | ## Design and accessibility rules [#design-and-accessibility-rules] - **ATTACH-01:** Figma 569:26294. Height 28px, radius 6px, 0.5px pearl-300 border, Label/12 Strong. Keep the original filename and extension; truncate at 240px with full name available to assistive technology and native hover text. - **ATTACH-02:** Uploading replaces the file icon with ProgressRing. Failure uses StatusFailed, a 1px red-300 border and red-800 text. Keep failures in the list; a Toast must not replace them. - **ATTACH-03:** Removal is a separate named button. The host cancels outstanding upload work and ignores stale completion results after removal. - **ATTACH-04:** These are user-added inputs. Agent-returned files are artifacts and belong to the output components in a later phase. - Preview and retry use native buttons with keyboard activation. Removal never triggers preview. Existing Popover and Sheet primitives supply focus and reduced-motion behavior. ## Migration notes [#migration-notes] Map existing `key` to `id`, `fileName` to `filename`, and `mimeType` to `mediaType`. Keep opaque `fileRef` resolution inside your `onPreview` or preview-content adapter; do not treat it as a public URL. Existing per-item callbacks can be delegated by ID. Actual consumer migration is out of scope. --- # Avatar Package: @commenda/pathfinder@0.1.0 Page: /docs/components/avatar Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=408-2 ```tsx import { Avatar } from '@commenda/pathfinder/avatar'; export const AvatarDefault = () => ( ); ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` Import `@commenda/pathfinder/styles.css` once after host CSS. Pathfinder ships compiled, prefixed Tailwind v4 CSS; the application needs no Tailwind scanning. Import from `@commenda/pathfinder/avatar`. Interactive callbacks belong in a client component in Next.js App Router. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `identity` | `AvatarIdentity` | required | Required discriminated person, organization, or logo object | | `size` | `AvatarSize` | optional | 24 | | `label` | `string` | optional | Optional accessible-name override/localized fallback | | `decorative` | `boolean` | optional | false; true suppresses name and tooltip | | `tooltipConfig` | `false \| { label?: string; }` | optional | Name tooltip enabled; false when visible copy already names it | | `className` | `string` | optional | Additional classes for the component root. | | `id` | `string` | optional | Stable HTML identifier for the component. | ### identity fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `kind` | `"person"` | Yes | | | `userId` | `string` | No | | | `name` | `string` | No | | | `email` | `string` | No | | | `src` | `string` | No | | #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `kind` | `"organization"` | Yes | | | `name` | `string` | Yes | | | `src` | `string` | No | | #### Option 3 | Field | Type | Required | Description | | --- | --- | --- | --- | | `kind` | `"logo"` | Yes | | | `label` | `string` | Yes | | | `artwork` | `ReactElement>` | Yes | | ### tooltipConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | No | | Root IDs, className, appropriate ARIA attributes, and typed refs are supported. ## Usage rules [#usage-rules] ### Identity structure [#identity-structure] ```ts type AvatarIdentity = | { kind: 'person'; userId?: string; name?: string; email?: string; src?: string; } | { kind: 'organization'; name: string; src?: string } | { kind: 'logo'; label: string; artwork: ReactElement }; ``` Person: photo → initials → neutral person icon. Initials use name, then email local-part; grapheme-aware extraction preserves emoji and non-Latin names. Size 16 shows one initial; other sizes show up to two. Image failure keeps the same dimensions and reveals the fallback. Tone comes from the stable user ID, never name/email. UTF-8 FNV-1a modulo eight chooses clay, apricot, ochre, olive, sage, dusk, plum, or rose using existing token pairs. Without an ID the fallback is neutral. The accessible fallback is “Unknown person”; label can localize it. Organizations use a neutral plate, logos use supplied artwork, legal entities use flags. Commenda's existing short-form mark is `Icons.Interface.CommendaBrand`. ### AvatarGroup [#avatargroup] | Prop | Type | Default / behavior | | -------------- | -------------------------------------------------------- | --------------------------------- | | label | string | Required group name | | people | `readonly { id; name?; email?; src? }[]` | Stable id also determines tone | | size | 20 or 24 or 32 | 24 | | surface | white or canvas | white; determines separating ring | | overflowConfig | `{ label?(count): string; formatNames?(names): string }` | Localize disclosure and name list | | unknownLabel | string | Unknown person | Three visible people, then +N. Names remain available on hover/focus, and the overflow supports tap. Escape or clicking outside closes the disclosure. Earlier people sit above later ones with a 1.5px surface ring. Empty groups render no fake members; hidden people must be supplied, not just a total count. Use a full people list elsewhere for very large groups. ## Examples [#examples] ### Sizes [#sizes] ```tsx import { Avatar } from '@commenda/pathfinder/avatar'; export const AvatarSizes = () => (
{([16, 20, 24, 32, 40, 64] as const).map((size) => ( ))}
); ``` ### Types [#types] ```tsx import { Icons } from '@commenda/pathfinder'; import { Avatar } from '@commenda/pathfinder/avatar'; export const AvatarTypes = () => (
, }} size={40} />
); ``` ### Groups [#groups] ```tsx import { AvatarGroup } from '@commenda/pathfinder/avatar'; const people = [ { id: 'priya', name: 'Priya Shah' }, { id: 'jan', name: 'Jan Kowalski' }, { id: 'mei', name: 'Mei Tan' }, { id: 'ana', name: 'Ana Silva' }, { id: 'sara', name: 'Sara Ali' }, ]; export const AvatarGroups = () => (
{([20, 24, 32] as const).map((size) => ( ))}
); ``` ### Fallback [#fallback] ```tsx import { Avatar } from '@commenda/pathfinder/avatar'; export const AvatarFallback = () => ( ); ``` ## Photo [#photo] An image loads inside the same fixed-size frame, retaining initials if loading fails. The demo portrait is a local copy of the sample provided in Figma (identified there as an Unsplash free-license image). Replace the example URL with your application's image URL. ```tsx import { Avatar } from '@commenda/pathfinder/avatar'; const portrait = new URL('./images/portrait.png', import.meta.url).href; export const AvatarPhoto = () => ( ); ``` ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Avatar and AvatarGroup [#avatar-and-avatargroup] Source: [412:74](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=412-74), group 412:12893, palette 533:26418, docs 414:485. - `AVATAR-01` — Source: people circular; organization/logo rounded plate, radii5/6/8/10/12/20 for sizes16/20/24/32/40/64. Legal entities use flags. Identity is a person/organization/logo union. Reuse the existing CommendaBrand icon for the short-form mark. - `AVATAR-02` — Source/Pathfinder: stable user-ID FNV-1a modulo8; UTF-8 and unsigned32 arithmetic. Tone order clay/apricot/ochre/olive/sage/dusk/plum/rose; reuse token pairs. No random tone or name/email hash. Missing user ID is neutral. - `AVATAR-03` — Source/Pathfinder: photo, then name/email initials, then neutral person. Grapheme-safe uppercase initials, one at16 and up to two otherwise. Dimensions stay reserved on image failure. Logo slots are10/12/14/18/22/36 at the six sizes, on white; neutral-person artwork is12/12/16/16/20/33.6. Full accessible name or localized fallback; decorative avatars suppress tooltip and repeated naming. - `AVATAR-04` — Source: show three people then +N; tooltip names hidden people; group sizes20/24/32. Earlier people stack above later ones. 1.5 surface-color rings separate overlapping avatars. Do not supply count without identities. - `AVATAR-05` — Pathfinder: names reachable on focus; overflow supports touch tap, outside press, and Escape. Localize overflow label/name list and unknown identity copy. No action semantics on ordinary avatars. Large groups need a full people list in the owning interface. ### Source discrepancies and implementation decisions [#source-discrepancies-and-implementation-decisions] --- # Badge Package: @commenda/pathfinder@0.1.0 Page: /docs/components/badge Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=240-2 ```tsx import { Badge } from '@commenda/pathfinder/badge'; export const BadgeDefault = () => ; ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` Import `@commenda/pathfinder/styles.css` once after host CSS. Pathfinder ships compiled, prefixed Tailwind v4 CSS; the application needs no Tailwind scanning. Import from `@commenda/pathfinder/badge`. Interactive callbacks belong in a client component in Next.js App Router. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `label` | `string` | required | Required; conveys meaning independently of color | | `tone` | `BadgeTone` | optional | neutral | | `size` | `"sm" \| "md"` | optional | sm: 20 high; md: 24 high | | `indicatorConfig` | `{ type: "dot"; } \| { type: "status"; status: StatusKind; }` | optional | Optional; dot and status icon are mutually exclusive | | `className` | `string` | optional | Additional classes for the component root. | | `id` | `string` | optional | Stable HTML identifier for the component. | ### indicatorConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | `"dot"` | Yes | | #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | `"status"` | Yes | | | `status` | `"failed" \| "completed" \| "to-do" \| "scheduled" \| "in-progress" \| "in-review" \| "canceled" \| "blocking" \| "blocked-by" \| "needs-attention"` | Yes | | Root IDs, className, appropriate ARIA attributes, and typed refs are supported. ## Usage rules [#usage-rules] Badges never remove a value, navigate, or trigger actions. Use Tag for user-chosen values and Button/link for interactions. Static badges do not create live regions. The application maps domain statuses to label, tone, and optional existing lifecycle icon. Pathfinder does not infer status from English text or depend on filing enums. Keep labels to one or two words in sentence case. Small badges use a 12px status icon, medium uses the existing 16px artwork scaled to its authored 14px slot. Text wraps for unexpected long translations without clipping the meaning. ## Examples [#examples] ### Tones [#tones] ```tsx import { Badge } from '@commenda/pathfinder/badge'; export const BadgeTones = () => (
); ``` ### Sizes [#sizes] ```tsx import { Badge } from '@commenda/pathfinder/badge'; export const BadgeSizes = () => (
); ``` ### Indicators [#indicators] ```tsx import { Badge } from '@commenda/pathfinder/badge'; export const BadgeIndicators = () => (
); ``` ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Badge [#badge] Source: [240:123](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=240-123), anatomy 242:311, guidance 242:344. - `BADGE-01` — Source: system-reported status, read-only. User-picked values belong to Tag; actions belong to Button/link. No removal, click, loading, disabled, or automatic live-region behavior. - `BADGE-02` — Source: label conveys meaning without color; neutral/info/success/warning/error; optional 6px dot or lifecycle icon, never both. sm 20/radius4/padding6/Label12 Strong/icon12; md 24/radius6/padding8/Label13 Strong/icon14; gap6, no border. Scale existing 16 artwork for the 14 slot, not a duplicate SVG download. - `BADGE-03` — Source/Pathfinder: one domain mapping owned by the application. One or two words, sentence case. Neutral pearl-200/900; semantic 100 fills and 800/900 copy. Unexpected long labels wrap without clipping meaning. --- # Banner Package: @commenda/pathfinder@0.1.0 Page: /docs/components/banner Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=299-282 ```tsx import { Banner } from '@commenda/pathfinder/banner'; ; ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` Import `@commenda/pathfinder/styles.css` once after host CSS. Styles are precompiled with Tailwind v4; consumers need no Tailwind scanning. Interactive callbacks belong in client components in Next.js App Router. ## Usage [#usage] ```tsx import { Banner } from '@commenda/pathfinder/banner'; ; ``` ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `description` | `string` | required | Required | | `title` | `string` | optional | Optional; unavailable for announcement | | `tone` | `FeedbackTone \| "announcement"` | conditional | neutral | | `actionConfig` | `FeedbackAction` | optional | One link; secondary button for announcement | | `dismissConfig` | `DismissConfig` | optional | Optional close control | | `open` | `boolean` | conditional | Controlled / uncontrolled; true initially | | `defaultOpen` | `boolean` | optional | Controlled / uncontrolled; true initially | | `onOpenChange` | `((open: boolean) => void)` | conditional | Required with open | | `announcement` | `FeedbackAnnouncement` | optional | auto | | `className` | `string` | optional | Additional classes for the component root. | ### dismissConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | No | | | `onDismiss` | `(() => void)` | No | | Place below the breadcrumb at the top of the content card. Use one banner at a time for account or product context, never a single record. Announcement centers the message and omits icon and title. Copy wraps on narrow screens. Dismissal follows Alert’s controlled-state and completion-callback rules; nothing expires automatically. ### Actions [#actions] `FeedbackAction` requires a label and exactly one of `onClick` (command) or `href` (native navigation). Commands support `disabled`, `disabledReason`, `loadingConfig`, and `leadingIcon`; navigation supports `target`, `rel`, and `leadingIcon`. The application owns async work and retry state. ## Examples [#examples] ### Tones [#tones] ```tsx <> {(['neutral', 'info', 'success', 'warning', 'error'] as const).map((tone) => ( ))} ; ``` ### Actions [#actions-1] ```tsx ; ``` ### Announcement [#announcement] ```tsx ; ``` ### Dismissible [#dismissible] ```tsx ; ``` Read the [design guidance](/docs/components/banner#design-rules) before using these components. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Alert and Banner — 299:2 [#alert-and-banner--2992] - **ALERT-01:** Persistent, contextual feedback beside the form, table or figure it concerns. Never auto-dismiss. No dismissConfig produces a callout that stays until the owner removes it. Field errors remain on fields; form alerts summarize them. - **ALERT-02:** Title states what happened without a period; description states the next step. Aim for two lines, but never clip essential text. At most a secondary small Button for recovery plus a link for more information. Passing confirmations use Toast. - **ALERT-03:** Neutral is a state, info is news, success is completion, warning is an approaching issue, error is a current problem. Auto announcement maps warnings/errors to alert and info/success to status; neutral stays nonurgent. The explicit announcement prop can override this for the actual event. - **ALERT-04:** Tinted background and 0.5 px border, radius 8, padding 12 vertical/16 start/12 end, gap 10; icon 16, Label/13 Strong title, Copy/13 body, gap 2. Close uses plain IconButton xs on tinted surfaces. - **BANNER-01:** Whole-account or product context only; place beneath the breadcrumb across the content card. One at a time, one concise message, one link. The application owns placement and prioritization. Wrap on narrow screens rather than truncate. - **BANNER-02:** Announcement is the sixth banner tone from node 917:314: blue 100/background, blue 300 bottom border, centered blue 900 copy, no icon or title, optional secondary small Button. - **FEEDBACK-DISMISS-01:** Dismissal fades and collapses space over 200 ms, ease-out. onOpenChange requests closure immediately; onDismiss follows the collapse after an accepted close. A controlled owner can reject the request. The owner restores focus if needed. - **FEEDBACK-ACTION-01:** Commands use Button; navigation uses native anchors. Require a label and exactly one of onClick or href. Components choose action appearance; callers own loading, disabled reasons and async behavior. --- # Breadcrumb Package: @commenda/pathfinder@0.1.0 Page: /docs/components/breadcrumb Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=346-2 ```tsx import { Breadcrumb } from '@commenda/pathfinder/breadcrumb'; import { GBFlag } from '@commenda/pathfinder/flags/GBFlag'; export const BreadcrumbDefault = () => ( , linkConfig: { href: '#acme' }, }, ]} current={{ label: 'Annual report 2027' }} /> ); ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` Import `@commenda/pathfinder/styles.css` once after host CSS. Pathfinder ships compiled, prefixed Tailwind v4 CSS; the application needs no Tailwind scanning. Import from `@commenda/pathfinder/breadcrumb`. Interactive callbacks belong in a client component in Next.js App Router. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `ancestors` | `readonly BreadcrumbAncestor[]` | required | Required; each has id, label, linkConfig, optional leadingIcon | | `current` | `BreadcrumbCurrent` | required | Required; never a link | | `label` | `string` | optional | Breadcrumb; names the navigation landmark | | `pageActionsConfig` | `{ menu: DropdownMenu; label?: string; }` | optional | Optional; trigger defaults to Page actions | | `overflowLabel` | `string` | optional | Show parent pages | | `className` | `string` | optional | Additional classes for the component root. | | `id` | `string` | optional | Stable HTML identifier for the component. | ### current fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | Yes | | | `leadingIcon` | `ReactElement>` | No | | ### pageActionsConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `menu` | `DropdownMenu` | Yes | | | `label` | `string` | No | | ### pageActionsConfig.menu fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `groups` | `DropdownGroup[]` | Yes | | Root IDs, className, appropriate ARIA attributes, and typed refs are supported. Item IDs/values must be unique and stable. ## Usage rules [#usage-rules] `linkConfig` requires `href`, with optional `target`, `rel`, and `render(anchorProps)` for Next Link or another anchor-compatible component. Forward all supplied props and the ref. Collapsed items remain real links inside the menu, preserving browser navigation and modified clicks. Beyond four levels, the component keeps the root, immediate parent, and current page. It derives hidden ancestors; do not maintain a second collapsed list. Labels truncate at 240 and reveal full text on hover/focus when truncated. The current page is not a link. Tabs never add breadcrumb levels. Show entity flags with the existing flag components. Page actions render only when supplied with a nonempty menu. In a page header, avoid a second visible copy of the current title; the enclosing page still owns a semantic heading. ## Examples [#examples] ### Collapsed [#collapsed] ```tsx import { Breadcrumb } from '@commenda/pathfinder/breadcrumb'; export const BreadcrumbCollapsed = () => ( ({ id: label, label, linkConfig: { href: `#${encodeURIComponent(label)}` }, }) )} current={{ label: 'Delaware annual report 2027' }} /> ); ``` ### Actions [#actions] ```tsx import { useState } from 'react'; import { Breadcrumb } from '@commenda/pathfinder/breadcrumb'; export const BreadcrumbActions = () => { const [message, setMessage] = useState(''); return (
{ setMessage('Report duplicated'); }, }, ], }, ], }, }} /> {message}
); }; ``` ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Breadcrumb [#breadcrumb] Source: [346:10629](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=346-10629), crumb 346:56, docs 348:309. - `BREADCRUMB-01` — Source: final crumb is current, not a link. Past four levels retain root and last two. Crumbs are 28 high, side padding 6, radius 6, Label/13 Strong; chevron 12, gap 2. Truncate labels at 240 and expose full names on hover/focus. - `BREADCRUMB-02` — Source: real names; entities lead with flags. Tabs never add levels. Do not add a redundant Back button or repeat the visible page title below the breadcrumb. The enclosing page supplies its semantic heading. - `BREADCRUMB-03` — Pathfinder: ancestors require stable IDs and native linkConfig; current is a separate non-link object. Page actions render only for a supplied nonempty menu, with a localized accessible label. Hidden ancestors retain native anchor behavior through recursive Dropdown link items. --- # Button Package: @commenda/pathfinder@0.1.0 Page: /docs/components/button Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=44-2 ```tsx 'use client'; import { useState } from 'react'; import { Button } from '@commenda/pathfinder/button'; const ButtonBasic = () => { const [saved, setSaved] = useState(false); return ( ); }; export default ButtonBasic; ``` ## Usage [#usage] ```tsx import { Button } from '@commenda/pathfinder/button'; ; ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` The package currently lives in the Commenda workspace. External installation requires a published version or packed tarball. Import the precompiled stylesheet once; no application Tailwind setup is required. ## Examples [#examples] ### Variants [#variants] ```tsx 'use client'; import { Button } from '@commenda/pathfinder/button'; const ButtonVariants = () => ( <> ); export default ButtonVariants; ``` ### Sizes and icons [#sizes-and-icons] ```tsx 'use client'; import { Button } from '@commenda/pathfinder/button'; import { PlusIcon } from '@commenda/pathfinder/icons/PlusIcon'; const ButtonSizes = () => ( <> ); export default ButtonSizes; ``` ### Loading and completion [#loading-and-completion] ```tsx 'use client'; import { Button } from '@commenda/pathfinder/button'; const ButtonFeedback = () => ( <> ); export default ButtonFeedback; ``` ### Disabled reason [#disabled-reason] ```tsx 'use client'; import { Button } from '@commenda/pathfinder/button'; const ButtonDisabled = () => ( ); export default ButtonDisabled; ``` ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `variant` | `"primary" \| "secondary" \| "tertiary" \| "link" \| "link-subtle"` | conditional | primary | | `size` | `"sm" \| "md" \| "lg"` | optional | md; 28/32/36px minimum height | | `destructive` | `boolean` | conditional | false; only primary/secondary/tertiary | | `loadingConfig` | `LoadingConfig` | optional | Spinner with optional replacement label | | `completionConfig` | `CompletionConfig` | optional | Explicit success, default check; only non-destructive primary/secondary/tertiary | | `leadingIcon` | `ReactNode` | optional | Decorative icon slots, 16px at sm/md and 20px at lg | | `trailingIcon` | `ReactNode` | optional | Decorative icon slots, 16px at sm/md and 20px at lg | | `disabledReason` | `string` | optional | Prevent activation; explain unavailable actions | | `className` | `string` | optional | Additional classes for the component root. | ### loadingConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isLoading` | `boolean` | Yes | | | `label` | `string` | No | | ### completionConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isComplete` | `boolean` | Yes | | | `label` | `string` | Yes | | | `icon` | `ReactNode` | No | | `ButtonProps`, `ButtonBaseProps`, `LoadingConfig` and `CompletionConfig` are exported from root and `/button`. ## Feedback rules [#feedback-rules] `CONTROL-STATE-01`: Loading ending does not mean success. Set `isComplete` only after application success. Loading wins if both flags are true. Disabled always blocks activation. Loading blocks repeat activation while retaining keyboard focus; the label and busy state remain accessible. Feedback replaces the normal leading icon; the trailing icon remains. The application owns completion timing. Figma's recipe resets after 1.5 seconds; clear your timer on unmount and reset success before the next request. Errors belong in application feedback. No `status` or `onActionComplete` configuration prop exists. ## Keyboard and labels [#keyboard-and-labels] Tab focuses the control; Enter/Space activates it. Disabled controls normally leave tab order. A disabled control with a reason remains focusable with `aria-disabled`, an accessible description and a tooltip; activation is blocked. Loading controls remain focusable. Preserve this behavior when composing custom elements. `BUTTON-COPY-01`: Name the action and object in sentence case: “Export entities”, “Delete version”. Avoid “Submit” and “OK”. Icon-only actions use IconButton. Labels wrap at narrow widths and enlarged text sizes; minimum heights can grow to keep content readable. Unnamed/whitespace-only labels are invalid. Import `@commenda/pathfinder/styles.css` once at the app root. Event handlers require a client component in Next.js App Router. For navigation, use a real anchor or Next Link. Base UI Button composition can assign button semantics to an anchor; changing the rendered tag alone does not guarantee link semantics. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Action rules [#action-rules] - `BUTTON-COPY-01` — Figma: labels name the action and object in sentence case. Use one primary main action; secondary alongside it; tertiary for quiet repeated actions. Add icons only when they add meaning. Navigation uses a real link. - `CONTROL-STATE-01` — Pathfinder: loadingConfig and completionConfig are explicit application signals. Loading wins over completion; disabled blocks activation. Ending loading never implies success. Applications own the 1.5-second completion reset recipe from Figma. - `CONTROL-SIZE-01` — Figma: Button and SplitButton sm/md/lg have 28/32/36px visual heights; md default. IconButton adds xs 24. Button icons are 16 through md and 20 at lg; pass size to generated icons to select optical artwork. Link styles have text-sized geometry. Long content can increase minimum height for accessibility. - `CONTROL-FOCUS-01` — Figma: show focus from keyboard, not pointer click. Button family uses a blue ring separated by a white gap. Open triggers retain hover styling. Disabled reason is a Pathfinder extension: retain action name, expose description, and allow focus without activation where the native wrapper supports it. --- # Calendar Package: @commenda/pathfinder@0.1.0 Page: /docs/components/calendar Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=804-5708 Review deadlines across entities, inspect a record without leaving the calendar, and start a filing or task for a specific day. ## Month [#month] ```tsx import { Calendar, type CalendarEvent } from '@commenda/pathfinder/calendar'; import { DEFlag } from '@commenda/pathfinder/flags/DEFlag'; import { USFlag } from '@commenda/pathfinder/flags/USFlag'; const calendarEntities = [ { id: 'us', label: 'Meridian US Inc.', flag: }, { id: 'de', label: 'Meridian GmbH', flag: }, ]; const calendarEvents: CalendarEvent[] = [ { id: '1', title: 'Corporate income tax', date: '2026-10-13', category: { id: 'direct', label: 'Direct tax', tone: 'amber' }, entity: calendarEntities[0], status: { label: 'Overdue', tone: 'error', appearance: 'overdue' }, assignee: { kind: 'person', name: 'Priya Shah' }, }, { id: '2', title: 'VAT return Q3', date: '2026-10-13', category: { id: 'indirect', label: 'Indirect tax', tone: 'blue' }, entity: calendarEntities[1], status: { label: 'In progress', tone: 'info' }, }, { id: '3', title: 'Transfer pricing report', date: '2026-10-13', category: { id: 'transfer', label: 'Transfer pricing', tone: 'indigo' }, entity: calendarEntities[1], status: { label: 'Filed', tone: 'success', appearance: 'filed' }, }, { id: '4', title: 'Annual report', date: '2026-10-13', category: { id: 'entity', label: 'Entity compliance', tone: 'purple' }, entity: calendarEntities[0], status: { label: 'Extended', tone: 'neutral', appearance: 'extended' }, }, { id: '5', title: 'Payroll withholding', date: '2026-10-13', category: { id: 'payroll', label: 'Payroll', tone: 'teal' }, entity: calendarEntities[0], status: { label: 'To do', tone: 'neutral' }, }, { id: '6', title: 'Import declaration', date: '2026-10-20', category: { id: 'customs', label: 'Customs', tone: 'rose' }, entity: calendarEntities[1], status: { label: 'Due soon', tone: 'warning' }, assignee: { kind: 'person', name: 'Lucas Moreau' }, }, { id: '7', title: 'Board review', date: '2026-10-28', category: { id: 'other', label: 'Other', tone: 'neutral' }, entity: calendarEntities[0], }, ]; export const CalendarDefault = () => ( ); ``` Click a filing to inspect its details. October 13 has five records: **2 more** opens all five; choosing a record replaces that list inside the same popup. Escape closes the preview and returns focus to the originating trigger. Today, previous and next navigate the selected month. The month has five or six weeks. Adjacent-month dates are muted but their events retain their colors. Weekend cells have a quiet background. Today uses the red date badge. At most three chips appear per day. ## Choosing the Component [#choosing-the-component] | You need to… | Use | | -------------------------------------------------- | ------------------------------------------ | | Review records by month or in a chronological list | **Calendar** | | Enter a date, month or date range in a form | [DatePicker](/docs/components/date-picker) | | Sort and filter records with many columns | [Table](/docs/components/table) | Calendar owns presentation and interaction. Your application owns records, permissions, status calculations and persistence. Dates represent whole days; time slots, recurrence and drag scheduling are outside this component's contract. ## Usage [#usage] ```tsx import { Calendar, type CalendarEvent } from '@commenda/pathfinder/calendar'; const events: CalendarEvent[] = [ { id: 'vat-q3', title: 'VAT return Q3', date: '2026-10-20', category: { id: 'indirect', label: 'Indirect tax', tone: 'blue' }, entity: { id: 'de', label: 'Meridian GmbH' }, status: { label: 'In progress', tone: 'info' }, }, ]; ; ``` Import `@commenda/pathfinder/styles.css` once in the application. No consumer Tailwind compilation is needed. Interactive callbacks belong in a client component in Next.js App Router. ## Agenda [#agenda] ```tsx import { Calendar, type CalendarEvent } from '@commenda/pathfinder/calendar'; import { DEFlag } from '@commenda/pathfinder/flags/DEFlag'; import { USFlag } from '@commenda/pathfinder/flags/USFlag'; const calendarEntities = [ { id: 'us', label: 'Meridian US Inc.', flag: }, { id: 'de', label: 'Meridian GmbH', flag: }, ]; const calendarEvents: CalendarEvent[] = [ { id: '1', title: 'Corporate income tax', date: '2026-10-13', category: { id: 'direct', label: 'Direct tax', tone: 'amber' }, entity: calendarEntities[0], status: { label: 'Overdue', tone: 'error', appearance: 'overdue' }, assignee: { kind: 'person', name: 'Priya Shah' }, }, { id: '2', title: 'VAT return Q3', date: '2026-10-13', category: { id: 'indirect', label: 'Indirect tax', tone: 'blue' }, entity: calendarEntities[1], status: { label: 'In progress', tone: 'info' }, }, { id: '3', title: 'Transfer pricing report', date: '2026-10-13', category: { id: 'transfer', label: 'Transfer pricing', tone: 'indigo' }, entity: calendarEntities[1], status: { label: 'Filed', tone: 'success', appearance: 'filed' }, }, { id: '4', title: 'Annual report', date: '2026-10-13', category: { id: 'entity', label: 'Entity compliance', tone: 'purple' }, entity: calendarEntities[0], status: { label: 'Extended', tone: 'neutral', appearance: 'extended' }, }, { id: '5', title: 'Payroll withholding', date: '2026-10-13', category: { id: 'payroll', label: 'Payroll', tone: 'teal' }, entity: calendarEntities[0], status: { label: 'To do', tone: 'neutral' }, }, { id: '6', title: 'Import declaration', date: '2026-10-20', category: { id: 'customs', label: 'Customs', tone: 'rose' }, entity: calendarEntities[1], status: { label: 'Due soon', tone: 'warning' }, assignee: { kind: 'person', name: 'Lucas Moreau' }, }, { id: '7', title: 'Board review', date: '2026-10-28', category: { id: 'other', label: 'Other', tone: 'neutral' }, entity: calendarEntities[0], }, ]; export const CalendarAgenda = () => ( ); ``` Agenda groups the selected month's records by date. Entity flags, category and status remain visible. Status, assignee and action slots retain their alignment when a record has no assignee. Actions are available on hover, keyboard focus and touch. Narrow containers move trailing details to a second line. ## Controlled State [#controlled-state] ```tsx import { useState } from 'react'; import { Button } from '@commenda/pathfinder/button'; import { Calendar, type CalendarView } from '@commenda/pathfinder/calendar'; export const CalendarDataControlled = () => { const [month, setMonth] = useState('2026-10'); const [view, setView] = useState('agenda'); return ( <> Application state: {month} · {view} ); }; ``` The text below the example shows application state. Navigate or change view, then use Reset. This is the pattern for URL state, saved preferences or a parent-owned toolbar. Use `defaultValue` for internal state with an optional change notification. Use `value` **with** `onValueChange` when the parent owns state. Calendar emits the requested value; it does not override a controlled prop. Supplying both modes is a TypeScript error. ## Responsive View [#responsive-view] ```tsx import { Calendar, type CalendarEvent } from '@commenda/pathfinder/calendar'; import { DEFlag } from '@commenda/pathfinder/flags/DEFlag'; import { USFlag } from '@commenda/pathfinder/flags/USFlag'; const calendarEntities = [ { id: 'us', label: 'Meridian US Inc.', flag: }, { id: 'de', label: 'Meridian GmbH', flag: }, ]; const calendarEvents: CalendarEvent[] = [ { id: '1', title: 'Corporate income tax', date: '2026-10-13', category: { id: 'direct', label: 'Direct tax', tone: 'amber' }, entity: calendarEntities[0], status: { label: 'Overdue', tone: 'error', appearance: 'overdue' }, assignee: { kind: 'person', name: 'Priya Shah' }, }, { id: '2', title: 'VAT return Q3', date: '2026-10-13', category: { id: 'indirect', label: 'Indirect tax', tone: 'blue' }, entity: calendarEntities[1], status: { label: 'In progress', tone: 'info' }, }, { id: '3', title: 'Transfer pricing report', date: '2026-10-13', category: { id: 'transfer', label: 'Transfer pricing', tone: 'indigo' }, entity: calendarEntities[1], status: { label: 'Filed', tone: 'success', appearance: 'filed' }, }, { id: '4', title: 'Annual report', date: '2026-10-13', category: { id: 'entity', label: 'Entity compliance', tone: 'purple' }, entity: calendarEntities[0], status: { label: 'Extended', tone: 'neutral', appearance: 'extended' }, }, { id: '5', title: 'Payroll withholding', date: '2026-10-13', category: { id: 'payroll', label: 'Payroll', tone: 'teal' }, entity: calendarEntities[0], status: { label: 'To do', tone: 'neutral' }, }, { id: '6', title: 'Import declaration', date: '2026-10-20', category: { id: 'customs', label: 'Customs', tone: 'rose' }, entity: calendarEntities[1], status: { label: 'Due soon', tone: 'warning' }, assignee: { kind: 'person', name: 'Lucas Moreau' }, }, { id: '7', title: 'Board review', date: '2026-10-28', category: { id: 'other', label: 'Other', tone: 'neutral' }, entity: calendarEntities[0], }, ]; export const CalendarResponsive = () => ( ); ``` The default `auto` view uses Agenda below **900px of container width** and Month at or above it. Choosing Month or Agenda makes that choice persistent. Set a controlled `viewConfig.value` to `auto` to restore automatic behavior. An explicitly selected Month view scrolls within its container on narrow screens. ## Entity Filtering [#entity-filtering] ```tsx import { Calendar, type CalendarEvent } from '@commenda/pathfinder/calendar'; import { DEFlag } from '@commenda/pathfinder/flags/DEFlag'; import { USFlag } from '@commenda/pathfinder/flags/USFlag'; const calendarEntities = [ { id: 'us', label: 'Meridian US Inc.', flag: }, { id: 'de', label: 'Meridian GmbH', flag: }, ]; const calendarEvents: CalendarEvent[] = [ { id: '1', title: 'Corporate income tax', date: '2026-10-13', category: { id: 'direct', label: 'Direct tax', tone: 'amber' }, entity: calendarEntities[0], status: { label: 'Overdue', tone: 'error', appearance: 'overdue' }, assignee: { kind: 'person', name: 'Priya Shah' }, }, { id: '2', title: 'VAT return Q3', date: '2026-10-13', category: { id: 'indirect', label: 'Indirect tax', tone: 'blue' }, entity: calendarEntities[1], status: { label: 'In progress', tone: 'info' }, }, { id: '3', title: 'Transfer pricing report', date: '2026-10-13', category: { id: 'transfer', label: 'Transfer pricing', tone: 'indigo' }, entity: calendarEntities[1], status: { label: 'Filed', tone: 'success', appearance: 'filed' }, }, { id: '4', title: 'Annual report', date: '2026-10-13', category: { id: 'entity', label: 'Entity compliance', tone: 'purple' }, entity: calendarEntities[0], status: { label: 'Extended', tone: 'neutral', appearance: 'extended' }, }, { id: '5', title: 'Payroll withholding', date: '2026-10-13', category: { id: 'payroll', label: 'Payroll', tone: 'teal' }, entity: calendarEntities[0], status: { label: 'To do', tone: 'neutral' }, }, { id: '6', title: 'Import declaration', date: '2026-10-20', category: { id: 'customs', label: 'Customs', tone: 'rose' }, entity: calendarEntities[1], status: { label: 'Due soon', tone: 'warning' }, assignee: { kind: 'person', name: 'Lucas Moreau' }, }, { id: '7', title: 'Board review', date: '2026-10-28', category: { id: 'other', label: 'Other', tone: 'neutral' }, entity: calendarEntities[0], }, ]; export const CalendarEntities = () => ( ); ``` Empty selected IDs mean all entities. Choose one entity to remove its repeated name from agenda metadata; flags remain. Filter selection survives month and view changes. The optional configuration follows Pathfinder's `value/onValueChange` or `defaultValue` pattern. Supply the complete available entity choices and stable IDs. ## Categories and Statuses [#categories-and-statuses] Category and lifecycle status are independent. The examples use these verified Figma bar modes: | Category | `category.tone` | | ----------------- | --------------- | | Direct tax | `amber` | | Indirect tax | `blue` | | Transfer pricing | `indigo` | | Entity compliance | `purple` | | Payroll | `teal` | | Customs | `rose` | | Other | `neutral` | `status.label` and `status.tone` describe the agenda badge. Optional `status.appearance` controls the compact record presentation: | Appearance | Behavior | | ---------- | ----------------------------------------------------------- | | `default` | Neutral chip with category bar | | `overdue` | Category bar retained; red status mark with Overdue tooltip | | `filed` | Muted category bar and text; green Completed mark | | `extended` | Dashed neutral outline | The revised October 12 Figma treatment supersedes older tinted-chip descriptions. The package never infers status from `today`. For example, an extended deadline can retain an “In progress” badge while using `appearance: 'extended'`. A flag is an optional React element; Pathfinder gives it optical 12px dimensions. Use the circular country flag assets. ## Record Previews and Actions [#record-previews-and-actions] ```tsx import { useState } from 'react'; import { Calendar, type CalendarEvent } from '@commenda/pathfinder/calendar'; import { DEFlag } from '@commenda/pathfinder/flags/DEFlag'; import { USFlag } from '@commenda/pathfinder/flags/USFlag'; const calendarEntities = [ { id: 'us', label: 'Meridian US Inc.', flag: }, { id: 'de', label: 'Meridian GmbH', flag: }, ]; const calendarEvents: CalendarEvent[] = [ { id: '1', title: 'Corporate income tax', date: '2026-10-13', category: { id: 'direct', label: 'Direct tax', tone: 'amber' }, entity: calendarEntities[0], status: { label: 'Overdue', tone: 'error', appearance: 'overdue' }, assignee: { kind: 'person', name: 'Priya Shah' }, }, { id: '2', title: 'VAT return Q3', date: '2026-10-13', category: { id: 'indirect', label: 'Indirect tax', tone: 'blue' }, entity: calendarEntities[1], status: { label: 'In progress', tone: 'info' }, }, { id: '3', title: 'Transfer pricing report', date: '2026-10-13', category: { id: 'transfer', label: 'Transfer pricing', tone: 'indigo' }, entity: calendarEntities[1], status: { label: 'Filed', tone: 'success', appearance: 'filed' }, }, { id: '4', title: 'Annual report', date: '2026-10-13', category: { id: 'entity', label: 'Entity compliance', tone: 'purple' }, entity: calendarEntities[0], status: { label: 'Extended', tone: 'neutral', appearance: 'extended' }, }, { id: '5', title: 'Payroll withholding', date: '2026-10-13', category: { id: 'payroll', label: 'Payroll', tone: 'teal' }, entity: calendarEntities[0], status: { label: 'To do', tone: 'neutral' }, }, { id: '6', title: 'Import declaration', date: '2026-10-20', category: { id: 'customs', label: 'Customs', tone: 'rose' }, entity: calendarEntities[1], status: { label: 'Due soon', tone: 'warning' }, assignee: { kind: 'person', name: 'Lucas Moreau' }, }, { id: '7', title: 'Board review', date: '2026-10-28', category: { id: 'other', label: 'Other', tone: 'neutral' }, entity: calendarEntities[0], }, ]; export const CalendarPreview = () => { const [opened, setOpened] = useState(''); return ( <> { setOpened(`Navigate to record ${event.id}: ${event.title}`); }, getMenu: (event) => ({ groups: [ { id: 'actions', items: [ { id: 'edit', label: 'Edit deadline', type: 'action', onClick: () => { setOpened(`Edit ${event.title}`); }, }, ], }, ], }), }} /> {opened} ); }; ``` Clicking a record opens its preview. Navigation occurs only through the explicit Open record action. `eventConfig.getMenu(event)` supplies the existing recursive Dropdown menu contract for record actions. All records retain their stable IDs when filtering or navigating. ```tsx import { Calendar } from '@commenda/pathfinder/calendar'; export const CalendarDataCustomPreview = () => ( (

{event.entity.label}

Application-owned task details, attachments and permissions go here.

), }} /> ); ``` `renderPreview(event)` replaces the default metadata body. Calendar retains the accessible popup title, close control and focus return. Applications own authorization and save flows. Keep critical information readable without relying on color or a tooltip. ## Create on a Date [#create-on-a-date] ```tsx import { useState } from 'react'; import { Calendar, type CalendarEvent } from '@commenda/pathfinder/calendar'; import { DEFlag } from '@commenda/pathfinder/flags/DEFlag'; import { USFlag } from '@commenda/pathfinder/flags/USFlag'; const calendarEntities = [ { id: 'us', label: 'Meridian US Inc.', flag: }, { id: 'de', label: 'Meridian GmbH', flag: }, ]; const calendarEvents: CalendarEvent[] = [ { id: '1', title: 'Corporate income tax', date: '2026-10-13', category: { id: 'direct', label: 'Direct tax', tone: 'amber' }, entity: calendarEntities[0], status: { label: 'Overdue', tone: 'error', appearance: 'overdue' }, assignee: { kind: 'person', name: 'Priya Shah' }, }, { id: '2', title: 'VAT return Q3', date: '2026-10-13', category: { id: 'indirect', label: 'Indirect tax', tone: 'blue' }, entity: calendarEntities[1], status: { label: 'In progress', tone: 'info' }, }, { id: '3', title: 'Transfer pricing report', date: '2026-10-13', category: { id: 'transfer', label: 'Transfer pricing', tone: 'indigo' }, entity: calendarEntities[1], status: { label: 'Filed', tone: 'success', appearance: 'filed' }, }, { id: '4', title: 'Annual report', date: '2026-10-13', category: { id: 'entity', label: 'Entity compliance', tone: 'purple' }, entity: calendarEntities[0], status: { label: 'Extended', tone: 'neutral', appearance: 'extended' }, }, { id: '5', title: 'Payroll withholding', date: '2026-10-13', category: { id: 'payroll', label: 'Payroll', tone: 'teal' }, entity: calendarEntities[0], status: { label: 'To do', tone: 'neutral' }, }, { id: '6', title: 'Import declaration', date: '2026-10-20', category: { id: 'customs', label: 'Customs', tone: 'rose' }, entity: calendarEntities[1], status: { label: 'Due soon', tone: 'warning' }, assignee: { kind: 'person', name: 'Lucas Moreau' }, }, { id: '7', title: 'Board review', date: '2026-10-28', category: { id: 'other', label: 'Other', tone: 'neutral' }, entity: calendarEntities[0], }, ]; export const CalendarCreation = () => { const [message, setMessage] = useState( 'Click a day background or use Add filing.' ); return ( <> { setMessage(`Open the filing form with due date ${date}.`); }, }, { id: 'task', label: 'Add task', onAction: ({ date }) => { setMessage(`Open the task form with due date ${date}.`); }, }, ], }} /> {message} ); }; ``` Click a day background or its plus affordance to choose an action. A temporary New chip appears while the menu is open; cancellation removes it. Each callback receives `{ date, entityIds }`. Clicking an existing record never invokes creation. The toolbar uses the first action as its default and puts alternatives in a SplitButton. Toolbar creation uses the supplied business-local today; day creation uses the clicked date, including adjacent-month cells. The app opens the form and persists a new event. Calendar does not insert optimistic records automatically. ## API Loading and Recovery [#api-loading-and-recovery] ```tsx import { useEffect, useState } from 'react'; import { Button } from '@commenda/pathfinder/button'; import { Calendar, type CalendarEvent, type CalendarVisibleRange, } from '@commenda/pathfinder/calendar'; export const CalendarDataDefault = () => { const [range, setRange] = useState(); const [response, setResponse] = useState<{ key: string; events: CalendarEvent[]; }>(); const [failed, setFailed] = useState(false); const key = range ? `${range.start}/${range.end}` : ''; useEffect(() => { if (!range || failed) return; const timer = setTimeout(() => { setResponse({ key: `${range.start}/${range.end}`, events: [ { id: range.month, title: 'API-loaded deadline', date: `${range.month}-15`, category: { id: 'tax', label: 'Direct tax', tone: 'amber' }, entity: { id: 'company', label: 'Meridian US Inc.' }, }, ], }); }, 500); return () => { clearTimeout(timer); }; }, [range, failed]); return ( <> {range && (

Requested dates: {range.start} through {range.end} ({range.view}).

)} { setResponse(undefined); setFailed(false); }, } : undefined, }} /> ); }; ``` This example simulates a request, discards obsolete results, and exposes failure/retry. Navigate rapidly or click Simulate API failure. Replace its timer with your query client or `fetch` and `AbortController`. `onVisibleRangeChange` reports inclusive `{ start, end, month, view }`. Month includes adjacent grid dates; Agenda reports the selected month's first and last dates. Include selected entity IDs in the application query key. Cancel stale requests and only pass results belonging to the current key. Do not turn loading or failed responses into an empty event array without the corresponding `stateConfig`. ## Empty State [#empty-state] ```tsx import { Calendar } from '@commenda/pathfinder/calendar'; export const CalendarDataEmpty = () => ( ); ``` `stateConfig.emptyContent` explains an empty month or current entity filter. Month retains its date grid so creation remains available. Loading and retryable errors take precedence over empty content. ## API Reference [#api-reference] Import `CalendarProps`, `CalendarEvent`, `CalendarEntity`, `CalendarView`, `CalendarTone`, `CalendarVisibleRange`, `CalendarAddContext` and `CalendarAddAction` from `@commenda/pathfinder/calendar` or the package root. The public API is one component with direct configuration props; day cells and popup components are internal. | Prop | Type | Default / ownership | | ------------------------- | ---------------------------------------------------- | ----------------------------------------- | | `label` | `string` | Required accessible region name | | `events` | `readonly CalendarEvent[]` | Required; app owns data and unique IDs | | `dateConfig.today` | `string` | Required `YYYY-MM-DD` business-local date | | `dateConfig.locale` | `string` | `en-US`; dates use Intl formatting | | `dateConfig.weekStartsOn` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6` | `0` (Sunday) | | `monthConfig` | `SelectionProps` | Today’s `YYYY-MM` month | | `viewConfig` | `SelectionProps` | `auto`; also `month` or `agenda` | | `entityFilterConfig` | Selection config + `entities`, optional `label` | Omitted: no filter; empty IDs: all | | `eventConfig` | `renderPreview`, `onOpen`, `getMenu` | Default metadata preview | | `addConfig.actions` | `readonly CalendarAddAction[]` | Omitted: read-only calendar | | `stateConfig` | `isLoading`, `loadingLabel`, `error`, `emptyContent` | Ready | | `onVisibleRangeChange` | `(range: CalendarVisibleRange) => void` | App-owned requests | | `className` | `string` | Root styling only | ### Event Records [#event-records] | Field | Type | Guidance | | ---------- | ------------------------------- | ----------------------------------------------------------------------------- | | `id` | `string` | Stable and unique across the supplied collection | | `title` | `string` | Full record name; chips truncate, previews expose it | | `date` | `string` | Canonical `YYYY-MM-DD` deadline | | `category` | `{ id, label, tone }` | Category identity, readable label and one of the seven bar tones | | `entity` | `{ id, label, flag? }` | Stable entity identity; optional circular flag element | | `status` | `{ label, tone?, appearance? }` | Badge content and independent compact-chip treatment | | `assignee` | `PersonIdentity` | `{ kind: 'person', userId?, name?, email?, src? }`; omission means unassigned | Input order determines event order within each day. Agenda sorts day groups chronologically. The application should sort its records before passing them if a business-specific priority is required. ### Navigation and Entity Configs [#navigation-and-entity-configs] | Config | Fields | Behavior | | -------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------- | | `monthConfig` | `value`, `onValueChange` **or** `defaultValue`, optional `onValueChange` | Values are `YYYY-MM` | | `viewConfig` | Same controlled/uncontrolled shape | `month`, `agenda` or `auto` | | `entityFilterConfig` | Same shape with `readonly string[]`, plus `entities`, optional `label` | Empty IDs means all; omitted config hides the filter | `entities` is a readonly array of `{ id, label, flag? }`. The app reconciles selected IDs when its available entities change. Calendar filters the supplied event collection by those IDs; it does not fetch missing records. ### Record Interactions [#record-interactions] | `eventConfig` field | Signature | When to use it | | ------------------- | ---------------------------------------- | ----------------------------------------------------------------- | | `renderPreview` | `(event: CalendarEvent) => ReactNode` | Replace default metadata with a domain-specific preview | | `onOpen` | `(event: CalendarEvent) => void` | Show Open record and route to the full record when it is pressed | | `getMenu` | `(event: CalendarEvent) => DropdownMenu` | Supply recursive actions; available in Agenda and record previews | An event click opens its preview; it does **not** call `onOpen`. Action callbacks receive the same record object you supplied. Check permissions before supplying action items, and apply the same checks in your API. ### Creation Config [#creation-config] `addConfig.actions` is a readonly array of `{ id, label, onAction }`. Its context is: ```tsx type CalendarAddContext = { date: string; entityIds: readonly string[]; }; ``` The first action is the toolbar default; remaining actions appear in its split menu. Every supplied action appears in the day menu. Omit `addConfig` for a review-only calendar. An empty action array also hides creation controls. ### Loading, Errors and Empty Results [#loading-errors-and-empty-results] | `stateConfig` field | Type | Behavior | | ------------------- | ------------------------------------------- | --------------------------------------------------------------------------------- | | `isLoading` | `boolean` | Replaces the event content with a named loading status; navigation remains usable | | `loadingLabel` | `string` | Defaults to “Loading events…” | | `error` | `{ message: string; onRetry?: () => void }` | Displays the message and an optional Retry action | | `emptyContent` | `ReactNode` | Shown when the selected month/filter has no events | Precedence is **loading → error → ready/empty**. On a request failure, clear loading and provide the error. Use empty content only for a successful request with no matching records. Do not pass stale results for a new visible range. ### Visible Range Callback [#visible-range-callback] ```tsx type CalendarVisibleRange = { start: string; // Inclusive YYYY-MM-DD end: string; // Inclusive YYYY-MM-DD month: string; // YYYY-MM view: 'month' | 'agenda'; // Effective view, never 'auto' }; ``` The callback runs after the initial render and when its range or effective view changes. Changing entities alone does not change the date range; include entity state separately in your request key. `auto` can report Agenda initially and then Month after the container is measured, so request cancellation matters. Dates are canonical date-only values. No timestamps, locale-formatted strings, UTC-midnight conversions or implicit browser clock. Calendar arithmetic does not shift dates across timezones. The supported range is years 1000–9999; boundary grid slots outside that range are blank. Invalid dates throw a descriptive RangeError; validate API data before rendering. ## Keyboard and Accessibility [#keyboard-and-accessibility] | Key / input | Result | | ------------------------------- | -------------------------------------------------------------------------- | | Tab / Shift+Tab | Move through navigation, filters, event triggers and available day actions | | Enter / Space on an event | Open its preview | | Enter / Space on N more | Open all events for the day | | Escape | Close the active popup and restore focus to its trigger | | Arrow keys in the view selector | Choose Month or Agenda | | Hover / focus a status mark | Show Completed or Overdue text | Provide an accessible `label`, meaningful event titles and text status labels. Color is supplementary. Every event remains reachable through its day overflow. The mobile default is Agenda; an explicitly chosen Month view uses internal horizontal scrolling. ## Figma Rules and Decisions [#figma-rules-and-decisions] - **CALENDAR-001:** Shared toolbar and filter state for both views; named navigation controls and month announcements. - **CALENDAR-002:** Outside-month text uses pearl-800 instead of the master’s pearl-600 to meet text contrast. Sunday-first default, five/six rows, 120px nominal days, 4px padding, half-pixel rules. - **CALENDAR-003:** Three chips plus overflow; selecting from a day list replaces its content in one dialog. - **CALENDAR-004:** 20px chips, 3px bar, 12px circular flag, single-line title. The preview exposes full text. - **CALENDAR-005:** Revised neutral fills; separate category and lifecycle presentation. - **CALENDAR-006:** Status tooltip targets are keyboard-focusable siblings, never nested buttons. - **CALENDAR-007:** Agenda retains status104 / avatar24 / actions24 slots; missing assignments do not shift columns. - **CALENDAR-008:** Creation forwards the date and entity IDs; a draft is temporary, not saved data. - **CALENDAR-009:** Enter/Space open previews. Escape/outside dismiss; focus returns to the originating trigger. - **CALENDAR-010:** Month changes are instant for all input methods, including reduced motion. The toolbar never slides. Month uses table semantics and native buttons, not an incomplete ARIA grid. Tab traverses interactive records and add targets; arrow keys operate the view selector and existing selection/menu primitives. Mobile defaults to Agenda. No dragging, timed-event scheduling or recurrence engine is included. Sources: [Calendar rules](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=804-5708), [component masters](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=781-3), [interaction examples](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=790-1134). Revised category modes were verified directly from Figma, including indigo Transfer pricing and teal Payroll. --- # Card Package: @commenda/pathfinder@0.1.0 Page: /docs/components/card Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=938-2 ```tsx import { Badge } from '@commenda/pathfinder/badge'; import { Card } from '@commenda/pathfinder/card'; export const CardDefault = () => ( Delaware annual report ); ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` Import `@commenda/pathfinder/styles.css` once after host CSS. Pathfinder ships compiled, prefixed Tailwind v4 CSS; the application needs no Tailwind scanning. Import from `@commenda/pathfinder/card`. Interactive callbacks belong in a client component in Next.js App Router. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `children` | `ReactNode` | required | Required section body | | `headerConfig` | `CardHeaderConfig` | optional | Optional; heading level defaults to 3 | | `footerConfig` | `CardFooterConfig` | optional | At least one action required | | `elevation` | `"flat" \| "raised"` | optional | raised | | `padding` | `"default" \| "none"` | optional | default: 16 body padding | | `scrollConfig` | `{ maxHeight: NonNullable; label: string; }` | optional | Optional explicit, named scrolling region | | `id` | `string` | optional | Stable HTML identifier for the component. | | `className` | `string` | optional | Additional classes for the component root. | ### headerConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | `string` | Yes | | | `description` | `string` | No | | | `headingLevel` | `2 \| 3 \| 4 \| 5 \| 6` | No | | | `actionConfig` | `FeedbackAction` | No | | ### footerConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `primaryAction` | `FeedbackAction` | Yes | | | `secondaryAction` | `FeedbackAction` | No | | #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `secondaryAction` | `FeedbackAction` | Yes | | Do not supply: `primaryAction`. ### scrollConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `maxHeight` | `NonNullable \| undefined>` | Yes | | | `label` | `string` | Yes | | Root IDs, className, appropriate ARIA attributes, and typed refs are supported. ## Usage rules [#usage-rules] Card keeps an arbitrary table, list, or form body as children, with direct header/footer configs. Header/footer actions use the established `FeedbackAction` contract: a label and either onClick or href. Primary and secondary action appearance follows the component design. Do not wrap a form/table in a card-wide link. Body content grows naturally unless scrollConfig is supplied. It becomes a keyboard-reachable scrolling region when it actually overflows. The authored Figma examples contain different height caps; none is imposed globally. Raised cards sit on the canvas, flat cards sit within another surface. ### StatCard [#statcard] | Prop | Type | Default / behavior | | ------------ | ------------------------------------ | ----------------------------- | | label | string | Required metric label | | value | string or number | Required; zero is preserved | | meta | string | Optional comparison/context | | changeConfig | `{ label: string; tone: BadgeTone }` | Optional change Badge | | elevation | raised or flat | raised | | linkConfig | `{ href; target?; rel?; render? }` | Optional native-link behavior | The application formats currency/numbers and determines whether a change is positive: a negative cost change can be good. Only linked StatCards have a hover affordance. A static section Card remains a content container. Neither component fetches data or hides arbitrary children behind a fabricated loading state. ## Examples [#examples] ### Flat [#flat] ```tsx import { Card } from '@commenda/pathfinder/card'; export const CardFlat = () => (

Annual report · Delaware

Due 1 March 2027

); ``` ### Footer [#footer] ```tsx import { useState } from 'react'; import { Card } from '@commenda/pathfinder/card'; export const CardFooter = () => { const [saved, setSaved] = useState(false); return ( { setSaved(true); }, }, secondaryAction: { label: 'Cancel', onClick: () => { setSaved(false); }, }, }} > ); }; ``` ### Scrolling [#scrolling] ```tsx import { Card } from '@commenda/pathfinder/card'; export const CardScrolling = () => (
    {Array.from({ length: 12 }, (_, index) => (
  • Annual report {2027 + index}
  • ))}
); ``` ### Stats [#stats] ```tsx import { StatCard } from '@commenda/pathfinder/card'; export const CardStats = () => (
); ``` ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Card and StatCard [#card-and-statcard] Source: [938:223](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=938-223), section 938:6, stat 938:182, padding-none 942:38. The Card documentation frame contains placeholder text; it is not a source of confirmed guidance. - `SURFACE-CARD-01` — Source: white, 0.5 pearl-300 border, radius12. Raised on canvas, flat within surfaces. Section header title/description/one action, divider, body, optional footer. Header padding12/16/12; body16 or none. No arbitrary dashboard/business behavior. - `SURFACE-CARD-02` — Pathfinder: natural body height by default. scrollConfig explicitly supplies maxHeight and region label; overflow becomes keyboard reachable. Caller-supplied dimensions are an intentional inline-style exception, not additional design tokens. - `SURFACE-CARD-03` — Source/Pathfinder: static cards have no hover affordance. Section actions remain individual buttons/anchors; no card-wide anchor around arbitrary forms/tables. Footer config requires at least one action, using the existing FeedbackAction contract. - `SURFACE-CARD-04` — Source/Pathfinder: StatCard is a separate small API with shared surface styles, padding16/gap8/Heading24 and optional Badge change. App formats values and chooses change meaning. linkConfig alone enables native navigation and authored hover/focus behavior; zero is a valid value. ### Source discrepancies and implementation decisions [#source-discrepancies-and-implementation-decisions] --- # Chain of thought Package: @commenda/pathfinder@0.1.0 Page: /docs/components/chain-of-thought Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=601-225 ```tsx import { ChainOfThought } from '@commenda/pathfinder/chain-of-thought'; ; ``` ## Usage [#usage] ```tsx import { ChainOfThought } from '@commenda/pathfinder/chain-of-thought'; ; ``` ## Live activity [#live-activity] ```tsx import { ChainOfThought } from '@commenda/pathfinder/chain-of-thought'; ; ``` Use Start/Finish activity to exercise streaming updates. New run resets disclosure intent. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `steps` | `readonly ChainOfThoughtStep[]` | required | Required ordered list with stable unique IDs | | `isStreaming` | `boolean` | optional | Live label and elapsed summary | | `duration` | `number` | optional | Live label and elapsed summary | | `label` | `string` | optional | Live label and elapsed summary | | `disclosureConfig` | `ActivityDisclosureConfig` | optional | Outer disclosure; each tool has independent configuration | | `className` | `string` | optional | Layout override | ### disclosureConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `open` | `boolean` | No | | | `defaultOpen` | `boolean` | No | | | `onOpenChange` | `((open: boolean) => void)` | No | | ## Step model [#step-model] ```tsx type ChainOfThoughtStep = | { id: string; type: 'reasoning'; content: ReactNode } | { id: string; type: 'tool'; tool: ToolCallProps }; ``` Update a step in place using the same ID. Several tool steps may be running concurrently; the host determines when the overall reasoning part ends. An empty list creates an empty disclosure without fabricated progress. ## Design rules [#design-rules] - **CHAIN-01:** IDs must be unique and stable across streaming updates and reordering. Use toolCallId for tool steps. - **CHAIN-02:** Figma 600:29113 / 600:32905: 12px between prose groups, 4px between consecutive tool rows; preserve source order and do not invent steps. - **CHAIN-03:** History starts closed, live starts open and closes after the next part plus 1s. Manual expansion stays respected; each nested tool retains its own disclosure. `disclosureConfig.onOpenChange` reports user toggle requests. A controlled `open` value delegates timing and persistence to the host. Automatic collapse also preserves a panel containing keyboard focus. ## Accessibility and integration [#accessibility-and-integration] Disclosure buttons support Enter and Space, preserve focus, and expose `aria-expanded`. Payload copy buttons are outside the disclosure trigger. Pointer disclosure uses the existing 200ms height transition; keyboard and reduced-motion interaction are immediate. Long text wraps while code scrolls inside its own region. Use one meaningful ThinkingIndicator status per active response rather than announcing every streamed token or tool payload. The application owns requests, cancellation, approval, retries, durations, and history persistence. No AI SDK provider is required. See [AI content integration](/docs/guides/ai-content) for migration mappings; consumer migration is outside this phase. --- # Checkbox Package: @commenda/pathfinder@0.1.0 Page: /docs/components/checkbox Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=199-2 ```tsx import { Checkbox } from '@commenda/pathfinder/checkbox'; ; ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` Import `@commenda/pathfinder/styles.css` once at the app root. The stylesheet is precompiled with Tailwind v4; your application does not need Tailwind or source scanning. Event handlers belong in a client component in Next.js App Router. ## Usage [#usage] ```tsx import { Checkbox } from '@commenda/pathfinder/checkbox'; ; ``` | Prop | Type | Default / behavior | | ---------------------- | ---------------------------- | ------------------------------------------------------------------------------- | | `label` | `string` | Required, visible text and accessible name | | `hideLabel` | `boolean` | `false`; retains the accessible name for dense/table controls | | `description` | `string` | Optional supporting text, associated with the control | | `checked` | `boolean` | Controlled selection; pair with `onCheckedChange` or `readOnly` | | `defaultChecked` | `boolean` | `false`; initial uncontrolled selection | | `onCheckedChange` | `(checked: boolean) => void` | Receives the next boolean only | | `indeterminate` | `boolean` | `false`; mixed parent selection, separate from `checked` | | `disabled` | `boolean` | Blocks interaction and submission | | `disabledReason` | `string` | Visible explanation while disabled, included in accessible description | | `readOnly` | `boolean` | Prevents changes while retaining value | | `required` | `boolean` | Requires selection for form validation | | `variant` | `'default' \| 'card'` | `'default'`; card retains checkbox semantics | | `details` | `readonly string[]` | Optional explanatory bullet lines, intended for cards | | `name` | `string` | Name used when the control submits its value with a form. | | `value` | `string` | Value submitted when checked; defaults to 'on'. | | `uncheckedValue` | `string` | Value submitted when unchecked; omitted when not configured. | | `form` | `string` | ID of the form associated with the control. | | `id` | `string` | ID of the underlying form input. | | `inputRef` | `Ref` | Access to the underlying form input. | | `className` | `string` | Additional class names for the outer layout. | | `ref` | `Ref` | Access to the interactive root element. | | Native events and ARIA | Base UI root props | Typed passthrough; arbitrary children/render overrides are intentionally hidden | A checkbox saves with a form. Its label is part of the target. `indeterminate` is a parent summary, not a third answer: the application derives it from children and updates the children on change. Do not pass `checked` and `defaultChecked` together. Use positive labels such as “Send me a copy”. A visible description is preferable to a tooltip-only disabled reason for form controls. The Figma control is one size: a 16px box, 4px radius, 12px check/minus mark and 8px label gap. There is no invented size or cosmetic hover/focus state prop. Checkbox cards reuse the same component with `variant="card"`. ## Examples [#examples] ### Checked, unchecked and mixed [#checked-unchecked-and-mixed] ```tsx <> setMixed(false)} /> ; ``` ### Description [#description] ```tsx ; ``` ### Parent and children [#parent-and-children] ```tsx const filings = ['Annual filing', 'Quarterly filing', 'Monthly filing']; const [selected, setSelected] = useState(['Annual filing']); <> 0 && selected.length < filings.length} onCheckedChange={(checked) => setSelected(checked ? [...filings] : [])} /> {filings.map((label) => ( setSelected((current) => checked ? [...current, label] : current.filter((item) => item !== label) ) } /> ))} ; ``` ### Checkbox card [#checkbox-card] ```tsx ; ``` ### Disabled states [#disabled-states] ```tsx <> ; ``` Read the [Figma choice-control rules](/docs/components/checkbox#design-rules) for design guidance, source discrepancies and accessibility requirements. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Checkbox [#checkbox] Source: [199:2](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=199-2), variants 199:90, guidance 203:226. - **CHECKBOX-01** — Use independent checkboxes for choices saved with a form. An immediate setting uses Switch. The label is part of the target. Use positive labels such as “Send me a copy”, not questions. - **CHECKBOX-02** — Mixed/indeterminate summarizes a parent whose children are partly selected. Activating it selects all its children. It is never a third answer. Applications own the child selection and derive the parent's checked/mixed flags. - **CHECKBOX-03** — Box 16px, radius 4px, 1px soft-pearl-400 border. Hover uses soft-pearl-500. Checked/mixed uses blue-700 and a white 12px check/minus; hover blue-800. Keyboard focus adds a white 2px gap and blue 2px ring. Disabled uses soft-pearl-100 surface, soft-pearl-300 border and soft-pearl-600 mark/text. - **CHECKBOX-04** — Label/13 regular sits 8px from the box; Copy/12 description uses soft-pearl-800, 2px below. Child rows indent 24px. Long content wraps and must not clip the box or focus ring. ### Radio and cards [#radio-and-cards] Source: [199:3](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=199-3), radio variants 200:46, card variants 201:413, guidance 205:320. - **RADIO-01** — One choice from two to five visible options. Use Select for longer lists or limited space. A lone boolean option uses Checkbox/Switch. Arrow keys move selection and skip disabled items; Tab enters/leaves the group. - **RADIO-02** — Prefer a sensible default. Leave the group unanswered only when deliberate choice is required. Never silently choose the first value or replace a removed controlled value. The application reconciles dynamic options. - **RADIO-03** — Order options by frequency, size or time rather than alphabetically by default. Circle 16px, selected white dot 6px; colors and focus match Checkbox. Group row gap 8px. - **CARD-01** — Use a card when each option needs title and description. Keep cards in one row equal-height. The “Radio card” Figma collection contains both radio and checkbox controls; Pathfinder exposes `RadioGroup variant="card"` for one choice and `Checkbox variant="card"` for independent choices. - **CARD-02** — Card padding 16px, control/text gap 12px, radius 8px. Title Label/13 Strong; description Copy/12 with 4px gap. Detail bullets have 2px gap and 4px top padding. Surface remains white when selected; border becomes blue-700. Focus surrounds the whole card. Disabled surface/border/text use soft-pearl-100/300/600. The 320px specimen width adapts to the container. ### Implementation decisions and source differences [#implementation-decisions-and-source-differences] The complete prop tables are on [Checkbox](/docs/components/checkbox), [RadioGroup](/docs/components/radio-group) and [Switch](/docs/components/switch). Source rule IDs are also kept in the component implementation comments and exported through `llms.txt`, `llms-full.txt` and the Markdown routes. - **CHOICE-01** — The sheets describe 8px list gaps but some documentation frames place rows 32px apart. Use the documented 8px component-list gap; documentation specimen whitespace is not a new size token. - **CHOICE-02** — Built-in text is plain, wrapping content. Do not place links, buttons or nested interactive controls inside labels or cards. Disabled reasons are visible descriptions, not tooltip-only explanations on controls removed from keyboard navigation. - **CHOICE-03** — Visual control size is not the hit target. Coarse-pointer rows provide at least 44px height, while preserving 16px marks and the 36×20px switch track. Cards already exceed this height. - **CHOICE-04** — Hover/focus are real browser states, not public cosmetic props. Retain native `name`, `value`, `form`, `required`, `readOnly`, disabled semantics and input refs. `hideLabel` preserves the required accessible label. - **CHOICE-05** — Use Pathfinder's existing Check/Minus optical artwork at 12px. Boxes, circles, tracks and thumbs are interactive primitive geometry, styled with the Figma tokens; they are not flattened screenshots. This keeps focus, RTL and switch movement functional. --- # Code block Package: @commenda/pathfinder@0.1.0 Page: /docs/components/code-block Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=596-28365 ```tsx import { CodeBlock } from '@commenda/pathfinder/code-block'; ; ``` ## Usage [#usage] ```tsx import { CodeBlock } from '@commenda/pathfinder/code-block'; import '@commenda/pathfinder/styles.css'; import '@commenda/pathfinder/code-font.css'; ; ``` Load compiled CSS once in your application. The optional code-font stylesheet supplies Geist Mono; without it, the component uses the host's monospace fallback. No app-side Tailwind compilation or syntax stylesheet is needed. ## Examples [#examples] ### Tool request and response [#tool-request-and-response] Pass the original payload string when exact formatting matters. The component does not parse, prettify, or execute it. ```tsx ; ``` ### Diff [#diff] Mark one-based **source line positions**. The original code remains unchanged, including when copied. Added and removed lines have both a background and a gutter marker. ```tsx ; ``` ### Plain text and unknown languages [#plain-text-and-unknown-languages] Unsupported languages fall back to readable plain text. Line numbers can be hidden. ```tsx ; ``` ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `code` | `string` | required | Required source; copying preserves whitespace and trailing newlines | | `language` | `string` | optional | `text`; Highlight.js common languages and aliases | | `filename` | `string` | optional | Optional header label; long names truncate with full native title | | `variant` | `"code" \| "payload" \| "diff"` | optional | `code` | | `lineNumberConfig` | `{ visible?: boolean; start?: number; }` | optional | Visible, starts at 1 | | `showLineNumbers` | `boolean` | optional | Compatibility prop; explicit `lineNumberConfig.visible` wins | | `diffConfig` | `{ added?: readonly number[]; removed?: readonly number[]; }` | optional | One-based source line positions, independent of displayed starting number | | `copyConfig` | `CodeCopyConfig` | optional | Copy enabled by default | ### lineNumberConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `visible` | `boolean` | No | | | `start` | `number` | No | | ### diffConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `added` | `readonly number[]` | No | | | `removed` | `readonly number[]` | No | | ### copyConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `enabled` | `boolean` | No | | | `label` | `string` | No | | | `copiedLabel` | `string` | No | | | `errorLabel` | `string` | No | | | `onCopy` | `(() => void)` | No | | | `onError` | `((error: Error) => void)` | No | | `CodeCopyConfig` supports `enabled`, `label`, `copiedLabel`, `errorLabel`, `onCopy`, and `onError(error)`. Clipboard failure is announced and remains retryable; the success indicator resets after two seconds. Callbacks run after the clipboard outcome. ## Design and accessibility rules [#design-and-accessibility-rules] - **CODE-01:** White surface, pearl-300 hairline, radius 8, 32px header, 12px body inset, Geist Mono 10/16 without ligatures. - **CODE-02:** Show line numbers by default. Long source scrolls horizontally; never truncate or collapse code silently. - **CODE-03:** Copy excludes labels and the gutter. Diff meaning must not rely on color alone. - **CODE-04:** Payloads remain inert source; unknown languages still render. Highlighting loads after mount and does not block initial content. - The scroll region is keyboard-focusable. Clipboard outcomes use a polite status announcement. ## Migration notes [#migration-notes] Existing `code`, `language`, and `showLineNumbers` values can be reused. Replace compound CodeBlock header/copy children with `filename` and `copyConfig`. This is a documented future migration path; no existing consumers are changed. The renderer uses Lowlight/Highlight.js, not Shiki. See the [AI content integration guide](/docs/guides/ai-content). --- # Composer context Package: @commenda/pathfinder@0.1.0 Page: /docs/components/composer-context Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=582-1565 ```tsx import { ComposerContext } from '@commenda/pathfinder/composer-context'; {editor} ; ``` ## Usage [#usage] ```tsx import { ComposerContext } from '@commenda/pathfinder/composer-context'; {editor} ; ``` The child is your editor surface. This phase demonstrates the existing TextArea; the AI Composer will be implemented in Phase 3. The wrapper never invents page context, adds it to prompts, or owns the request payload. ## Connector invitation [#connector-invitation] ```tsx {editor} ; ``` Use `position="bottom"` with `connectorConfig`. Its main action opens the application's connector flow; dismiss is independent. Supply existing Pathfinder logos through `logos`. After dismissal, set `connectorConfig={null}`. This removes the invitation and its colored footer while preserving the mounted editor and its draft. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `children` | `ReactNode` | required | Required editor surface | | `position` | `"top" \| "bottom"` | conditional | Top default; determines the configuration branch | | `items` | `readonly ComposerContextItem[]` | conditional | Top only: stable `id`, `label`, `role: current \| carried`, optional `removable` | | `onRemove` | `((id: string) => void)` | optional | Top only; shown only on explicitly removable items | | `connectorConfig` | `{ label: string; logos?: readonly ReactNode[]; onConnect: () => void; onDismiss?: () => void; } \| null` | conditional | Required for bottom position | | `className` | `string` | optional | Outer wrapper | ### connectorConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | Yes | | | `logos` | `readonly ReactNode[]` | No | | | `onConnect` | `() => void` | Yes | | | `onDismiss` | `(() => void)` | No | | ## Design and behavior rules [#design-and-behavior-rules] - **CONTEXT-01:** Figma 582:1565 and 588:27564. Pearl-200 wrapper, top pills inset 12px and separated from the editor by 6px. Current context is white with pearl-1000 text; carried context uses pearl-100 and pearl-800. Pills use Label/12 Strong, radius 6px and a 0.5px border. - **CONTEXT-02:** Only show context actually supplied by the host. Removal emits the stable ID; the application changes both displayed context and request data. - **CONTEXT-03:** Bottom connector invitation uses stacked logos and a separate dismiss control. Native buttons preserve Enter/Space activation and visible focus. ## Migration notes [#migration-notes] Map ChatPanelPill `label` and `onDismiss` to an item and `onRemove(id)`. Keep the existing decision about whether the assistant receives page content in the application. Current/carried presentation is explicit and does not replace stored conversation state. --- # Composer Package: @commenda/pathfinder@0.1.0 Page: /docs/components/composer Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=580-28168 ```tsx import { Composer } from '@commenda/pathfinder/composer'; ; ``` ## Usage [#usage] ```tsx import { Composer } from '@commenda/pathfinder/composer'; { // Set request status synchronously; catch failures in your request handler. sendDraft({ text, attachments, command }); }} requestConfig={{ status, onStop: stop }} />; ``` The host owns the draft, uploads, selected skill, and request lifecycle. Composer never starts a network request or clears data. Clear the submitted draft only after acceptance, and avoid clearing text the user has typed since submitting. Handle rejected requests in the application and preserve the draft for retry. ## Standard and hero [#standard-and-hero] Standard starts with one 20px text line. Hero reserves a 53px editing area. Both grow with content, cap the composer surface at 40dvh, and keep Send reachable while the editor scrolls. The maximum width is 736px; smaller containers fill available width. ```tsx ; ``` ## Skills and API search [#skills-and-api-search] Type `/` at a word boundary to open skills. Text after the slash filters local labels and descriptions. Arrow keys move through enabled results; Enter selects without sending. Selection removes the query and displays the selected skill as a blue Tag beside the editable text. The submit payload preserves the command separately from plain text; no rich-text document or Vercel message object is required. ```tsx ``` Manual mode preserves API ranking and does not filter results again. `onSearchChange` receives the query while open and `null` when closed. Debounce, cancel outdated requests, and supply results only for the current query. Set `isLoading` while results are stale: selection is blocked and previous options are hidden. Empty results cannot accidentally send the query. The following example simulates an API; search for `tax` to receive semantically related results. ```tsx ; ``` The selected skill is one token preceding the plain-text draft. This is not an arbitrary inline rich-text mention editor. Shared context comes from the host through `contextConfig`; no undocumented `@` picker is introduced. ## Attachments, mode, and dictation [#attachments-mode-and-dictation] `attachmentConfig` composes AttachmentGroup above the editor. `onAdd` opens your existing file picker or upload menu; validate with AttachmentValidation before uploading. Pending or failed uploads disable Send until resolved or removed. Removal, retry, previews, cancellation, and URLs remain application-owned. `toolbar.leading` accepts Mode/Model Select or Dropdown controls. `toolbar.trailing` accepts a dictation IconButton. Render only actions your application supports and pass their disabled states explicitly. Composer does not request microphone permission or connect to a speech service. ```tsx ), trailing: ( ), }} />; ``` ## Context [#context] Pass ComposerContext's top or bottom configuration through `contextConfig`. Context stays separate from the outgoing text; your host adds any domain identifiers to its request. To dismiss a bottom invitation, retain its position and set `connectorConfig: null`. ```tsx ; ``` ## Sending and failure [#sending-and-failure] `submitted` and `streaming` show Stop. Typing remains available, but Enter never queues a message. `ready` and `error` allow an explicit retry when text and attachments are valid. An absent `onStop` disables Stop. Errors belong in the appropriate Attachment, Message, or host notification, with no red composer border. ```tsx ; ``` ## Keyboard and accessibility [#keyboard-and-accessibility] - Provide a meaningful `label`; the placeholder is not the accessible name. - `enter` sends with Enter, while Shift+Enter inserts a newline. `mod-enter` sends with Command/Ctrl+Enter and leaves ordinary Enter for newlines. - IME composition does not submit or choose a skill. Pasted line breaks are preserved. - Skills use a listbox linked to the textarea, with an active descendant. Focus stays in the editor. - Escape closes suggestions and stops propagation. It never cancels generation; use Stop. Tab leaves suggestions without choosing a result. - Disabled state locks the editor and built-in actions. Its visible reason is associated with the editor. Host toolbar controls must also receive disabled state. - The forwarded ref is the textarea, so selecting a starter prompt can focus it. ```tsx ; ``` ## Mobile layout [#mobile-layout] The host uses a flex column with scrollable conversation history and a footer that stays above the software keyboard. This example listens to `visualViewport.resize`, bounds its height, and includes the bottom safe area. Composer stays in normal document flow. Test the containing application on actual iOS/Android keyboards before release. ```tsx // Host layout: viewport resize and safe-area padding belong to your chat shell.
{messages}
; ``` ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `label` | `string` | required | Required accessible editor name | | `value` | `string` | required | Required controlled draft | | `onValueChange` | `((value: string) => void)` | required | Required controlled draft | | `onSubmit` | `((message: ComposerSubmission) => void)` | required | Required; host handles async failures | | `size` | `"standard" \| "hero"` | optional | `standard` | | `placeholder` | `string` | optional | `Ask Commenda anything`; mention `/` only when skills are configured | | `requestConfig` | `{ status: "ready" \| "submitted" \| "streaming" \| "error"; onStop?: () => void; submitLabel?: string; stopLabel?: string; }` | optional | `status`: `ready`, `submitted`, `streaming`, or `error` | | `attachmentConfig` | `Omit & { onAdd?: () => void; addLabel?: string; }` | optional | Alignment/class controlled internally; default three rows | | `commandConfig` | `ComposerCommandConfig` | optional | Optional local/manual skill list and controlled selection | | `contextConfig` | `ComposerContextConfig` | optional | Top context or bottom connector invitation | | `toolbar` | `{ leading?: ReactNode; trailing?: ReactNode; }` | optional | Application-owned controls | | `submitShortcut` | `"enter" \| "mod-enter"` | optional | `enter` | | `disableConfig` | `{ isDisabled: boolean; reason?: string; }` | optional | Optional disabled state and caption | | `inputProps` | `Pick, "autoFocus" \| "id" \| "spellCheck" \| "aria-describedby" \| "onFocus" \| "onBlur" \| "name" \| "autoComplete" \| "maxLength">` | optional | `id`, `name`, `autoFocus`, `autoComplete`, `spellCheck`, `maxLength`, `aria-describedby`, `onFocus`, `onBlur` | | `className` | `string` | optional | Outer layout and imperative focus | ### requestConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `"ready" \| "submitted" \| "streaming" \| "error"` | Yes | | | `onStop` | `(() => void)` | No | | | `submitLabel` | `string` | No | | | `stopLabel` | `string` | No | | ### commandConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | `readonly ComposerCommand[]` | Yes | | | `selected` | `ComposerCommand \| null` | Yes | | | `onSelect` | `(command: ComposerCommand \| null) => void` | Yes | | | `filterMode` | `"local" \| "manual"` | No | Manual mode accepts results as supplied, without applying a second filter. | | `onSearchChange` | `((query: string \| null) => void)` | No | | | `isLoading` | `boolean` | No | | | `label` | `string` | No | | | `emptyLabel` | `string` | No | | ### commandConfig.selected fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | `string` | Yes | | | `label` | `string` | Yes | | | `description` | `string` | No | | | `disabled` | `boolean` | No | | ### contextConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | `readonly ComposerContextItem[]` | Yes | | | `onRemove` | `((id: string) => void)` | No | | | `position` | `"top"` | No | | Do not supply: `connectorConfig`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `position` | `"bottom"` | Yes | | | `connectorConfig` | `{ label: string; logos?: readonly ReactNode[]; onConnect: () => void; onDismiss?: () => void; } \| null` | Yes | | Do not supply: `items`, `onRemove`. ### contextConfig.connectorConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | Yes | | | `logos` | `readonly ReactNode[]` | No | | | `onConnect` | `() => void` | Yes | | | `onDismiss` | `(() => void)` | No | | ### toolbar fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `leading` | `ReactNode` | No | | | `trailing` | `ReactNode` | No | | ### disableConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isDisabled` | `boolean` | Yes | | | `reason` | `string` | No | | `ComposerSubmission` contains `{ text: string; attachments: readonly AttachmentItem[]; command: ComposerCommand | null }`. `ComposerCommand` contains `{ id, label, description?, disabled? }`. IDs must be unique and stable. `ComposerCommandConfig` requires `items`, `selected`, and `onSelect`, with optional `filterMode`, `onSearchChange`, `isLoading`, `label`, and `emptyLabel`. ## Design rules [#design-rules] - **COMPOSER-01:** Figma 580:28873: 16px inset, 6px gap, 20px radius, pearl400 hairline, soft raised shadow, 736px maximum. Hover/focus border uses blue700. - **COMPOSER-02:** Send requires nonblank text and ready attachments. Generating changes Send to Stop without locking the draft. - **COMPOSER-03:** Never clear, silently queue, upload, or execute commands internally. - **COMPOSER-04:** Respect IME and both keyboard modes. Escape is scoped to suggestions; explicit Stop avoids cancelling a request when dismissing a menu. - **COMPOSER-05:** Do not render a red error composer. Disabled state explains why in a caption. - **COMPOSER-06:** Skills use a searchable listbox and blue Tag; toolbar actions use existing Pathfinder controls. Application context is supplied by the host. - **COMPOSER-07:** Mobile editor text is 16px. The host docks its chat layout above the keyboard and accounts for safe areas; Composer itself does not take over the viewport. The existing TextArea's field shell is intentionally not reused: it adds a second border, field padding, and an 80px minimum. Composer keeps a private native textarea and reuses AttachmentGroup, ComposerContext, Tag, and IconButton. --- # Conversation Package: @commenda/pathfinder@0.1.0 Page: /docs/components/conversation Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=543-2 ```tsx import { Conversation } from '@commenda/pathfinder/conversation'; message.id} renderMessage={renderMessage} footer={} announcement="Response complete." className="conversation-layout" />; ``` ## Usage [#usage] ```tsx import { Conversation } from '@commenda/pathfinder/conversation'; import { Message } from '@commenda/pathfinder/message'; message.id} renderMessage={(message, { isLast }) => ( )} announcement={turnAnnouncement} footer={} className="conversation-layout" />; ``` Give the container a bounded height or `flex: 1; min-height: 0` inside a bounded flex layout. Conversation owns its scrolling viewport, not the page scroll. Change its React `key` when switching conversation IDs. ## Docked request and recap [#docked-request-and-recap] ```tsx import { Conversation } from '@commenda/pathfinder/conversation'; message.id} renderMessage={renderMessage} footer={
{pendingRequest && ( )}
} className="conversation-layout" />; ``` The live question sits 12px above Composer. Acceptance appends a stable recap part to the originating reply and removes the docked card. Temporary dismissal keeps the question pending and provides a way to reopen it. Escape belongs to the focused nested overlay first; the request handles it only after the overlay closes. Typing in Composer never activates a question's numeric shortcuts. ## Empty conversation [#empty-conversation] ```tsx import { Conversation } from '@commenda/pathfinder/conversation'; } className="conversation-layout" />; ``` Supply `emptyState` with suggested prompts or introductory content. Message fetching, permissions and transport remain in the host. ## History and stream following [#history-and-stream-following] Use **Load earlier messages**, scroll into the history, and choose **Append reply** or **Stream chunk**. The viewport follows only within 32px of the bottom. Earlier rows retain their keys and the first visible row retains its offset. **Jump to latest** resumes following. ResizeObserver also handles delayed image/font/content growth. Scroll positioning is immediate and respects reduced-motion users without animated jumps. `historyConfig` reports loading and an optional inline error. The host guards overlapping loads, keeps previously loaded items, and retries via `onLoadMore`. This component does not virtualize an unbounded history: paginate on the server and profile realistic histories before adding a virtualization layer. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `messages` | `readonly Item[]` | required | Original typed application/SDK objects | | `getMessageKey` | `((message: Item, index: number) => string)` | required | Stable unique identity across prepend/append | | `renderMessage` | `((message: Item, context: { index: number; isLast: boolean; }) => ReactNode)` | required | Host maps roles, states and parts | | `historyConfig` | `{ hasMore: boolean; isLoading: boolean; onLoadMore: () => void; error?: string; }` | optional | App-owned earlier-history loading | | `emptyState` | `ReactNode` | optional | Empty transcript content | | `footer` | `ReactNode` | optional | Composer, docked requests and related controls | | `announcement` | `string` | optional | Concise turn-level live status | | `label` | `string` | optional | Conversation and transcript accessible names | | `className` | `string` | optional | Bounded layout; no default fixed height | ### historyConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `hasMore` | `boolean` | Yes | | | `isLoading` | `boolean` | Yes | | | `onLoadMore` | `() => void` | Yes | | | `error` | `string` | No | | ## Design rules and integration [#design-rules-and-integration] - **CONVERSATION-01:** Preserve original objects and keyed part identity. Do not flatten text around tool calls or discard unknown custom parts. - **CONVERSATION-02:** Do not pull a reader to the bottom while they inspect history. Preserve the visible row during prepend and delayed layout changes. - **CONVERSATION-03:** The transcript is a keyboard-focusable region, not a per-token live log. Use `announcement` for “Response complete”, “Stopped” or an error once per transition. - **CONVERSATION-04:** Transport, providers, execution, feedback persistence and message storage remain in the application. Compose existing Pathfinder components through the footer and typed render callbacks. The API follows the separation in [Vercel AI Elements Conversation](https://elements.ai-sdk.dev/components/conversation) and [Message](https://elements.ai-sdk.dev/components/message), using Pathfinder's direct props instead of requiring compound children. Existing `useChat` messages can pass through unchanged; adapt rendering at the boundary. Migration of the existing application is out of scope. Download/export, virtualization and automatic message trimming are not performed here. --- # Data grid Package: @commenda/pathfinder@0.1.0 Page: /docs/components/data-grid Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=506-22089 ```tsx 'use client'; import { DataGrid } from '@commenda/pathfinder/data-grid'; import { TableNumberCell } from '@commenda/pathfinder/table'; const rows = [ { id: 'revenue', label: 'Revenue', actual: 450000, budget: 420000 }, { id: 'expenses', label: 'Operating expenses', actual: 185000, budget: 190000, }, ]; const DataGridBasic = () => ( row.id }} rowHeaderConfig={{ header: 'Line item', getLabel: (row) => row.label, width: 200, }} columns={[ { id: 'q1', header: 'Q1 2026', columns: [ { id: 'actual', header: 'Actual', align: 'end', width: 160, cell: (row) => , }, { id: 'budget', header: 'Budget', align: 'end', width: 160, cell: (row) => , }, ], }, ]} /> ); export default DataGridBasic; ``` Use DataGrid when readers compare values across periods or entities. Each record supplies a row label and values; the grid preserves that context while navigating cells. Use [Table](/docs/components/table) for searchable record lists with selection and pagination. ## Usage [#usage] ```tsx 'use client'; import { DataGrid } from '@commenda/pathfinder/data-grid'; import { TableNumberCell } from '@commenda/pathfinder/table'; ``` Import `@commenda/pathfinder/styles.css` once in your application layout. Each example's **Code** tab contains its complete source with formatting and line numbers. Start with stable row IDs, a `rowHeaderConfig`, and value columns; add grouping or editing when needed. ## Grouped headers [#grouped-headers] ```tsx import { DataGrid } from '@commenda/pathfinder/data-grid'; import { TableNumberCell } from '@commenda/pathfinder/table'; export const DataGridPatternsDefault = () => { const rows = [ { id: 'revenue', label: 'Revenue', actual: 125400, budget: 120000 }, { id: 'expenses', label: 'Expenses', actual: 86000, budget: 84000 }, ]; return ( row.id }} rowHeaderConfig={{ header: 'Line item', getLabel: (row) => row.label }} columns={[ { id: 'year', header: 'FY 2026', columns: [ { id: 'quarter', header: 'Q1', columns: [ { id: 'actual', header: 'Actual', width: 160, cell: (row) => ( ), }, { id: 'budget', header: 'Budget', width: 160, cell: (row) => ( ), }, ], }, { id: 'total', header: 'Total actual', width: 160, cell: (row) => , }, ], }, { id: 'variance', header: 'Variance', width: 160, cell: (row) => ( ), }, ]} /> ); }; ``` FY 2026 contains Q1 Actual/Budget and a Total column. Variance sits outside the year. This intentionally mixes header depths: groups span their descendant leaves and shallower branches span the remaining levels. A group has `{ id, header, columns }`; a value column has an accessor or renderer. Groups recurse, but all IDs must be unique across the tree and every group must contain columns. Use a Total column per period group when the domain supports that aggregate. ## Expandable rows and totals [#expandable-rows-and-totals] ```tsx import { useState } from 'react'; import { DataGrid } from '@commenda/pathfinder/data-grid'; import { TableNumberCell } from '@commenda/pathfinder/table'; export const DataGridGuidesDefault = () => { interface Account { id: string; label: string; amount: number; children?: Account[]; } const rows: Account[] = [ { id: 'expenses', label: 'Operating expenses', amount: 86000, children: [ { id: 'salary', label: 'Salaries', amount: 70000 }, { id: 'rent', label: 'Office rent', amount: 16000 }, ], }, ]; const [expanded, setExpanded] = useState(['expenses']); return ( row.id, getChildren: (row) => row.children ?? [], getSubtotal: (row) => ({ ...row, label: `Total ${row.label.toLowerCase()}`, }), }} rowHeaderConfig={{ header: 'Account', getLabel: (row) => row.label }} expansionConfig={{ value: expanded, onValueChange: setExpanded }} columns={[ { id: 'amount', header: 'Amount', width: 240, cell: (row) => ( ), }, ]} totalConfig={{ label: 'Grand total', row: { id: 'total', label: 'Grand total', amount: 86000 }, }} /> ); }; ``` Collapse Operating expenses. The parent now shows its supplied aggregate; expand it to reveal Salaries, Office rent and a subtotal below the children. The grand total remains independent of expansion. Control expanded IDs with `expansionConfig`. `getChildren` supplies the hierarchy and `getSubtotal` supplies the subtotal record. The app owns every aggregate. Do not add parent aggregates and their children together, or assume averages and ratios are additive. ## Currency and trends [#currency-and-trends] ```tsx import { DataGrid } from '@commenda/pathfinder/data-grid'; import { TableNumberCell } from '@commenda/pathfinder/table'; export const DataGridGuidesFormatting = () => { const rows = [ { id: 'revenue', label: 'Revenue', amount: 125400.5, change: 0.12 }, { id: 'expenses', label: 'Expenses', amount: 86000, change: 0.04 }, { id: 'other', label: 'Other income', amount: null, change: null }, ]; return ( row.id }} rowHeaderConfig={{ header: 'Line item', getLabel: (row) => row.label }} columns={[ { id: 'amount', header: 'Amount', width: 200, cell: (row) => ( ), }, { id: 'change', header: 'Change', width: 180, cell: (row) => ( ), }, ]} /> ); }; ``` Amounts keep two decimal places and a separate USD code. Missing income is an en dash. Both trends point up, but rising expenses are critical while rising revenue is positive: direction does not determine business meaning. `TableNumberCell` formats at the display boundary. Use `unit="USD"` for a separate ISO code; avoid also asking Intl to add a currency symbol unless repetition is intended. `style: 'percent'` expects a fraction (`0.12` → 12%). For whole percentage values use `value={12}` with `suffix="%"` instead. Accounting precision, rounding and conversion belong to the application. ## Frozen headers and scrolling [#frozen-headers-and-scrolling] ```tsx import { DataGrid } from '@commenda/pathfinder/data-grid'; import { TableNumberCell } from '@commenda/pathfinder/table'; export const DataGridGuidesScrolling = () => { const rows = Array.from({ length: 16 }, (_, index) => ({ id: `account-${String(index)}`, label: `Account ${String(index + 1).padStart(2, '0')}`, monthly: (index + 1) * 1250, })); return ( label="Scrollable monthly budget" rows={rows} rowConfig={{ getId: (row) => row.id }} rowHeaderConfig={{ header: 'Account', getLabel: (row) => row.label, width: 200, }} scrollConfig={{ maxHeight: 260 }} columns={['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'].map((month) => ({ id: month, header: month, width: 160, cell: (row) => , }))} totalConfig={{ label: 'Total budget', row: { id: 'total', label: 'Total budget', monthly: rows.reduce((sum, row) => sum + row.monthly, 0), }, }} /> ); }; ``` Scroll through sixteen accounts and six monthly columns. The first column preserves row context, headers stay above the values, and the grand total pins below them. Set a bounded `scrollConfig.maxHeight` rather than allowing a long matrix to dominate the page. At narrow widths, horizontal pinning releases when the label plus one value column cannot fit. Values remain reachable through horizontal scrolling. The example repeats a monthly budget for clarity; real monthly amounts and totals should come from your reporting data. ## Editing allocations [#editing-allocations] ```tsx import { useState } from 'react'; import { DataGrid } from '@commenda/pathfinder/data-grid'; import { DEFlag } from '@commenda/pathfinder/flags/DEFlag'; import { GBFlag } from '@commenda/pathfinder/flags/GBFlag'; import { USFlag } from '@commenda/pathfinder/flags/USFlag'; import { TableCell, TableNumberCell } from '@commenda/pathfinder/table'; export const DataGridEditing = () => { const [rows, setRows] = useState([ { id: 'de', label: 'Meridian GmbH', it: 40, hr: 25, finance: 30, legal: 35, }, { id: 'us', label: 'Meridian US Inc.', it: 35, hr: 45, finance: 35, legal: 40, }, { id: 'uk', label: 'Meridian UK Ltd.', it: 25, hr: 30, finance: 25, legal: 25, }, ]); const [saveStatus, setSaveStatus] = useState('All changes saved'); const sum = (key: 'it' | 'hr' | 'finance' | 'legal') => rows.reduce((total, row) => total + row[key], 0); return (
label="Cost allocation" rows={rows} rowConfig={{ getId: (row) => row.id }} rowHeaderConfig={{ header: 'Entity', getLabel: (row) => row.label, width: 240, cell: (row) => ( ) : row.id === 'uk' ? ( ) : ( ) } /> ), }} columns={(['it', 'hr', 'legal'] as const).map((key) => ({ id: key, header: key === 'it' ? 'IT' : key === 'hr' ? 'HR' : 'Legal', width: 128, cell: (row) => ( 0.001 ? 'critical' : 'neutral' } suffix="%" format={{ minimumFractionDigits: 2, maximumFractionDigits: 2, }} /> ), validationMessage: (row) => row.id === 'total' && Math.abs(row[key] - 100) > 0.001 ? `${key.toUpperCase()} adds up to ${String(row[key])}%. Allocations need to total 100%.` : undefined, editConfig: { editor: { type: 'number', min: 0, max: 100, suffix: '%' }, getValue: (row) => String(row[key]), validate: (value) => value.trim() === '' || !Number.isFinite(Number(value)) || Number(value) < 0 || Number(value) > 100 ? 'Enter a percentage between 0 and 100.' : undefined, onCommit: async (row, value) => { setSaveStatus('Saving changes…'); await new Promise((resolve) => setTimeout(resolve, 250)); setRows((current) => current.map((item) => item.id === row.id ? { ...item, [key]: Number(value) } : item ) ); setSaveStatus('Saved just now'); }, }, }))} totalConfig={{ label: 'Total', row: { id: 'total', label: 'Total', it: sum('it'), hr: sum('hr'), finance: sum('finance'), legal: sum('legal'), }, }} />

{saveStatus}

); }; ``` Click the German entity's Legal allocation, change **35** to **40**, then press Enter. The example saves the value, updates “Saved just now”, and marks the Legal total **105%** with an explanation. A successful save and a valid aggregate are separate states. Only leaf record cells edit; parents, subtotals and totals remain read-only. Enter or leaving a changed text/number cell saves; Escape cancels. Finish or cancel an open editor before starting another. Select/calendar portals remain part of the same editing session. The app owns persistence and the status line. After the request resolves, replace the row immutably with the canonical response. Use server versions or another concurrency policy to reject stale writes from other sessions. ## Save failure and retry [#save-failure-and-retry] ```tsx import { useState } from 'react'; import { Checkbox } from '@commenda/pathfinder/checkbox'; import { DataGrid } from '@commenda/pathfinder/data-grid'; import { TableNumberCell } from '@commenda/pathfinder/table'; export const DataGridPatternsRecovery = () => { const [amount, setAmount] = useState('40.00'); const [rejectSave, setRejectSave] = useState(true); const [status, setStatus] = useState( 'Changes are saved locally in this demo' ); return (
row.id }} rowHeaderConfig={{ header: 'Entity', getLabel: (row) => row.label }} columns={[ { id: 'allocation', header: 'Allocation', width: 240, cell: (row) => ( ), editConfig: { outlined: true, editor: { type: 'number', min: 0, max: 100, suffix: '%' }, getValue: (row) => row.amount, validate: (value) => value.trim() ? undefined : 'Enter an allocation.', onCommit: async (_row, value) => { setStatus('Saving…'); await new Promise((resolve) => setTimeout(resolve, 300)); if (rejectSave) { setStatus('Save failed; your draft is retained'); throw new Error( 'Could not save. Check your connection and retry.' ); } setAmount(value); setStatus('Saved just now'); }, }, }, ]} />

{status}

); }; ``` Change the outlined allocation and press Enter with simulated failure enabled. The draft stays available. Disable the simulation and choose Retry save. Success is announced only after the simulated request resolves; the example makes no network calls. | Event | Result | | --------------------------------------- | ------------------------------------------------------- | | Click, Enter or F2 on an editable value | Opens one editor with its canonical string draft | | Valid changed draft + Enter or leaving | Starts one commit transaction | | Escape before persistence | Restores the original value and cell focus | | Validation failure | Keeps the editor and explanation visible | | Pending request | Prevents duplicate commits | | Rejected request | Retains the draft and exposes Retry save | | Resolved request | Closes the editor; app supplies updated data and status | Use `outlined` for persistent resting borders. Text and number editors use compact 28px sizing; existing Select/DatePicker controls retain their accessible dimensions. Numeric drafts remain strings until validated and parsed by the application. ## Profit and loss [#profit-and-loss] ```tsx import { DataGrid } from '@commenda/pathfinder/data-grid'; import { TableNumberCell } from '@commenda/pathfinder/table'; export const DataGridPivot = () => { interface Line { id: string; label: string; q1: number; q2: number; q3: number; q4: number; previous: number; children?: Line[]; } const rows: Line[] = [ { id: 'revenue', label: 'Revenue', q1: 450000, q2: 480000, q3: 510000, q4: 560000, previous: 1800000, children: [ { id: 'services', label: 'Services', q1: 350000, q2: 380000, q3: 410000, q4: 450000, previous: 1400000, }, { id: 'licences', label: 'Licences', q1: 100000, q2: 100000, q3: 100000, q4: 110000, previous: 400000, }, ], }, { id: 'expenses', label: 'Operating expenses', q1: 185000, q2: 195000, q3: 200000, q4: 210000, previous: 730000, children: [ { id: 'salaries', label: 'Salaries', q1: 140000, q2: 150000, q3: 155000, q4: 165000, previous: 550000, }, { id: 'rent', label: 'Office rent', q1: 30000, q2: 30000, q3: 30000, q4: 30000, previous: 120000, }, { id: 'software', label: 'Software', q1: 15000, q2: 15000, q3: 15000, q4: 15000, previous: 60000, }, ], }, { id: 'costs', label: 'Cost of sales', q1: 95000, q2: 105000, q3: 115000, q4: 130000, previous: 420000, children: [ { id: 'delivery', label: 'Delivery', q1: 95000, q2: 105000, q3: 115000, q4: 130000, previous: 420000, }, ], }, ]; const currency = (value: number) => ( ); const annual = (row: Line) => row.q1 + row.q2 + row.q3 + row.q4; return ( row.id, getChildren: (row) => row.children ?? [], getSubtotal: (row) => ({ ...row, label: `Total ${row.label.toLowerCase()}`, }), }} rowHeaderConfig={{ header: 'Line item', getLabel: (row) => row.label, width: 240, }} expansionConfig={{ defaultValue: ['revenue', 'expenses'] }} columns={[ { id: 'fy2026', header: 'FY 2026', columns: [ ...(['q1', 'q2', 'q3', 'q4'] as const).map((key) => ({ id: key, header: key.toUpperCase(), width: 128, cell: (row: Line) => currency(row[key]), })), { id: 'current', header: 'Total', width: 144, cell: (row) => currency(annual(row)), }, ], }, { id: 'fy2025', header: 'FY 2025', columns: [ { id: 'previous', header: 'Total', width: 144, cell: (row) => currency(row.previous), }, ], }, { id: 'change', header: 'Change vs FY 2025', width: 160, cell: (row) => ( = row.previous ? 'up' : 'down', intent: row.id === 'expenses' || row.id === 'costs' ? 'critical' : 'positive', }} /> ), }, ]} totalConfig={{ label: 'Operating profit', row: { id: 'profit', label: 'Operating profit', q1: 170000, q2: 180000, q3: 195000, q4: 220000, previous: 650000, }, }} /> ); }; ``` This complete example combines year/quarter bands, expandable account groups, period totals, prior-year comparison and operating profit. It demonstrates composition after the smaller examples above. Subtotals and profit are supplied deliberately; the grid is not a formula engine. ## Loading, empty and error [#loading-empty-and-error] ```tsx import { useState } from 'react'; import { Button } from '@commenda/pathfinder/button'; import { DataGrid } from '@commenda/pathfinder/data-grid'; export const DataGridPatternsStates = () => { const [state, setState] = useState<'loading' | 'empty' | 'error'>('loading'); return (
{(['loading', 'empty', 'error'] as const).map((value) => ( ))}
'' }} rowHeaderConfig={{ header: 'Entity', getLabel: () => '' }} columns={[{ id: 'value', header: 'Allocation' }]} stateConfig={{ isLoading: state === 'loading', error: state === 'error' ? { message: 'Could not load allocations. Your reporting scope is preserved.', onRetry: () => { setState('loading'); }, } : undefined, empty: { title: 'No allocations yet', description: 'Choose a reporting period with allocation data.', }, }} />
); }; ``` Switch between skeletons, no allocation data and a retryable failure. These reuse Pathfinder's existing state components. Keep the reporting scope stable during loading and retry; distinguish “no data in this period” from a failed request. ## Keyboard navigation [#keyboard-navigation] | Key | Behavior | | ----------------- | --------------------------------------------------------------- | | Tab | Enters the active owning cell; leaves the grid when not editing | | Arrow keys | Moves among cells; horizontal direction follows RTL | | Home / End | Moves to the row edges | | Ctrl + Home / End | Moves to the first/last grid cell | | Enter / F2 | Edits a value or activates the row-group control | | Escape | Cancels an edit and restores cell focus | Native controls retain their own keyboard interactions while editing. Avoid independently tabbable action clusters in every matrix cell; record-heavy actions usually belong in Table. ## Scope and performance [#scope-and-performance] The grid renders its scoped matrix without virtualization. Use bounded periods/entities and profile representative row and column counts. Storybook's 1,000-row adversarial fixture is a stress scenario, not an unlimited-data performance guarantee. Range selection, fill handle, bulk paste and undo remain outside the Figma v1 scope. See the [Figma source coverage and deliberate differences](/docs/components/data-grid#design-rules) for provenance, dimensions and accessibility adjustments. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `label` | `string` | required | Required accessible grid name | | `rows` | `readonly TRow[]` | required | Root rows; app owns immutable updates | | `columns` | `readonly DataGridColumn[]` | required | Recursive bands or value columns | | `rowConfig` | `{ getId: (row: TRow) => string; getChildren?: (row: TRow) => readonly TRow[]; getSubtotal?: (row: TRow) => TRow \| undefined; }` | required | Stable IDs; optional child and subtotal accessors | | `rowHeaderConfig` | `{ header: string; getLabel: (row: TRow) => string; cell?: (row: TRow) => ReactNode; width?: number; }` | required | Frozen row axis; width defaults to 240px | | `expansionConfig` | `SelectionProps` | optional | Controlled/default expanded row IDs | | `totalConfig` | `{ row: TRow; label: string; }` | optional | App-supplied grand total pinned at the bottom | | `scrollConfig` | `{ maxHeight?: number; }` | optional | Bounded scrolling; default maximum height 480px | | `stateConfig` | `TabularStateConfig` | optional | Skeleton, retryable error and first-use state | | `dataMode` | `"local" \| "remote"` | optional | Local filters matching leaf paths; remote renders supplied rows unchanged | | `searchConfig` | `SelectionProps & { getText?: ((row: TRow) => string) \| undefined; placeholder?: string; }` | optional | Controlled or local search; defaults to row labels | | `filterConfig` | `{ filters: readonly TableFilter[]; operators?: SelectionProps>>; value: Readonly>; onValueChange: (value: Record) => void; }` | optional | Definitions, controlled values/callback, controlled or default operators | | `toolbarConfig` | `TableToolbarConfig` | optional | Primary, secondary, export and overflow actions | | `toolbar` | `ReactNode` | optional | App-owned matrix controls above the grid | | `className` | `string` | optional | Root classes | ### rowConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `getId` | `(row: TRow) => string` | Yes | | | `getChildren` | `((row: TRow) => readonly TRow[])` | No | | | `getSubtotal` | `((row: TRow) => TRow \| undefined)` | No | | ### rowHeaderConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `header` | `string` | Yes | | | `getLabel` | `(row: TRow) => string` | Yes | | | `cell` | `((row: TRow) => ReactNode)` | No | | | `width` | `number` | No | | ### expansionConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `value` | `readonly string[]` | Yes | | | `onValueChange` | `(value: readonly string[]) => void` | Yes | | Do not supply: `defaultValue`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `defaultValue` | `readonly string[]` | No | | | `onValueChange` | `((value: readonly string[]) => void)` | No | | Do not supply: `value`. ### totalConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `row` | `TRow` | Yes | | | `label` | `string` | Yes | | ### scrollConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `maxHeight` | `number` | No | | ### stateConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isLoading` | `boolean` | No | | | `error` | `{ message: string; onRetry: () => void; }` | No | | | `empty` | `{ title: string; description?: string; action?: TableAction; }` | No | | ### stateConfig.error fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `message` | `string` | Yes | | | `onRetry` | `() => void` | Yes | | ### stateConfig.empty fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | `string` | Yes | | | `description` | `string` | No | | | `action` | `TableAction` | No | | ### stateConfig.empty.action fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | `string` | Yes | | | `label` | `string` | Yes | | | `onAction` | `() => void` | Yes | | | `disabled` | `boolean` | No | | ### filterConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | `readonly TableFilter[]` | Yes | | | `operators` | `SelectionProps>>` | No | | | `value` | `Readonly>` | Yes | | | `onValueChange` | `(value: Record) => void` | Yes | | ### filterConfig.operators fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `value` | `Readonly>` | Yes | | | `onValueChange` | `(value: Readonly>) => void` | Yes | | Do not supply: `defaultValue`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `defaultValue` | `Readonly>` | No | | | `onValueChange` | `((value: Readonly>) => void)` | No | | Do not supply: `value`. ### toolbarConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `actions` | `readonly TableAction[]` | No | Prefer named actions. Legacy actions use first=primary, second=secondary, remainder=overflow. | | `primaryAction` | `(TableAction & { menu?: DropdownMenu; icon?: ReactNode; })` | No | | | `secondaryAction` | `TableAction` | No | | | `onExport` | `(() => void)` | No | | | `moreMenu` | `DropdownMenu` | No | | ### toolbarConfig.secondaryAction fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | `string` | Yes | | | `label` | `string` | Yes | | | `onAction` | `() => void` | Yes | | | `disabled` | `boolean` | No | | ### toolbarConfig.moreMenu fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `groups` | `DropdownGroup[]` | Yes | | A column group is `{ id, header, columns }`; groups can nest recursively. A value column is `{ id, header, value?, cell?, width?, align?, editConfig?, validationMessage? }`. Default width is 128px and alignment is `end`. IDs must be unique across the column tree; groups cannot be empty. Row IDs must be unique throughout an acyclic row tree. Invalid recursive structures throw a clear configuration error. Use `cell` with TableCell/TableNumberCell for entity flags, numeric/currency formatting and trends. Header bands span their descendant leaves; shorter branches span the remaining header levels. The first column and headers remain visible while scrolling horizontally and vertically. If the container cannot fit the row header plus a value column, horizontal pinning releases so values remain reachable; column headers still pin vertically. Keyboard arrows follow logical order in RTL. ### Editing configuration [#editing-configuration] A value column's `editConfig` contains: | Property | Type | Behavior | | ---------- | --------------------------------------- | ------------------------------------------------------ | | `editor` | TableCellEditor | Text, numeric draft, Select or segmented date/calendar | | `outlined` | boolean | Keeps resting editor outlines visible | | `getValue` | `(row) => string` | Editable canonical draft | | `canEdit` | `(row) => boolean` | Optional app permission predicate | | `validate` | `(value, row) => string or undefined` | App validation before saving | | `onCommit` | `(row, value) => void or Promise` | App persistence and row update | Only leaf record rows edit; groups, subtotals and totals stay read-only. One editor opens at a time. Leaving a changed cell saves; Enter saves text/numbers; Escape discards the draft. Pending saves cannot duplicate. Rejection retains the draft with a readable error and retry. While an editor is still open, finish or cancel it before editing another cell. Do not silently accept failed requests. `validationMessage(row)` exposes invalid totals with an accessible explanation and error color. The allocation example calculates totals in its application layer; changing Legal from 35 to 40 produces a 105% invalid total, not a modal alert. Number strings are parsed by the app after validation. Financial precision, permissions, stale-write protection and server transactions remain application responsibilities. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Shared cells [#shared-cells] - **CELL-001:** Text starts at the logical leading edge; numbers align at the trailing edge and use tabular figures. Empty values show an en dash. Entity flags are 16px, person avatars 20px. Entity names wrap; do not truncate monetary amounts. Cells compose existing Badge, Tag, Avatar, ProgressBar and exact icon assets. - **CELL-002:** Currency units remain quiet and separate from values. Fixed decimals are supplied via Intl format options. Trend direction and semantic intent are separate. A cost increase can be critical even though its arrow points up. - **CELL-003:** At most primary bordered icon, secondary icon and overflow in a row action group. Do not repeat text CTAs in every row. - **CELL-EDIT-001:** Compact text/number editor minimum 28px and radius 6px; calm border at rest, outline on hover/focus, persistent invalid treatment. Existing Select and DatePicker keep their accessible field geometry; the DatePicker retains the user's approved Kakashi DateField padding rather than shrinking its segmented inputs. - **CELL-EDIT-002:** Enter or leaving the editor commits one cell; Escape cancels. Pending work cannot duplicate; errors retain drafts and offer retry. React portal focus stays within the same editing session. Application code validates and persists canonical values. The Figma cell master depicts combinations, not 32 separate exported component classes. The shared content family composes those visual patterns. Toggle and upload actions use existing Switch/FileUpload application workflows in custom cells; they do not pretend to be string editors. TableCell content, TableNumberCell, TableRowActions and TableEditCell work in both owning components. ### Matrix grid [#matrix-grid] - **GRID-001:** Pearl-100 headers, 36px bands, 40px minimum body rows, 0.5px vertical/horizontal gridlines. Freeze the first column and headers. Grouped columns recurse; row-header width defaults to 240px and value columns to 128px. - **GRID-002:** Collapsed groups display supplied aggregates. Expanded groups place supplied subtotals below children; subtotal background is pearl-100. Grand total pins to the bottom with a pearl-400 1px top border. No inferred formulas or double-counting. - **GRID-003:** One active cell with inset blue-700 outline and blue-200 ring. Arrow keys navigate; Enter/F2 edits or opens a group; Escape cancels. No range selection, fill handle, bulk paste, undo or virtualization in this version. - **GRID-004:** Only leaf record cells edit. Totals/aggregates remain read-only. Invalid totals explain the problem with error styling and a tooltip, not a modal. The app owns calculations, precision, constraints, permission checks and persistence. Quiet secondary text uses pearl-800 where the design's lighter text would fail contrast at these small sizes. Narrow layouts release horizontal pinning when the frozen columns would hide readable values and scroll within their own container; financial values retain their width. Storybook's shared worst-case fixture covers long CJK/emoji names, large negative values, 1,000 paginated records, empty data, RTL and a 320px container. ### Explicit differences and boundaries [#explicit-differences-and-boundaries] | Reference | Coverage | | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [Cell master 430:13537](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=430-13537) | Text/description/icon/badge, people/groups/email, entities, files, tags, boolean, progress, copy/link, numbers/units/currency/trends, loading and totals compose TableCell, TableNumberCell and TableRowActions. Tree parent/child/grandchild behavior belongs to the recursive DataGrid examples. Hover and selection share the row tint in both densities. | - **Accessibility:** quiet text uses pearl-800 for contrast. Long content and text zoom can grow rows. Narrow viewports release horizontal pinning. Copy controls remain visible on touch, and long status labels expose their full text on focus/hover. - **Application patterns:** presets, summary aggregates, “Saved just now”, network requests, exports, permissions and financial calculations are owned by the app. Demos perform local updates or simulated requests and say so. - **States and scale:** matrix loading/error fallbacks reuse existing primitives. No virtualization, range operations or spreadsheet formulas are claimed. Stress fixtures do not establish an unlimited-data performance guarantee. AI consumers should read this page together with the component API and integration pages. Do not infer a public prop from a Figma variant name, or treat a prototype's sample amounts as domain validation rules. ### Header and cell master reconciliation [#header-and-cell-master-reconciliation] | Reference | Coverage | | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [Header master 430:13536](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=430-13536) | Start/center/end alignment, unsorted/ascending/descending, hover/focus, optional information, checkbox and empty action header; Headers, Selection and Pinning examples. | | [Groups and add row 445:13858](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=445-13858) | Grouped filings show flags, counts, collapse and page-selection persistence; editable lines show totals and Add line. Application callbacks own actual persistence. | Long content and zoom can grow rows beyond Figma's nominal heights. Linear progress and ring progress use their existing Pathfinder primitives; cell recipes do not introduce a new progress implementation. A file-picker example selects a local file and displays its name; production upload, progress and retry belong to the application's upload workflow. These are deliberate compositional boundaries, not built-in backend features. ## Search and filters [#search-and-filters] ```tsx import { useState } from 'react'; import { DataGrid } from '@commenda/pathfinder/data-grid'; const records = [ { id: 'uk', label: 'Meridian UK', jurisdiction: 'GB', status: 'review', due: '2026-10-20', amount: 1200, }, { id: 'us', label: 'Meridian US', jurisdiction: 'US', status: 'complete', due: '2026-11-10', amount: 2400, }, { id: 'de', label: 'Meridian Germany', jurisdiction: 'DE', status: 'review', due: '2026-10-25', amount: 800, }, ]; export const DataGridFiltersDefault = () => { const [filters, setFilters] = useState>({}); return ( row.id }} rowHeaderConfig={{ header: 'Entity', getLabel: (row) => row.label }} columns={[ { id: 'jurisdiction', header: 'Country', value: (row) => row.jurisdiction, align: 'start', }, { id: 'amount', header: 'Allocation', value: (row) => row.amount }, { id: 'due', header: 'Due date', value: (row) => row.due }, ]} searchConfig={{ defaultValue: '' }} filterConfig={{ value: filters, onValueChange: setFilters, filters: [ { id: 'jurisdiction', label: 'Country', type: 'multiple', searchable: true, getValue: (row) => row.jurisdiction, options: [ { value: 'GB', label: 'United Kingdom' }, { value: 'US', label: 'United States' }, { value: 'DE', label: 'Germany' }, ], }, { id: 'status', label: 'Status', type: 'single', getValue: (row) => row.status, options: [ { value: 'review', label: 'In review' }, { value: 'complete', label: 'Complete' }, ], }, { id: 'due', label: 'Due date', type: 'date', getValue: (row) => row.due, presets: [ { id: 'october', label: 'October 2026', start: '2026-10-01', end: '2026-10-31', }, ], }, { id: 'amount', label: 'Allocation', type: 'number', initiallyVisible: false, getValue: (row) => row.amount, }, ], }} /> ); }; ``` `searchConfig`, `filterConfig` and `toolbarConfig` share Table’s contracts. The grid reuses its single/multiple choice, searchable options, date presets/ranges, numeric operators, active chips, overflow and clear controls. Supply `toolbarConfig` for named primary/secondary/overflow/export actions; `toolbar` remains available for custom controls. Local mode searches row labels by default; `searchConfig.getText` adds searchable fields. Filters combine with AND between fields. Matching leaves retain and expand their ancestor paths, including groups whose labels match the search. Clearing the search and filters restores the previous expansion state. Expansion controls are unavailable while showing filtered paths. **GRID-FILTER-001:** Local filtering hides subtotals and grand totals because arbitrary cell values cannot safely be aggregated. Clear filters to restore complete totals. If the API computes filtered aggregates, pass those with `dataMode="remote"`. ### API-owned filtering [#api-owned-filtering] ```tsx import { useState } from 'react'; import { DataGrid, type DataGridFilterOperator, } from '@commenda/pathfinder/data-grid'; const records = [ { id: 'uk', label: 'Meridian UK', jurisdiction: 'GB', status: 'review', due: '2026-10-20', amount: 1200, }, { id: 'us', label: 'Meridian US', jurisdiction: 'US', status: 'complete', due: '2026-11-10', amount: 2400, }, { id: 'de', label: 'Meridian Germany', jurisdiction: 'DE', status: 'review', due: '2026-10-25', amount: 800, }, ]; export const DataGridFiltersRemote = () => { const [query, setQuery] = useState(''); const [operators, setOperators] = useState< Readonly> >({}); const [filters, setFilters] = useState>({}); // Simulates a server response. Real applications fetch with query/filter/operator state and cancel stale requests. const response = records.filter( (row) => row.label.toLowerCase().includes(query.toLowerCase()) && (!Object.hasOwn(filters, 'status') || filters.status.length === 0 || (operators.status === 'isNot' ? !filters.status.includes(row.status) : filters.status.includes(row.status))) ); return (

Simulated API response. Search and filter callbacks own the request state.

row.id }} rowHeaderConfig={{ header: 'Entity', getLabel: (row) => row.label }} columns={[ { id: 'amount', header: 'Allocation', value: (row) => row.amount }, ]} searchConfig={{ value: query, onValueChange: setQuery }} filterConfig={{ operators: { value: operators, onValueChange: setOperators }, value: filters, onValueChange: setFilters, filters: [ { id: 'status', label: 'Status', type: 'single', options: [ { value: 'review', label: 'In review' }, { value: 'complete', label: 'Complete' }, ], }, ], }} />
); }; ``` **GRID-FILTER-002:** In `dataMode="remote"`, the component renders the rows supplied by the application without applying local filtering. Control query, values and operators with their callbacks, cancel stale requests, and supply `stateConfig` for loading, errors and retry. The example simulates a response in memory; it does not perform a network request. --- # Date picker Package: @commenda/pathfinder@0.1.0 Page: /docs/components/date-picker Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=188-2 ```tsx import { DatePicker } from '@commenda/pathfinder/date-picker'; export const DatePickerDefault = () => ( ); ``` ## Installation [#installation] Import the component in your screen: ```tsx import { DatePicker } from '@commenda/pathfinder/date-picker'; ``` Load the stylesheet once in your app root, after host styles: ```tsx import '@commenda/pathfinder/styles.css'; ``` Pathfinder ships precompiled CSS; no Tailwind setup is required. In Next.js App Router, put interactive state and callbacks in a client component. See [installation](/docs/getting-started/installation) for the workspace setup. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `mode` | `"single" \| "range" \| "month"` | conditional | `'single'`; discriminates value and constraints | | `value` | `string \| null \| DateRange` | conditional | Single `YYYY-MM-DD`, month `YYYY-MM`, range `{ start, end }` of date strings/null | | `defaultValue` | `string \| null \| DateRange` | optional | Single `YYYY-MM-DD`, month `YYYY-MM`, range `{ start, end }` of date strings/null | | `onValueChange` | `((value: string \| null) => void) \| ((value: DateRange) => void)` | conditional | Controlled mode requires it. Receives valid values, explicit clears, or partial ranges | | `format` | `"us" \| "in" \| "iso"` | optional | `'iso'`; MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD | | `calendarConfig` | `false \| { weekStartsOn?: 0 \| 1; }` | optional | Calendar enabled, Sunday first; false is typed-only | | `constraintConfig` | `DateConstraints \| MonthConstraints` | optional | Single/range date constraints; month mode uses `isMonthUnavailable` and month strings | | `presetConfig` | `{ items: readonly { id: string; label: string; value: DateRange; }[]; }` | optional | Range-only; presets obey the same constraints | | `clearConfig` | `{ label?: string; }` | optional | Optional clear action | | `name` | `string` | optional | Single/month submit one ISO value; range submits `name.start` and `name.end` | | `form` | `string` | optional | Single/month submit one ISO value; range submits `name.start` and `name.end` | | `label` | `string` | conditional | Visible field label. Supply an accessible name when it is omitted. | | `hideLabel` | `boolean` | optional | Visually hide the label while preserving its accessible name. | | `aria-label` | `string` | conditional | Defines a string value that labels the current element. | | `required` | `boolean` | conditional | Mark the field as required. See validation guidance for the component. | | `labelConfig` | `{ optionalLabel: string; help?: never; } \| { help: string; optionalLabel?: never; }` | optional | Choose either `optionalLabel` or `help`, never both. `never` excludes the other field. | | `description` | `string` | optional | Supporting guidance associated with the field. | | `errorConfig` | `{ message: string; reveal?: "after-blur" \| "immediate"; }` | optional | Validation message and when it becomes visible. | | `disableConfig` | `DisableConfig` | optional | Disabled state with an optional explanation. | | `id` | `string` | optional | Stable HTML identifier for the component. | | `className` | `string` | optional | Additional classes for the component root. | | `readOnly` | `boolean` | optional | Allow reading the current value without editing it. | | `onBlur` | `FocusEventHandler` | optional | Called when focus leaves the component. | | `onFocus` | `FocusEventHandler` | optional | Called when focus enters the component. | ### value fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `start` | `string \| null` | Yes | | | `end` | `string \| null` | Yes | | ### defaultValue fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `start` | `string \| null` | Yes | | | `end` | `string \| null` | Yes | | ### calendarConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `weekStartsOn` | `0 \| 1` | No | | ### constraintConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `min` | `string` | No | | | `max` | `string` | No | | | `isDateUnavailable` | `((date: string) => boolean)` | No | | #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `min` | `string` | No | | | `max` | `string` | No | | | `isMonthUnavailable` | `((month: string) => boolean)` | No | | ### presetConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | `readonly { id: string; label: string; value: DateRange; }[]` | Yes | | ### clearConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | No | | ### labelConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `optionalLabel` | `string` | Yes | | Do not supply: `help`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `help` | `string` | Yes | | Do not supply: `optionalLabel`. ### errorConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `message` | `string` | Yes | | | `reveal` | `"after-blur" \| "immediate"` | No | | ### disableConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isDisabled` | `boolean` | Yes | | | `reason` | `string` | No | | Shared presentation and `readOnly` follow [Input](/docs/components/input). Refs and native form behavior differ because this is a composite editor. Set valid ISO strings in controlled props; invalid or incomplete drafts remain in the editor and block native form submission. They never silently revert on blur. A changed external value replaces the draft; form reset restores uncontrolled defaults and clears draft edits. ## Typing and calendar selection [#typing-and-calendar-selection] The parent field is the Kakashi-derived DateField: type or adjust its day, month, and year segments directly. The calendar icon opens the matching day, range, or month picker. Choosing a date updates those same segments and the same ISO value; neither interaction owns a second date state. Invalid drafts stay visible until corrected. ## Range and presets [#range-and-presets] ```tsx import { DatePicker } from '@commenda/pathfinder/date-picker'; export const DatePickerRange = () => ( ); ``` Click a day to start a new range; the calendar stays open while you choose its end. The second click completes the range, including a same-day range, and closes the calendar. Choosing an earlier second day orders the two dates chronologically. End must be on or after start. A range cannot cross an unavailable date. Wide screens show two months; narrow screens show one navigable month. For large selectable periods, provide practical application min/max bounds so predicate checks stay inexpensive. ## Month [#month] ```tsx import { DatePicker } from '@commenda/pathfinder/date-picker'; export const DatePickerMonth = () => ( ); ``` ## Typed-only [#typed-only] ```tsx import { DatePicker } from '@commenda/pathfinder/date-picker'; export const DatePickerTyped = () => ( ); ``` Each segment has an accessible name. Arrow keys move between or adjust segments. Dates are calendar strings; do not convert through `new Date('YYYY-MM-DD').toISOString()` in application adapters. ## Constraints [#constraints] ```tsx import { DatePicker } from '@commenda/pathfinder/date-picker'; export const DatePickerConstraints = () => ( date === '2026-10-15', }} description="October only; the 15th is unavailable. Typed dates use the same constraints." /> ); ``` ## States [#states] ```tsx import { DatePicker } from '@commenda/pathfinder/date-picker'; export const DatePickerStates = () => (
); ``` Read [composite field rules](/docs/components/date-picker#design-rules). ## Design rules [#design-rules] The calendar follows [Calendar](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=190-587), [day cells](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=188-33), and [month cells](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=188-50). - The typed block follows Kakashi DateField spacing: 40px minimum height, 8px horizontal padding, 24px-tall segments, 40px year and 28px month/day widths, 2px gaps, and 2px separator padding. This user-requested spacing overrides Figma's compact 28px field. The 16px calendar icon sits in a 24px action target. - The floating card has a 10px radius, pearl-300 hairline, menu shadow, and 12px calendar padding. - Navigation is 24px; weekday labels are 24px tall; days are 32px square with 2px between weeks. Month options are 72 × 32px. - Selected dates and range ends use blue-700. Interior dates form a continuous blue-100 band. Today has a 4px dot. Presets show a check when their range matches. - Outside days and weekday labels use pearl-800 for readable contrast, a deliberate adjustment from Figma's lighter colors. - At narrow widths the range calendar shows one month and puts supplied presets above the calendar. Preset dates and labels remain application-owned. ### Reference rules [#reference-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Dates [#dates] - **DATE-001:** Single, range, month, and typed-only use Kakashi DateField spacing per the user override: minimum height 40px, horizontal inset 8px, fixed 40px year and 28px month/day segments, 24px segment height, 2px segment gaps and separator padding. Locale segment order is explicit through `format`; default is ISO. - **DATE-002:** Calendar 190:587, day cell 188:33, and month cell 188:50: offset 4px, radius 10, pearl-300 hairline, 12px padding, 24px navigation and weekday row, 32px days with 2px week gaps, 72 × 32px month options. Sunday first; selected blue-700, continuous blue-100 range band, 4px today dot, unavailable dates disabled and muted. These exact cell frames supersede the older page note about strikethrough. Supplied presets have a divided sidebar and selection check; narrow screens place presets above one month. Weekday/outside text is strengthened to pearl-800 for contrast. - **DATE-003:** Private DateField derives from Kakashi's editing/navigation engine, with Pathfinder tokens and no private workspace runtime imports. Keep incomplete/invalid drafts visible; never replace them with the old committed value on blur. - **DATE-004:** ISO date-only strings, no UTC conversion. Typed input, calendar, and presets use identical constraints. Typed ranges reject reversed dates and ranges crossing unavailable days. Calendar selection uses two clicks: the first starts a new range and keeps the popup open; the second completes it (including the same day). Month mode uses month predicates. - **DATE-005:** `calendarConfig={false}` removes the icon/popover. Composite refs target the group. Native forms submit ISO values; invalid drafts cannot submit an older saved value. - **DATE-006:** First calendar click starts a fresh range and keeps the popup open. Second click completes it, including a same-day range. Display a prompt for the pending end date. Selected days retain their blue fill on hover. ### Explicit differences and boundaries [#explicit-differences-and-boundaries] - **Editors:** Select and DatePicker retain their existing control heights; they are not pixel-identical to compact picker drawings. Text/number editors use the compact geometry. Async save errors remain visibly actionable with retry. ### Shared rules [#shared-rules] Labels activate the first input; every date segment has its own accessible label. Descriptions, disabled reasons, and validation messages stay associated with the field. Error icons center on the first 18px text line, including wrapped messages. Controlled values require callbacks; uncontrolled form resets restore defaults. Public props expose application values, never private primitive or calendar objects. Styles ship precompiled for React 18/19 and Next.js consumers. Date segments use native text selection as their single editing highlight. Focusing a segment selects its text for replacement; the field retains its own outer focus or validation border. --- # Description list Package: @commenda/pathfinder@0.1.0 Page: /docs/components/description-list Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=967-2 ```tsx import { Badge } from '@commenda/pathfinder/badge'; import { DescriptionList } from '@commenda/pathfinder/description-list'; import { USFlag } from '@commenda/pathfinder/flags/USFlag'; export const DescriptionListDefault = () => ( , }, { id: 'status', label: 'Status', value: , }, { id: 'tax', label: 'Tax ID', value: null }, ]} /> ); ``` Import from `@commenda/pathfinder/description-list`, and load the shared stylesheet once. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `items` | `readonly DescriptionItem[]` | required | Stable IDs, label, and ReactNode value | | `layout` | `"inline" \| "auto" \| "stacked"` | optional | `auto`: stack below 480px of container width | | `size` | `"default" \| "compact"` | optional | Default 36px inline minimum; compact 28px | | `emptyLabel` | `string` | optional | `Not set`; zero and false remain visible | | `className` | `string` | optional | Root dl styling | Each item supports `localLabel`, `help` text, `leadingIcon`, `copyConfig: { text, onCopy?, onError? }`, and `editConfig`. Editable items use `{ kind: 'editable', onEdit }`; protected items use `{ kind: 'locked', reason, onRequestChange }`. The application opens its editor or change-request flow. Copy text is explicit, including when the displayed value is a badge or link. ## Copy and edit [#copy-and-edit] ```tsx import { useState } from 'react'; import { DescriptionList } from '@commenda/pathfinder/description-list'; export const DescriptionListActions = () => { const [status, setStatus] = useState(''); return ( <> { setStatus('Change request opened.'); }, }, }, { id: 'website', label: 'Website', value: example.com, editConfig: { kind: 'editable', onEdit: () => { setStatus('Website editor opened.'); }, }, }, ]} /> {status} ); }; ``` Copy success appears only after the clipboard write succeeds. Failed writes announce an error. Actions appear on hover, keyboard focus, and touch without moving content. Protected fields remain readable and request a change rather than opening an inline editor. ## Compact panels [#compact-panels] ```tsx import { DescriptionList } from '@commenda/pathfinder/description-list'; export const DescriptionListCompact = () => ( ); ``` Use a Card with `padding="none"` for section framing. DescriptionList supplies its own 8px inset. Section titles and section-level Edit actions belong to Card. Read the [record component rules](/docs/components/description-list#design-rules). ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Source rules [#source-rules] | Rule | Contract and provenance | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DESCRIPTION-001 | Figma 967:9 / 969:377: 8px list inset, 2px row gaps, 200px inline label column; stack below 480px container width. Default/compact row heights are minimums, allowing wrapping. | | DESCRIPTION-002 | Native dl/dt/dd; zero/false are values. Actions reserve space and appear on focus/touch as well as hover. | | DESCRIPTION-003 | Copy explicit text, announce real success/failure. Locked edit requests a change; apps own editor surfaces. | ### Recorded design decisions [#recorded-design-decisions] Description row source/provenance, sensitive-value reveal, and edit-history explorations are not combined into the initial approved row interface. Consumers can compose record value content; future named behavior needs a chosen design contract. --- # Dialog Package: @commenda/pathfinder@0.1.0 Page: /docs/components/dialog Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=372-2 ```tsx import { useState } from 'react'; import { Button } from '@commenda/pathfinder/button'; import { Dialog } from '@commenda/pathfinder/dialog'; import { Input } from '@commenda/pathfinder/input'; export const DialogDefault = () => { const [open, setOpen] = useState(false); const [name, setName] = useState('Acme Inc.'); return ( Edit entity} open={open} onOpenChange={setOpen} footerConfig={{ primaryAction: { label: 'Save changes', type: 'submit', form: 'entity-dialog-form', }, }} >
{ event.preventDefault(); setOpen(false); }} >
); }; ``` Import `Dialog` and `ConfirmDialog` from `@commenda/pathfinder/dialog`. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `title` | `string` | required | Required accessible heading | | `description` | `ReactNode` | optional | Optional supporting text | | `trigger` | `ReactElement>` | optional | Optional ref-forwarding button | | `children` | `ReactNode` | required | Form or focused task body | | `size` | `"sm" \| "md" \| "lg" \| "xl"` | optional | lg; 400 / 480 / 600 / 800px, viewport-clamped | | `footerConfig` | `OverlayFooterConfig` | optional | Optional footer | | `dismissConfig` | `OverlayDismissConfig` | optional | Outside dismissal enabled; guard may be async | | `focusConfig` | `OverlayFocusConfig` | optional | First body control, then popup; returns to trigger | | `className` | `string` | optional | Popup classes | | `open` | `boolean` | conditional | | | `onOpenChange` | `((open: boolean) => void)` | conditional | | | `defaultOpen` | `boolean` | optional | | ### footerConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `primaryAction` | `OverlayAction` | No | | | `secondaryAction` | `OverlayAction` | No | | | `leadingContent` | `ReactNode` | No | | | `cancelLabel` | `string` | No | | ### dismissConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `outsidePress` | `boolean` | No | | | `beforeClose` | `((reason: OverlayCloseReason) => boolean \| Promise)` | No | | | `onError` | `((error: unknown) => void)` | No | | ### focusConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `initialFocusRef` | `RefObject` | No | | | `returnFocusRef` | `RefObject` | No | | `open` with `onOpenChange` is controlled; otherwise use `defaultOpen`. Primary actions never imply success or automatically close the surface. Close it after the application completes its work. `footerConfig` accepts `primaryAction`, `secondaryAction`, `leadingContent`, and `cancelLabel`. Actions have `label`, `onClick`, native `type`/`form`, `disabled`, `disabledReason`, `loadingConfig`, `destructive`, and icons. Without a secondary action the footer renders Cancel. A footer is absent unless configured. Submit uses native form IDs; IDs must be unique. ## Sizes [#sizes] ```tsx import { Button } from '@commenda/pathfinder/button'; import { Dialog } from '@commenda/pathfinder/dialog'; export const DialogSizes = () => (
{(['sm', 'md', 'lg', 'xl'] as const).map((size) => ( {size}} footerConfig={{}} >

Choose the smallest size that comfortably fits the task.

))}
); ``` ## Confirmation [#confirmation] ```tsx import { useState } from 'react'; import { Button } from '@commenda/pathfinder/button'; import { ConfirmDialog } from '@commenda/pathfinder/dialog'; export const DialogConfirmation = () => { const [open, setOpen] = useState(false); const [deleted, setDeleted] = useState(false); return ( <> Delete policy } open={open} onOpenChange={setOpen} confirmAction={{ label: 'Delete policy', destructive: true, onClick: () => { setDeleted(true); setOpen(false); }, }} /> {deleted ? 'Policy deleted.' : ''} ); }; ``` `ConfirmDialog` requires `title`, `description`, and `confirmAction`. It also accepts the same open state, trigger, focus config, a `cancelLabel`, and dismissal guard. It uses alert-dialog semantics, focuses Cancel, has no close icon, and ignores outside presses. A loading confirmation blocks dismissal until the application completes or reports an error. ## Scrolling and nested controls [#scrolling-and-nested-controls] ```tsx import { Button } from '@commenda/pathfinder/button'; import { Dialog } from '@commenda/pathfinder/dialog'; import { Select } from '@commenda/pathfinder/select'; export const DialogScrolling = () => ( Review filings} footerConfig={{ primaryAction: { label: 'Save review', onClick: () => {} }, }} > ); ``` ## Installation [#installation] Import the component in your screen: ```tsx import { Input } from '@commenda/pathfinder/input'; ``` Load the stylesheet once in your app root, after host styles: ```tsx import '@commenda/pathfinder/styles.css'; ``` Pathfinder ships precompiled CSS; no Tailwind setup is required. In Next.js App Router, put interactive state and callbacks in a client component. See [installation](/docs/getting-started/installation) for the workspace setup. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `type` | `"password" \| "number" \| "search" \| "text" \| "email" \| "url" \| "tel"` | conditional | text | | `leadingIcon` | `ReactElement>` | optional | Decorative 16px slot | | `prefix` | `string` | optional | Non-editable unit segments | | `suffix` | `string` | optional | Non-editable unit segments | | `actionConfig` | `FieldActionConfig \| CopyAction \| { kind: "clear"; label?: string; }` | optional | Reveal is password-only; copy supports onCopyError, reveal supports hideLabel | | `label` | `string` | conditional | Visible field label. Supply an accessible name when it is omitted. | | `hideLabel` | `boolean` | optional | Visually hide the label while preserving its accessible name. | | `aria-label` | `string` | conditional | Accessible name when the field has no visible label. | | `required` | `boolean` | conditional | Mark the field as required. See validation guidance for the component. | | `labelConfig` | `{ optionalLabel: string; help?: never; } \| { help: string; optionalLabel?: never; }` | optional | Choose either `optionalLabel` or `help`, never both. `never` excludes the other field. | | `description` | `string` | optional | Supporting guidance associated with the field. | | `errorConfig` | `{ message: string; reveal?: "after-blur" \| "immediate"; }` | optional | Validation message and when it becomes visible. | | `disableConfig` | `DisableConfig` | optional | Disabled state with an optional explanation. | | `value` | `string` | conditional | | | `onValueChange` | `((value: string) => void)` | conditional | | | `defaultValue` | `string` | optional | | ### actionConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `kind` | `"copy"` | Yes | | | `label` | `string` | No | | | `onCopyError` | `((error: Error) => void)` | No | | #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `kind` | `"clear"` | Yes | | | `label` | `string` | No | | #### Option 3 | Field | Type | Required | Description | | --- | --- | --- | --- | | `kind` | `"reveal"` | Yes | | | `label` | `string` | No | | | `hideLabel` | `string` | No | | ### labelConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `optionalLabel` | `string` | Yes | | Do not supply: `help`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `help` | `string` | Yes | | Do not supply: `optionalLabel`. ### errorConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `message` | `string` | Yes | | | `reveal` | `"after-blur" \| "immediate"` | No | | ### disableConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isDisabled` | `boolean` | Yes | | | `reason` | `string` | No | | ## Shared field props [#shared-field-props] | Prop | Type | Behavior | | --------------- | --------------------------------------------------- | ------------------------------------------------------------ | | `label` | `string` | Visible accessible label; hideLabel hides it visually. | | `aria-label` | `string` | Accessible name when a visible label is not supplied. | | `value` | `string` | Controlled value; provide the corresponding change callback. | | `defaultValue` | `string` | Initial value for an uncontrolled control. | | `onValueChange` | `(value: string) => void` | Immediate editing changes; no implicit parsing. | | `description` | `string` | Visible supporting guidance. | | `required` | `boolean` | Native required semantics and visual asterisk. | | `labelConfig` | `{ optionalLabel }` or `{ help }` | One annotation; cannot combine with required. | | `errorConfig` | `{ message, reveal?: 'after-blur' \| 'immediate' }` | Replaces guidance after blur by default. | | `disableConfig` | `{ isDisabled, reason? }` | Disables editing; reason stays visible. | | `readOnly` | `boolean` | Preserves focus, selection, and form submission. | Native `name`, `form`, autocomplete, focus/blur handlers, constraints, IDs, ARIA attributes, and typed refs are supported. `className` styles the outer field. Use `onValueChange` for text changes. Form reset restores uncontrolled defaults; controlled values remain application-owned. ## Usage rules [#usage-rules] Use noun labels in sentence case, examples as placeholders, and actionable error messages. One authored 28px size with radius 6. Copy remains available when editing is disabled. Clear never mutates disabled/read-only input. Password reveal changes visibility without changing the value. Numeric input retains native string editing; currency formatting is not included. ### Segments [#segments] ```tsx import { Input } from '@commenda/pathfinder/input'; export const InputSegments = () => ( ); ``` ### Actions [#actions] ```tsx import { Input } from '@commenda/pathfinder/input'; export const InputActions = () => (
); ``` ### States [#states] ```tsx import { Input } from '@commenda/pathfinder/input'; export const InputStates = () => (
); ``` ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Source rules [#source-rules] | Rule | Source | Contract | | --------- | ---------------- | ----------------------------------------------------------------------------- | | INPUT-001 | 68:225, 158:222 | 28px field, radius 6, pearl control border, blue focus glow. | | INPUT-002 | 158:318, 158:819 | One label annotation; units use prefix/suffix, actions use their own segment. | | INPUT-003 | 158:807, 158:816 | Sentence-case noun label; actionable error replaces guidance after blur. | | INPUT-004 | 158:482 | Copy remains available for disabled values. | All source IDs refer to [The System](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=68-2). ### Explicit implementation decisions [#explicit-implementation-decisions] True native readOnly remains distinct from disabled despite Figma 158:822 using disabled appearance for non-editable fields. Disabled values do not submit; read-only values do. Disabled editing does not disable the independent copy/reveal actions. Clear cannot modify a disabled/read-only field. Error reveal defaults to after-blur; immediate reveal supports submission/server errors. Soft-limit feedback is immediate. Applications own validation schemas and server error clearing. Additional required/help guidance belongs in description when the single label annotation is already occupied. Optional-label text uses pearl 800 instead of the authored pearl 700: browser axe measured the latter at 3.38:1 on white, below the 4.5:1 requirement for this text. --- # List Package: @commenda/pathfinder@0.1.0 Page: /docs/components/list Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=1101-2 ```tsx import { useState } from 'react'; import { USFlag } from '@commenda/pathfinder/flags/USFlag'; import { List } from '@commenda/pathfinder/list'; export const ListDefault = () => { const [selected, setSelected] = useState('annual'); const [pinned, setPinned] = useState(false); return ( , }, unread: true, pinned, onAction: () => {}, menuConfig: { groups: [ { id: 'actions', items: [ { id: 'pin', type: 'action', label: pinned ? 'Unpin record' : 'Pin record', onClick: () => { setPinned(!pinned); }, }, ], }, ], }, }, { id: 'tax', title: 'Tax registration documents', description: 'Your documents are ready for review.', timestamp: { label: 'Yesterday' }, onAction: () => {}, }, ]} /> ); }; ``` Import from `@commenda/pathfinder/list` and load the shared stylesheet once. This component represents inbox/mailroom records, not bullet lists or selection options. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `label` | `string` | required | Accessible list name | | `items` | `readonly ListItem[]` | required | Stable ID and title; each item has `onAction` or `linkConfig` | | `selectedId` | `string \| null` | optional | The currently open record | | `onSelectionChange` | `((id: string) => void)` | optional | Activation notification; app owns selection | | `loadingConfig` | `{ isLoading: boolean; label?: string; }` | optional | Announces loading and marks list busy | | `emptyContent` | `ReactNode` | optional | Defaults to `No items.` | | `paginationConfig` | `{ hasMore: boolean; isLoading: boolean; onLoadMore: () => void; }` | optional | App-owned paging with Load more | | `className` | `string` | optional | Root styling | ### loadingConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isLoading` | `boolean` | Yes | | | `label` | `string` | No | | ### paginationConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `hasMore` | `boolean` | Yes | | | `isLoading` | `boolean` | Yes | | | `onLoadMore` | `() => void` | Yes | | Items also accept `description`, `leadingIcon`, `entityConfig: { label, flag }`, `timestamp: { label, dateTime? }`, `unread`, `pinned`, and `menuConfig` using Dropdown's existing menu groups. `linkConfig` preserves native link behavior and supports an anchor-compatible `render` function for Next Link. More is a separate control; it never activates the row. Titles, entity names, and previews truncate with the full text retained in the DOM and native title. The selected row retains the same font weight. Time and More share reserved space, with menus available on focus and touch. ## Loading and empty states [#loading-and-empty-states] ```tsx import { List } from '@commenda/pathfinder/list'; export const ListStates = () => (
); ``` ## Paging [#paging] ```tsx import { useState } from 'react'; import { List } from '@commenda/pathfinder/list'; export const ListPagination = () => { const [count, setCount] = useState(3); return ( ({ id: String(index), title: `Filing ${String(index + 1)}`, linkConfig: { href: `#filing-${String(index + 1)}` }, }))} paginationConfig={{ hasMore: count < 9, isLoading: false, onLoadMore: () => { setCount(count + 3); }, }} /> ); }; ``` Use server paging for large datasets. List renders the provided page without virtualization; avoid loading an entire unbounded inbox. It does not fetch or automatically advance pages. Read the [record component rules](/docs/components/list#design-rules). ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Source rules [#source-rules] | Rule | Contract and provenance | | -------- | -------------------------------------------------------------------------------------------------------------------------------- | | LIST-001 | Figma 1101:3: 80px minimum inbox/mailroom rows, stable font weight; selected soft-pearl 300. | | LIST-002 | Native record link/action and a separate More button. No nested buttons or accidental record activation from its menu. | | LIST-003 | Application owns paging, selection, and remote state. Timestamp swaps with More without shifting content; touch exposes actions. | --- # Markdown Package: @commenda/pathfinder@0.1.0 Page: /docs/components/markdown Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=620-31413 ```tsx import { Markdown } from '@commenda/pathfinder/markdown'; ; ``` ## Usage [#usage] ```tsx import { Markdown } from '@commenda/pathfinder/markdown'; ; ``` Pass each text part without flattening tool calls, attachments, or other message parts into Markdown. The renderer needs no chat provider or AI SDK runtime. ## Streaming [#streaming] Incomplete fences and formatting remain readable while content arrives. Finished blocks use the same components and styles as static content. ```tsx ; ``` ## References [#references] Resolve known record URLs using application data. Pathfinder renders the reference and preview; the application supplies routing and data access. ```tsx href === '/entities/acme' ? { label, href, previewConfig: { title: 'Acme Pte Ltd', content: , }, } : undefined, }} /> ``` Use an icon in the resolved reference to add the correct Pathfinder flag to an entity in a table. The renderer does not infer countries from names. ## Tables [#tables] Tables use compact Pathfinder cell styles, a pinned first column, horizontal scrolling, copy as TSV, CSV/Markdown downloads, and an expanded dialog. Use Markdown alignment markers (`---:`) for numeric columns. Rich cell overrides can use existing Pathfinder cells; sorting, editing, and pagination belong in the standalone Table component. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `content` | `string` | conditional | Required; choose one. `children` preserves the existing MessageResponse calling pattern | | `children` | `string` | conditional | Required; choose one. `children` preserves the existing MessageResponse calling pattern | | `isStreaming` | `boolean` | optional | Enables incomplete Markdown handling; defaults to false | | `isAnimating` | `boolean` | optional | Compatibility alias; explicit `isStreaming` wins. Does not add word animation | | `components` | `MarkdownComponents` | optional | Typed overrides for prose, headings, links, lists, tables/cells, and images | | `referenceConfig` | `{ resolve: (href: string, label: string) => InlineReferenceProps \| undefined; }` | optional | | | `codeConfig` | `{ showLineNumbers?: boolean; }` | optional | | ### referenceConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `resolve` | `(href: string, label: string) => InlineReferenceProps` | Yes | | ### codeConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `showLineNumbers` | `boolean` | No | | Custom renderers are trusted application code and own their accessibility and URL handling. Raw HTML is not a supported extension mechanism. Math, diagrams, custom tool cards, and executable previews are outside Phase 1; their fences render as code. ## Design and accessibility rules [#design-and-accessibility-rules] - **MARKDOWN-01:** Body 13/20; h1/h2 use Heading/15, h3–h6 use Copy/13 Strong. Paragraph gaps 12px; lists 4px between items. No library-default typography. - **MARKDOWN-02:** Use this renderer for chat, briefing, and human threads. Keep the application's original message text. - **MARKDOWN-03:** Code is inert. Unsafe navigation schemes and executable HTML are not rendered. External links use native browser navigation; app-specific confirmation is an explicit link override. - **MARKDOWN-REF-01:** Record references have strong body text and a neutral underline. Preview data belongs to the application. - **MARKDOWN-TABLE-01:** Subtle tray, compact semantic table, pinned first column, copy/export/expand controls. - Streaming marks the content busy but does not announce every token. The conversation layer owns meaningful reply announcements. ## Migration notes [#migration-notes] ```tsx // Existing text and stream state can be reused without converting messages. {segment.part.text} ``` `components` remains available. Product-specific question fences, math/Mermaid plugins, link-confirmation providers, and word animations are not silently copied; document those separately before migrating a consumer. See the [AI content integration guide](/docs/guides/ai-content). --- # Message Package: @commenda/pathfinder@0.1.0 Page: /docs/components/message Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=543-2 ```tsx import { Message } from '@commenda/pathfinder/message'; part.id} renderPart={renderPart} actionConfig={{ copy: { text: plainText } }} />; ``` ## Usage [#usage] Pass your existing typed parts directly. The renderer receives the original part, including tool IDs, metadata and custom fields. ```tsx import { Message } from '@commenda/pathfinder/message'; import { Markdown } from '@commenda/pathfinder/markdown'; part.id ?? `${message.id}:${index}`} renderPart={(part) => { switch (part.type) { case 'text': return ; case 'request-recap': return ; default: return renderCustomPart(part); } }} actionConfig={{ copy: { text: plainText }, feedback: { value: feedback, onChange: saveFeedback }, retry: { isLatest: isLatestReply, onRetry: regenerate }, }} />; ``` Use a stable tool/part ID where available. An index fallback is safe only for append-only SDK parts; never reorder those parts. Keep Markdown a separate subpath import. Message does not import a parser or require an AI SDK provider. ## User and people threads [#user-and-people-threads] ```tsx import { Message } from '@commenda/pathfinder/message'; ; ``` ```tsx import { Message } from '@commenda/pathfinder/message'; ; ``` `from` is a presentation choice. A person in a shared thread does not introduce a new AI SDK role. User avatars come from `authorConfig`; set `showName` for shared conversations. People threads show the name automatically. Assistant replies never render the author avatar. ## System events [#system-events] ```tsx import { Message } from '@commenda/pathfinder/message'; ; ``` Keep system events short. They are visual timeline events, not model system instructions. ## Versions [#versions] ```tsx import { Message } from '@commenda/pathfinder/message'; ; ``` The host stores alternatives and swaps the selected message. Keep the active index within `[0, count - 1]`. Controls appear only with at least two alternatives; changing version does not call the model. ## Interleaved activity and content [#interleaved-activity-and-content] ```tsx import { Message } from '@commenda/pathfinder/message'; ; ``` Reasoning and tool calls remain ordered parts of their originating reply. Applications map their existing tool state into ToolCall without flattening the transcript. ## Streaming and stopped replies [#streaming-and-stopped-replies] ```tsx import { Message } from '@commenda/pathfinder/message'; ; ``` Hide actions while streaming. A stopped reply retains its parts and exposes its configured actions. Put activity components in the original parts array when activity is associated with that turn. ## Partial failure [#partial-failure] ```tsx import { Message } from '@commenda/pathfinder/message'; ; ``` Errors preserve streamed content and hide the regular action toolbar. Retry appears inline only for an eligible latest assistant reply. The host handles retry failures and supplies the next state; Message never starts network requests. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `from` | `"user" \| "assistant" \| "system" \| "person"` | required | Display kind, separate from the app's model role | | `parts` | `readonly Part[]` | required | Original ordered typed parts | | `getPartKey` | `((part: Part, index: number) => string)` | required | Stable unique key within the message | | `renderPart` | `((part: Part, index: number) => ReactNode)` | required | Typed content renderer; preserve unsupported parts intentionally | | `state` | `MessageState` | optional | Done by default | | `authorConfig` | `{ identity: PersonIdentity; showName?: boolean; }` | optional | User/person Avatar, no assistant avatar | | `timestamp` | `{ dateTime: string; label: string; }` | optional | Host-formatted accessible time | | `attachmentConfig` | `AttachmentGroupProps` | optional | Attachments above the bubble, aligned by author | | `actionConfig` | `MessageActionConfig` | optional | Copy, controlled feedback, latest-only retry, reply and controlled pin | | `branchConfig` | `MessageBranchConfig` | optional | Controlled zero-based alternative selection | | `label` | `string` | optional | Accessible article name and layout extension | | `className` | `string` | optional | Accessible article name and layout extension | ### state fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `"streaming" \| "done" \| "stopped"` | Yes | | #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `"error"` | Yes | | | `message` | `string` | Yes | | ### authorConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `identity` | `PersonIdentity` | Yes | | | `showName` | `boolean` | No | | ### authorConfig.identity fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `kind` | `"person"` | Yes | | | `userId` | `string` | No | | | `name` | `string` | No | | | `email` | `string` | No | | | `src` | `string` | No | | ### timestamp fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `dateTime` | `string` | Yes | | | `label` | `string` | Yes | | ### attachmentConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | `readonly AttachmentItem[]` | Yes | | | `align` | `"start" \| "end"` | No | | | `maxRows` | `1 \| 2 \| 3` | No | | | `getPreview` | `((item: AttachmentItem) => AttachmentProps["previewConfig"])` | No | | | `className` | `string` | No | | | `label` | `string` | No | | | `onPreview` | `((item: AttachmentItem) => void)` | No | | | `onRetry` | `((item: AttachmentItem) => void)` | No | | | `onRemove` | `((item: AttachmentItem) => void)` | No | | ### actionConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `copy` | `{ text: string; }` | No | | | `feedback` | `{ value: "good" \| "bad" \| null; onChange: (value: "good" \| "bad" \| null) => void; }` | No | | | `retry` | `{ isLatest: boolean; onRetry: () => void; disabled?: boolean; }` | No | | | `onReply` | `(() => void)` | No | | | `pin` | `{ isPinned: boolean; onChange: (value: boolean) => void; }` | No | | ### actionConfig.copy fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `text` | `string` | Yes | | ### actionConfig.feedback fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `value` | `"good" \| "bad" \| null` | Yes | | | `onChange` | `(value: "good" \| "bad" \| null) => void` | Yes | | ### actionConfig.retry fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isLatest` | `boolean` | Yes | | | `onRetry` | `() => void` | Yes | | | `disabled` | `boolean` | No | | ### actionConfig.pin fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isPinned` | `boolean` | Yes | | | `onChange` | `(value: boolean) => void` | Yes | | ### branchConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `index` | `number` | Yes | Zero-based active alternative; keep the message/part keys stable within each alternative. | | `count` | `number` | Yes | | | `onChange` | `(index: number) => void` | Yes | | `actionConfig.copy` accepts `{ text }`. Feedback accepts `{ value: 'good' | 'bad' | null, onChange }`; clicking a chosen thumb clears it. Retry accepts `{ isLatest, onRetry, disabled? }`. People-thread actions use `onReply` and `pin: { isPinned, onChange }`. Callbacks own persistence and error reporting; only copy writes to the clipboard locally. ## Design rules and accessibility [#design-rules-and-accessibility] - **MESSAGE-01:** User bubble pearl 300, person bubble pearl 200, radius 12, padding 12 vertically/16 horizontally, content at most 520px. Rendered frames use Copy/15, superseding the stale Copy/13 description. - **MESSAGE-02:** Assistant parts stack 16px apart without an avatar. Generated files, requests and recaps stay in their originating reply. - **MESSAGE-03:** Actions sit 8px below per the component description. No actions while streaming; branches require alternatives; retry requires latest-reply eligibility. Copy changes to a check for two seconds, selected feedback changes fill rather than weight. - **MESSAGE-04:** Keep partial output and show the error inline. Do not announce each token or replace the reply with a toast. Long messages wrap; turn long pasted documents into attachments at the application boundary. Screen readers navigate named articles and labeled action buttons. The Storybook worst-case switch retains Unicode, unbroken text and long author fixtures. --- # Multi select Package: @commenda/pathfinder@0.1.0 Page: /docs/components/multi-select Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=865-3077 ```tsx import { DEFlag, GBFlag, INFlag, SGFlag } from '@commenda/pathfinder/flags'; import { MultiSelect } from '@commenda/pathfinder/multi-select'; const entityOptions = [ { value: 'gb', label: 'Meridian UK Ltd', leadingIcon: }, { value: 'de', label: 'Meridian GmbH', leadingIcon: }, { value: 'in', label: 'Meridian India', leadingIcon: }, { value: 'sg', label: 'Meridian Singapore', leadingIcon: , }, ]; export const MultiSelectDefault = () => ( ); ``` ## Installation [#installation] Import the component in your screen: ```tsx import { MultiSelect } from '@commenda/pathfinder/multi-select'; ``` Load the stylesheet once in your app root, after host styles: ```tsx import '@commenda/pathfinder/styles.css'; ``` Pathfinder ships precompiled CSS; no Tailwind setup is required. In Next.js App Router, put interactive state and callbacks in a client component. See [installation](/docs/getting-started/installation) for the workspace setup. ## Empty [#empty] ```tsx import { DEFlag, GBFlag, INFlag, SGFlag } from '@commenda/pathfinder/flags'; import { MultiSelect } from '@commenda/pathfinder/multi-select'; const entityOptions = [ { value: 'gb', label: 'Meridian UK Ltd', leadingIcon: }, { value: 'de', label: 'Meridian GmbH', leadingIcon: }, { value: 'in', label: 'Meridian India', leadingIcon: }, { value: 'sg', label: 'Meridian Singapore', leadingIcon: , }, ]; export const MultiSelectEmpty = () => ( ); ``` ## Clearable [#clearable] ```tsx import { DEFlag, GBFlag, INFlag, SGFlag } from '@commenda/pathfinder/flags'; import { MultiSelect } from '@commenda/pathfinder/multi-select'; const entityOptions = [ { value: 'gb', label: 'Meridian UK Ltd', leadingIcon: }, { value: 'de', label: 'Meridian GmbH', leadingIcon: }, { value: 'in', label: 'Meridian India', leadingIcon: }, { value: 'sg', label: 'Meridian Singapore', leadingIcon: , }, ]; export const MultiSelectClearable = () => ( ); ``` ## States [#states] ```tsx import { DEFlag, GBFlag, INFlag, SGFlag } from '@commenda/pathfinder/flags'; import { MultiSelect } from '@commenda/pathfinder/multi-select'; const entityOptions = [ { value: 'gb', label: 'Meridian UK Ltd', leadingIcon: }, { value: 'de', label: 'Meridian GmbH', leadingIcon: }, { value: 'in', label: 'Meridian India', leadingIcon: }, { value: 'sg', label: 'Meridian Singapore', leadingIcon: , }, ]; export const MultiSelectStates = () => (
); ``` ## Search [#search] ```tsx import { DEFlag, GBFlag, INFlag, SGFlag } from '@commenda/pathfinder/flags'; import { MultiSelect } from '@commenda/pathfinder/multi-select'; const entityOptions = [ { value: 'gb', label: 'Meridian UK Ltd', leadingIcon: }, { value: 'de', label: 'Meridian GmbH', leadingIcon: }, { value: 'in', label: 'Meridian India', leadingIcon: }, { value: 'sg', label: 'Meridian Singapore', leadingIcon: , }, ]; export const MultiSelectSearch = () => ( ); ``` ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `options` | `readonly ChoiceOption[]` | conditional | Same option contract as Select | | `groups` | `readonly ChoiceGroup[]` | conditional | Same option contract as Select | | `value` | `readonly string[]` | conditional | Controlled callback required; empty array clears | | `defaultValue` | `readonly string[]` | optional | Controlled callback required; empty array clears | | `searchConfig` | `{ mode: "local"; enabled?: boolean; placeholder?: string; } \| { mode: "remote"; query: string; onQueryChange: (query: string) => void; resultsQuery: string; isLoading: boolean; error?: string; onRetry?: () => void; placeholder?: string; }` | conditional | Same contract as Select | | `selectedOptions` | `readonly ChoiceOption[]` | optional | Remote selected metadata independent from current results | | `paginationConfig` | `ChoicePaginationConfig` | optional | Remote-only pagination | | `selectionDisplayConfig` | `{ mode: "tags"; } \| { mode: "summary"; format: (count: number) => string; }` | optional | tags; measured +N overflow | | `selectionLimitConfig` | `{ max: number; message?: string; }` | optional | Blocks additional selection, permits deselection | | `selectAllConfig` | `{ label?: string; }` | optional | Only listed eligible options; respects limits | | `clearConfig` | `{ label?: string; }` | optional | Clears unlocked values | | `renderConfig` | `{ optionContent?: (option: ChoiceOption, state: { isSelected: boolean; }) => ReactNode; selectedContent?: (option: ChoiceOption) => ReactNode; }` | optional | Shared customization, virtualization, creation action | | `listConfig` | `{ virtualized?: boolean; maxHeight?: number; }` | optional | Shared customization, virtualization, creation action | | `addItemConfig` | `{ label: string; onAddItem: (query: string) => void; isDisabled?: boolean; }` | optional | Shared customization, virtualization, creation action | | `label` | `string` | conditional | Visible field label. Supply an accessible name when it is omitted. | | `hideLabel` | `boolean` | optional | Visually hide the label while preserving its accessible name. | | `aria-label` | `string` | conditional | Defines a string value that labels the current element. | | `required` | `boolean` | conditional | Mark the field as required. See validation guidance for the component. | | `labelConfig` | `{ optionalLabel: string; help?: never; } \| { help: string; optionalLabel?: never; }` | optional | Choose either `optionalLabel` or `help`, never both. `never` excludes the other field. | | `description` | `string` | optional | Supporting guidance associated with the field. | | `errorConfig` | `{ message: string; reveal?: "after-blur" \| "immediate"; }` | optional | Validation message and when it becomes visible. | | `disableConfig` | `DisableConfig` | optional | Disabled state with an optional explanation. | | `id` | `string` | optional | Stable HTML identifier for the component. | | `name` | `string` | optional | | | `form` | `string` | optional | | | `autoComplete` | `string` | optional | | | `className` | `string` | optional | Additional classes for the component root. | | `placeholder` | `string` | optional | | | `onBlur` | `FocusEventHandler` | optional | Called when focus leaves the component. | | `onFocus` | `FocusEventHandler` | optional | Called when focus enters the component. | | `emptyLabel` | `string` | optional | | | `onValueChange` | `((value: readonly string[]) => void)` | conditional | | ### searchConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `mode` | `"local"` | Yes | | | `enabled` | `boolean` | No | | | `placeholder` | `string` | No | | #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `mode` | `"remote"` | Yes | | | `query` | `string` | Yes | | | `onQueryChange` | `(query: string) => void` | Yes | | | `resultsQuery` | `string` | Yes | | | `isLoading` | `boolean` | Yes | | | `error` | `string` | No | | | `onRetry` | `(() => void)` | No | | | `placeholder` | `string` | No | | ### paginationConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `hasMore` | `boolean` | Yes | | | `isLoadingMore` | `boolean` | Yes | | | `onLoadMore` | `() => void` | Yes | | | `error` | `string` | No | | | `onRetry` | `(() => void)` | No | | ### selectionDisplayConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `mode` | `"tags"` | Yes | | #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `mode` | `"summary"` | Yes | | | `format` | `(count: number) => string` | Yes | | ### selectionLimitConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `max` | `number` | Yes | | | `message` | `string` | No | | ### selectAllConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | No | | ### clearConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | No | | ### renderConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `optionContent` | `((option: ChoiceOption, state: { isSelected: boolean; }) => ReactNode)` | No | | | `selectedContent` | `((option: ChoiceOption) => ReactNode)` | No | | ### listConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `virtualized` | `boolean` | No | | | `maxHeight` | `number` | No | | ### addItemConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | Yes | | | `onAddItem` | `(query: string) => void` | Yes | | | `isDisabled` | `boolean` | No | | ### labelConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `optionalLabel` | `string` | Yes | | Do not supply: `help`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `help` | `string` | Yes | | Do not supply: `optionalLabel`. ### errorConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `message` | `string` | Yes | | | `reveal` | `"after-blur" \| "immediate"` | No | | ### disableConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isDisabled` | `boolean` | Yes | | | `reason` | `string` | No | | Shared labels, descriptions, required annotations, errorConfig, disableConfig, IDs, form names, refs, and focus/blur callbacks follow [Input](/docs/components/input). Selection controls do not expose readOnly. ## Local and remote search [#local-and-remote-search] A ChoiceOption has `value`, `label`, optional `description`, `keywords`, `leadingIcon`, and `disableConfig`. Groups contain stable `id`, `label`, and `options`. Keywords participate only in local filtering. Rich rendering preserves plain-text labels for accessibility and search. Local config: `{ mode: 'local', enabled?, placeholder? }`. Small local lists use Base UI Select; searchable lists use Base UI Combobox internally. There are exactly two public selection components: Select and MultiSelect. Remote config: `{ mode: 'remote', query, onQueryChange, resultsQuery, isLoading, error?, onRetry?, placeholder? }`. Query updates immediately; the application owns debouncing, requests, cache, and cancellation. Remote results bypass client filtering and retain server order. While query differs from resultsQuery, stale options are unavailable for selection. Associate resultsQuery with the response that actually produced the options. Supply selectedOptions for edit-form selections absent from current results. Selection never disappears merely because a page changes. A selected-only cache bridges user selection updates; it is not an unbounded history of every result. Missing metadata renders an explicit unavailable-value label until supplied. Pagination receives accumulated, deduplicated options. New pages preserve scroll; new queries reset it. The UI gates duplicate load-more requests and provides a keyboard-accessible Load more button. Page errors preserve loaded results and offer retry. Virtualization limits mounted rows; remote pagination limits fetched data. Use both where appropriate. Locked selected options survive tag removal, clear-all, Backspace, and bulk operations. MultiSelect select-all affects listed enabled options only and cannot imply selecting every record on a server. Bulk selection that would exceed a limit is disabled rather than partially applied. ### Limit [#limit] ```tsx import { DEFlag, GBFlag, INFlag } from '@commenda/pathfinder/flags'; import { MultiSelect } from '@commenda/pathfinder/multi-select'; export const MultiSelectLimit = () => ( , value: 'gb', label: 'United Kingdom', }, { leadingIcon: , value: 'de', label: 'Germany' }, { leadingIcon: , value: 'in', label: 'India' }, ]} /> ); ``` ### Summary [#summary] ```tsx import { DEFlag, GBFlag } from '@commenda/pathfinder/flags'; import { MultiSelect } from '@commenda/pathfinder/multi-select'; export const MultiSelectSummary = () => ( `${String(count)} entities selected`, }} defaultValue={['gb', 'de']} options={[ { leadingIcon: , value: 'gb', label: 'Meridian UK Ltd', }, { leadingIcon: , value: 'de', label: 'Meridian GmbH', }, ]} /> ); ``` ### Remote [#remote] ```tsx import { useEffect, useState } from 'react'; import { type ChoiceOption, MultiSelect, } from '@commenda/pathfinder/multi-select'; export const MultiSelectRemote = () => { const [query, setQuery] = useState(''); const [values, setValues] = useState(['saved']); const [selected, setSelected] = useState([ { value: 'saved', label: 'Previously saved entity' }, ]); const [response, setResponse] = useState({ query: '', options: [] as ChoiceOption[], count: 10, }); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); useEffect(() => { const abort = new AbortController(); // Demo transport: replace this timer with your query library or fetch using abort.signal. // Query updates immediately; the 250ms delay represents request debouncing. const timer = setTimeout(() => { if (abort.signal.aborted) return; const options = Array.from({ length: 30 }, (_, index) => ({ value: String(index), label: `Meridian entity ${String(index + 1)}`, })).filter((option) => option.label.toLowerCase().includes(query.toLowerCase()) ); setResponse({ query, options, count: 10 }); setLoading(false); }, 250); return () => { abort.abort(); clearTimeout(timer); }; }, [query]); const listed = response.options.slice(0, response.count); return ( { setValues(next); const lookup = new Map( [...selected, ...listed].map((option) => [option.value, option]) ); setSelected( next.flatMap((value) => { const option = lookup.get(value); return option ? [option] : []; }) ); }} selectedOptions={selected} options={listed} searchConfig={{ mode: 'remote', query, onQueryChange: (next) => { setQuery(next); setLoading(true); }, resultsQuery: response.query, isLoading: loading, }} paginationConfig={{ hasMore: response.count < response.options.length, isLoadingMore: loadingMore, onLoadMore: () => { setLoadingMore(true); setTimeout(() => { setResponse((current) => current.query === query ? { ...current, count: current.count + 10 } : current ); setLoadingMore(false); }, 250); }, }} clearConfig={{}} /> ); }; ``` ### Large [#large] ```tsx import { MultiSelect } from '@commenda/pathfinder/multi-select'; export const MultiSelectLarge = () => ( ({ value: String(index), label: `Entity ${String(index + 1)}`, }))} listConfig={{ virtualized: true, maxHeight: 280 }} /> ); ``` Read [selection and search rules](/docs/components/multi-select#design-rules) for source provenance, API ownership, and constraints. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Source rules [#source-rules] | Rule | Figma source | Contract | | --------------- | ------------ | --------------------------------------------------------------- | | MULTISELECT-001 | 865:59967 | Single-line tags with measured +N; removal does not open popup. | | MULTISELECT-002 | 865:59988 | Keep popup open during selection; preserve option order. | Sources: [Search](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=635-13518), [Select](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=179-2), [MultiSelect](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=865-3077). The supplied Select node 182:1182 is its dimensions frame; 180:1177 is the actual component set. ### Field geometry and states [#field-geometry-and-states] The component sheets are [Select 182:1216](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=182-1216) and [MultiSelect 865:3078](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=865-3078). - **SELECT-VISUAL-001:** 28px field, 6px radius, 8px inner padding, 4px content gaps, 16px flag and chevron. Clear precedes the chevron (180:593). Empty fields have no leading flag. - **MULTISELECT-VISUAL-001:** 4px starting inset; selected chips and +N are 20px high with radius 4 and a 0.5px pearl 300 border. Chip icons/removal are 12px; the final chevron is 16px (180:620). - **SELECTION-STATE-001:** Pearl 400 default border; pearl 500 hover. Open fields retain blue 700 border and a 2px blue 200 ring while focus is in the popup (180:152, 180:766). Invalid uses red 600 at rest and red 700/red 200 when open. Error text is red 900 with a 12px alert icon (180:1031). - **SELECTION-STATE-002:** Disabled fields use pearl 100 background, pearl 300 border, pearl 600 values and pearl 800 labels. Disabled MultiSelect chips omit removal buttons (180:839). The Figma 320px MultiSelect frame clips part of the second chip inside a 256px strip. The implementation deliberately truncates the final visible chip's label instead, preserving its removal button and full-label tooltip; a 96px minimum keeps partial chips readable before using +N. Placeholder text uses pearl 800 rather than source pearl 700 to meet text contrast on white. These are documented accessibility adjustments, not new design variants. The entire field opens the list; removal and clear remain independent buttons. Simple and searchable paths share the same field rendering. Popups follow the embedded Dropdown instances 182:606 and 865:59996 (10px radius, 6px list inset, 2px row gap, 32px minimum rows, 36px flush search row with 12px horizontal inset); option roles remain Select/Combobox semantics. Multi-selection uses Pathfinder Checkbox in read-only presentation mode inside each option; the option owns selection and keyboard focus. Select-all is a functional Checkbox with mixed state. Single selection shows a trailing check. Search, clear and removal icons sit in explicit flex boxes so their centers align with the input and chip rows. ### API ownership [#api-ownership] Exactly Select and MultiSelect are public. Both support local search, server search, groups, rich option content, pagination, and virtualization. Dropdown remains an action menu. Free-text query entry belongs to Search; typed text never silently becomes a selected option ID. The application owns query requests, debounce, cancellation, cursors, deduplicated page merging, and cache invalidation. Pathfinder blocks stale-query selection using query/resultsQuery, retains server ranking, and displays initial loading, empty, error, load-more, and retry states. Do not pass yesterday's query as resultsQuery for today's response. Use a query-keyed data hook or AbortController plus request identity protection. A late response for an earlier query must not overwrite the active query's results. Reset cursors when the query changes. The interactive remote example uses a cancellable simulated transport so it works without an application endpoint; replace it with your authenticated API adapter. Selected IDs and selectedOptions are separate from fetched pages. selectedOptions resolves labels/icons for saved values missing from the current result set. It does not pin unmatched rows to the results. The component caches only currently selected metadata as a bridge; applications remain responsible for authoritative record updates and deleted-record decisions. ### Large lists and bulk actions [#large-lists-and-bulk-actions] Enable listConfig.virtualized for large lists. This enables searchable interaction and measured-height rows; keyboard-highlighted options stay mounted. API pagination and virtualization solve different costs and can be combined. Option values must be globally unique and stable across groups and pages. Select-all means currently listed, filtered, enabled results. Locked values survive all UI removal paths. A bulk selection that exceeds selectionLimitConfig.max is disabled; it never chooses an arbitrary subset. Selecting all matches across an entire backend requires a separate server-side selection model. Custom renderers supply noninteractive content. Pathfinder owns trigger and option roles, events, refs, selection indicators, and focus. Add-item is a separate explicit action; application creation returns a real record and updates options/value. ### Accessibility and forms [#accessibility-and-forms] Labels, descriptions, error reveal, and disable reasons follow the shared field contract. Single selection serializes one ID; multiple selection serializes repeated names. Trigger refs and form reset work for both primitive paths. Searchable popups provide an explicitly named query input. Load more and retry are keyboard-accessible buttons outside the option list. Tag remove buttons are siblings of the trigger, never nested inside it. Long selected labels remain available through tooltips. Search result compositions must handle overlay Escape before field clear/collapse through preventDefault. Flags, chevrons, and clear/remove icons sit in explicit square flex containers with both axes centered. Do not rely on inline SVG baseline alignment or compensate with pixel offsets. The selected value, tags, and trailing controls share the field’s vertical center. Row Checkbox instances are inert visual indicators; the option owns selection and accessibility semantics. ### Selection guidance [#selection-guidance] - `DROPDOWN-FUTURE-01` — Figma selection guidance (the historical rule ID is retained): search beyond about eight choices; empty state names the query; footer has one link action outside options. Multiple selection keeps the popup open and preserves row order. Selected rows have checks; highlight remains independent. These selection behaviors belong to Select and MultiSelect, not the action-oriented Dropdown. --- # OTP Package: @commenda/pathfinder@0.1.0 Page: /docs/components/otp Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=85-2 ```tsx import { OTP } from '@commenda/pathfinder/otp'; export const OTPDefault = () => ( ); ``` ## Installation [#installation] Import the component in your screen: ```tsx import { OTP } from '@commenda/pathfinder/otp'; ``` Load the stylesheet once in your app root, after host styles: ```tsx import '@commenda/pathfinder/styles.css'; ``` Pathfinder ships precompiled CSS; no Tailwind setup is required. In Next.js App Router, put interactive state and callbacks in a client component. See [installation](/docs/getting-started/installation) for the workspace setup. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `value` | `string` | conditional | Keep leading zeros; controlled mode requires `onValueChange` | | `defaultValue` | `string` | optional | Keep leading zeros; controlled mode requires `onValueChange` | | `length` | `4 \| 5 \| 6 \| 7 \| 8` | optional | `6` numeric characters | | `groupConfig` | `{ sizes: readonly number[]; }` | optional | Six digits use `[3, 3]`; other lengths use one group. Sizes must sum to length | | `onComplete` | `((code: string) => void)` | optional | Fires after an edit completes the code; never on mount or blur | | `loadingConfig` | `{ isLoading: boolean; label?: string; }` | optional | Locks edits, retains form value, announces verification | | `label` | `string` | conditional | Visible field label. Supply an accessible name when it is omitted. | | `hideLabel` | `boolean` | optional | Visually hide the label while preserving its accessible name. | | `aria-label` | `string` | conditional | Accessible name when the field has no visible label. | | `required` | `boolean` | conditional | Mark the field as required. See validation guidance for the component. | | `labelConfig` | `{ optionalLabel: string; help?: never; } \| { help: string; optionalLabel?: never; }` | optional | Choose either `optionalLabel` or `help`, never both. `never` excludes the other field. | | `description` | `string` | optional | Supporting guidance associated with the field. | | `errorConfig` | `{ message: string; reveal?: "after-blur" \| "immediate"; }` | optional | Validation message and when it becomes visible. | | `disableConfig` | `DisableConfig` | optional | Disabled state with an optional explanation. | | `onValueChange` | `((value: string) => void)` | conditional | | ### groupConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `sizes` | `readonly number[]` | Yes | | ### loadingConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isLoading` | `boolean` | Yes | | | `label` | `string` | No | | ### labelConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `optionalLabel` | `string` | Yes | | Do not supply: `help`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `help` | `string` | Yes | | Do not supply: `optionalLabel`. ### errorConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `message` | `string` | Yes | | | `reveal` | `"after-blur" \| "immediate"` | No | | ### disableConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isDisabled` | `boolean` | Yes | | | `reason` | `string` | No | | Shared `label`, `hideLabel`, `description`, `required`, `errorConfig`, `disableConfig`, and `readOnly` follow [Input](/docs/components/input). The underlying `input-otp` primitive is private. ## Grouping [#grouping] ```tsx import { OTP } from '@commenda/pathfinder/otp'; export const OTPGrouping = () => ( ); ``` ## Verification [#verification] The application starts verification from `onComplete`, owns pending/error state, and clears its previous server error in `onValueChange`. Do not submit a form automatically from both `onComplete` and a separate input handler. Guard against a previous request completing after the user has edited the code; associate the response with the submitted code or request ID. ```tsx import { useState } from 'react'; import { OTP } from '@commenda/pathfinder/otp'; export const OTPVerification = () => { const [code, setCode] = useState(''); const [error, setError] = useState(); const [complete, setComplete] = useState(false); return (
{ setCode(next); setError(undefined); setComplete(false); }} onComplete={(next) => { if (next === '012345') setComplete(true); else setError('That code is incorrect. Try again.'); }} errorConfig={ error ? { message: error, reveal: 'immediate' } : undefined } description="Demo verification: enter 012345. Your application owns the real request." /> {complete &&

Code accepted.

}
); }; ``` ## States [#states] ```tsx import { OTP } from '@commenda/pathfinder/otp'; export const OTPStates = () => (
); ``` Paste fills the code, Backspace edits the previous slot, and leading zeros remain strings. Read [composite field rules](/docs/components/otp#design-rules) for Figma provenance and accessibility rules. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### OTP [#otp] - **OTP-001:** Default six 36px slots in two groups of three; radius 6, 15px medium digits. Other supported lengths are 4–8. - **OTP-002:** One accessible native input; numeric string values preserve leading zeros, paste, and one-time-code autocomplete. Slots are presentational. - **OTP-003:** Completion emits an event. The application verifies the code, handles stale responses, and clears server errors on edit. Figma's auto-submit wording is implemented as `onComplete`, with no implicit form submission. - **OTP-004:** Loading makes the input read-only and announces verification without dropping its native form value. Disabled fields do not submit. ### Shared rules [#shared-rules] Labels activate the first input. Descriptions, disabled reasons, and validation messages stay associated with the field. Error icons center on the first 18px text line, including wrapped messages. Controlled values require callbacks; uncontrolled form resets restore defaults. Public props expose application values, never private primitive or calendar objects. Styles ship precompiled for React 18/19 and Next.js consumers. --- # Output List Package: @commenda/pathfinder@0.1.0 Page: /docs/components/output-list Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=670-38136 ```tsx import { OutputList } from '@commenda/pathfinder/output-list'; ; ``` ## Usage [#usage] ```tsx import { OutputList } from '@commenda/pathfinder/output-list'; ; ``` Pass stable IDs and newest-first items. Opening, downloading, saving, permissions, and persistence belong to the application. The menu offers download/save only when callbacks are supplied; set an item's `actions.download` or `actions.saveToDocuments` to `false` to omit that action for a particular artifact. Menu actions never activate the row. ## Empty, loading, and collapsed [#empty-loading-and-collapsed] ```tsx import { OutputList } from '@commenda/pathfinder/output-list'; ; ``` ```tsx import { OutputList } from '@commenda/pathfinder/output-list'; ; ``` ```tsx import { OutputList } from '@commenda/pathfinder/output-list'; ; ``` Initial loading shows four skeleton lines. Refreshing a populated list keeps its rows visible. A collapsed section shows the loaded artifact count. Use `disclosureConfig.defaultOpen` for local state, or `open` with `onOpenChange` to persist the preference in your app. ## Responsive output panel [#responsive-output-panel] ```tsx import { Button } from '@commenda/pathfinder/button'; import { OutputList } from '@commenda/pathfinder/output-list'; import { RunList } from '@commenda/pathfinder/run-list'; import { Sheet } from '@commenda/pathfinder/sheet'; Show outputs} > ; ``` The example composes Output List and Run List in a 292px column. At viewport widths of 1280px and above it starts visible; from 1024px to 1279px it starts hidden with a header toggle. Below 1024px, the toggle opens a modal bottom Sheet. These layout and preference choices remain in the host, so a compact card is also usable outside chat. ## Complete workflow [#complete-workflow] ```tsx import { Conversation } from '@commenda/pathfinder/conversation'; import { Message } from '@commenda/pathfinder/message'; import { Composer } from '@commenda/pathfinder/composer'; message.id} renderMessage={(message) => ( part.id} renderPart={renderPart} /> )} footer={} />; ``` Choose a suggested prompt, add the simulated ledger attachment, send, answer the entity question, and open the generated workpaper. This example composes Composer, Attachment, Conversation, Message, Reasoning, Tool Call, Question Card, Response Recap, and the output panel. Timers simulate upload and processing; there is no backend, real file upload/download, artifact viewer, or editor. Stop cancels the pending simulated response; a temporarily dismissed question can be reopened. ## Overflow and interaction [#overflow-and-interaction] Titles and kind labels truncate without moving the icon or menu. The full title remains in the accessible name and native title tooltip. Touch devices keep the menu button visible; hover devices reveal it on hover or keyboard focus. Removing a focused row moves focus to the next available row or section trigger. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `items` | `readonly OutputItem[]` | required | Stable IDs, newest-first order | | `onOpen` | `((id: string) => void)` | required | Open the app's artifact preview | | `selectedId` | `string \| null` | optional | Controlled active artifact | | `onSelectionChange` | `((id: string) => void)` | optional | Selection request; does not persist internally | | `actionConfig` | `{ onDownload?: (id: string) => void; onSaveToDocuments?: (id: string) => void; }` | optional | Callbacks receive the artifact ID | | `disclosureConfig` | `OutputDisclosureConfig` | optional | Controlled or local disclosure | | `loadingConfig` | `{ isLoading: boolean; label?: string; }` | optional | Initial skeletons / busy state | | `title` | `string` | optional | Defaults to Outputs | | `emptyLabel` | `string` | optional | Empty-state explanation | | `className` | `string` | optional | Host width and placement | ### actionConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `onDownload` | `((id: string) => void)` | No | | | `onSaveToDocuments` | `((id: string) => void)` | No | | ### disclosureConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `open` | `boolean` | Yes | | | `onOpenChange` | `(open: boolean) => void` | Yes | | Do not supply: `defaultOpen`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `defaultOpen` | `boolean` | No | | | `onOpenChange` | `((open: boolean) => void)` | No | | Do not supply: `open`. ### loadingConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isLoading` | `boolean` | Yes | | | `label` | `string` | No | | `OutputItem` includes `id`, `title`, optional `kindLabel`, `disabled`, and `actions`. `kind` is `document`, `plan`, `workpaper`, `pdf`, `csv`, `file`, or `record`. A file can specify `fileType`; a record requires an `icon` such as its country flag. Other kinds can override their icon too. Prefer user-facing artifact names and localized kind labels. ## Design rules [#design-rules] - **OUTPUT-01:** Use 32px rows, a 40px header, 12px card radius, 6px body padding, and 2px row gaps. Do not add a shadow or bold the selected title. - **OUTPUT-02:** Show artifact titles and kinds, never internal tool names. The host supplies newest-first order without date headers. - **OUTPUT-03:** Keep row opening and menu actions independent. Do not place buttons inside another button. The app owns storage, routing, permissions, and disclosure persistence. - **OUTPUT-04:** New rows fade in and displaced rows slide for the base duration; keyboard-focused lists and reduced-motion users get immediate updates. Retain stable keys and restore focus after removal. Metadata uses Soft Pearl 800 for readable contrast at 12px, a deliberate contrast adjustment from Figma's lighter 700. The header disclosure uses the existing Base UI measured-height pattern; only infrequent disclosure changes animate height, while insertion animates opacity and transforms. --- # Phone input Package: @commenda/pathfinder@0.1.0 Page: /docs/components/phone-input Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=284-2 ```tsx import { PhoneInput } from '@commenda/pathfinder/phone-input'; export const PhoneInputDefault = () => ( ); ``` ## Installation [#installation] Import the component in your screen: ```tsx import { PhoneInput } from '@commenda/pathfinder/phone-input'; ``` Load the stylesheet once in your app root, after host styles: ```tsx import '@commenda/pathfinder/styles.css'; ``` Pathfinder ships precompiled CSS; no Tailwind setup is required. In Next.js App Router, put interactive state and callbacks in a client component. See [installation](/docs/getting-started/installation) for the workspace setup. Country artwork loads through an isolated lazy module; other field entry points do not import phone metadata or flags. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `value` | `PhoneValue` | conditional | One is required; the app supplies its initial jurisdiction | | `defaultValue` | `PhoneValue` | conditional | One is required; the app supplies its initial jurisdiction | | `onValueChange` | `((value: PhoneValue) => void)` | conditional | Required in controlled mode; receives the editable text | | `countryConfig` | `{ allowedCountries?: readonly CountryCode[]; }` | optional | All metadata-supported countries unless constrained | | `onValidationChange` | `((details: PhoneValueDetails) => void)` | optional | Normalized details when validation changes | | `label` | `string` | conditional | Visible field label. Supply an accessible name when it is omitted. | | `hideLabel` | `boolean` | optional | Visually hide the label while preserving its accessible name. | | `aria-label` | `string` | conditional | Accessible name when the field has no visible label. | | `required` | `boolean` | conditional | Mark the field as required. See validation guidance for the component. | | `labelConfig` | `{ optionalLabel: string; help?: never; } \| { help: string; optionalLabel?: never; }` | optional | Choose either `optionalLabel` or `help`, never both. `never` excludes the other field. | | `description` | `string` | optional | Supporting guidance associated with the field. | | `errorConfig` | `{ message: string; reveal?: "after-blur" \| "immediate"; }` | optional | Validation message and when it becomes visible. | | `disableConfig` | `DisableConfig` | optional | Disabled state with an optional explanation. | ### value fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `country` | `CountryCode` | Yes | | | `number` | `string` | Yes | | ### defaultValue fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `country` | `CountryCode` | Yes | | | `number` | `string` | Yes | | ### countryConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `allowedCountries` | `readonly CountryCode[]` | No | | ### labelConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `optionalLabel` | `string` | Yes | | Do not supply: `help`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `help` | `string` | Yes | | Do not supply: `optionalLabel`. ### errorConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `message` | `string` | Yes | | | `reveal` | `"after-blur" \| "immediate"` | No | | ### disableConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isDisabled` | `boolean` | Yes | | | `reason` | `string` | No | | Shared presentation, native input attributes, and `readOnly` follow [Input](/docs/components/input). Disabled fields do not submit. Read-only values remain copyable and valid values submit. `CountryCode` is a phone metadata type, with no application enum dependency. ## Countries [#countries] ```tsx import { PhoneInput } from '@commenda/pathfinder/phone-input'; export const PhoneInputCountries = () => ( ); ``` Search by country name or calling code. Switching country preserves entered text for review. International input updates country only when metadata resolves it unambiguously; a partial shared dial code such as `+1` does not guess US or Canada. ## Validation and submission [#validation-and-submission] ```tsx import { useState } from 'react'; import { PhoneInput, type PhoneValueDetails, } from '@commenda/pathfinder/phone-input'; export const PhoneInputValidation = () => { const [details, setDetails] = useState({ isValid: false, e164: null, }); return (
{details.e164 ?? 'Enter a valid number to see its E.164 value.'}
); }; ``` Typing preserves spaces, brackets, dashes, and incomplete text. Valid numbers format nationally on blur. `onValidationChange` supplies E.164 separately; store the draft and canonical value according to application needs. E.164 excludes extensions; if your workflow uses extensions, retain the draft or collect an extension separately. Phone metadata validity is not proof that a number exists or belongs to a person; ownership verification is application-owned. ## States [#states] ```tsx import { PhoneInput } from '@commenda/pathfinder/phone-input'; export const PhoneInputStates = () => (
); ``` Read [composite field rules](/docs/components/phone-input#design-rules). ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Phones [#phones] - **PHONE-001:** One 28px border/focus ring, 16px country flag, 13px dial code, 12px chevron, and searchable country selector. Center artwork in flex containers, never through inline SVG baselines. - **PHONE-002:** Preserve editable text; format valid numbers nationally on blur. Validate with libphonenumber metadata and expose E.164 separately. The app owns verification and storage. - **PHONE-003:** The application must supply its jurisdiction. Resolve international input only when unambiguous; do not infer a country from shared partial dial codes or browser locale. - **PHONE-004:** Country switches preserve typed text for review. Allowed-country policy comes from the application, and restricted countries cannot yield a valid canonical form value. ### Shared rules [#shared-rules] Labels activate the first input. Descriptions, disabled reasons, and validation messages stay associated with the field. Error icons center on the first 18px text line, including wrapped messages. Controlled values require callbacks; uncontrolled form resets restore defaults. Public props expose application values, never private primitive or calendar objects. Styles ship precompiled for React 18/19 and Next.js consumers. --- # Plan review card Package: @commenda/pathfinder@0.1.0 Page: /docs/components/plan-review-card Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=607-4610 ```tsx import { PlanReviewCard } from '@commenda/pathfinder/plan-review-card'; ; ``` ## Usage [#usage] ```tsx import { PlanReviewCard } from '@commenda/pathfinder/plan-review-card'; ; ``` ## Inline plan review [#inline-plan-review] ```tsx import { PlanReviewCard } from '@commenda/pathfinder/plan-review-card'; ; ``` ## Failure and retry [#failure-and-retry] ```tsx // Return { status: 'failed', message: 'Please try again.' } from the handler to retain this card. import { PlanReviewCard } from '@commenda/pathfinder/plan-review-card'; ; ``` Reset restores the example. Simulate failure retains the card for retry; no backend request is made. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `planConfig` | `{ label?: string; onOpen: () => void; }` | required | Opens the application plan tab; label defaults to Open plan | | `onApprove` | `RequestHandler` | required | Explicit plan approval; return an attributed plan receipt | | `onSuggestChanges` | `(() => void)` | required | Opens/focuses the host's change workflow without accepting the plan | | `approveLabel` | `string` | optional | Defaults to Approve plan | | `title` | `string` | required | | | `description` | `string` | optional | Supporting guidance associated with the field. | | `placement` | `"docked" \| "inline"` | optional | | | `state` | `RequestState` | optional | | | `onDismiss` | `(() => void)` | optional | | | `className` | `string` | optional | Additional classes for the component root. | ### planConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | No | | | `onOpen` | `() => void` | Yes | | ### state fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `"ready" \| "pending" \| "loading"` | Yes | | #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `"resolved"` | Yes | | | `recap` | `ResponseRecapProps` | Yes | | ### Shared request props [#shared-request-props] | Prop | Type | Behavior | | ------------- | ---------------------- | ------------------------------------------------------------------------ | | `title` | `string` | Required card title. | | `description` | `string` | Optional supporting text. | | `placement` | `'docked' \| 'inline'` | Docked by default; parent controls width and placement | | `state` | `RequestState` | Ready, loading skeleton, externally pending, or resolved with a recap | | `onDismiss` | `() => void` | Temporary dismissal via Not now or scoped Escape; not a submitted answer | | `className` | `string` | Layout override | `RequestHandler` returns `{ status: 'accepted', recap: ResponseRecapProps }` or `{ status: 'failed', message: string }`, directly or as a Promise. A thrown error shows a retry message and preserves the draft. The host persists accepted responses; the component displays the returned receipt. `state={{ status: 'resolved', recap }}` restores history. Use a stable request ID as the React key, and change the key when the request changes. External pending is a rendering state; authorization and idempotency remain server responsibilities. **REQUEST-01:** Docked tray uses pearl100, a raised shadow, radius16 and a white radius10 inset with 16px padding. Header40, footer at least44; controls wrap on narrow screens. **REQUEST-02:** The parent places the request 12px above Composer at the same width. Inline requests are flat and fill their message column. Escape only temporarily dismisses; no global listener is installed. **REQUEST-03:** Reading content does not animate. Existing Button and Popover motion respects reduced motion. Pending retains the draft and exposes a polite status. Acceptance never precedes the host result. ## Design rules [#design-rules] - **PLAN-01:** Keep the card compact: title, short scope summary, Open plan, Suggest changes, Approve plan. No Skip and no embedded plan editor. - **PLAN-02:** Opening the plan or asking for changes does not approve it. Return a `kind: 'plan'` recap with actor and timestamp after explicit server acceptance. The host owns the plan version. Key each review by request/version ID so an in-flight response cannot approve a newly replaced plan. ## Integration and accessibility [#integration-and-accessibility] Buttons have explicit names and keyboard focus. No network transport or AI SDK provider is required. Requests, tool execution, permission checks, durable outcomes and composer coordination stay in the application. See [AI content integration](/docs/guides/ai-content) for migration mappings; application migration is outside this build. --- # Popover Package: @commenda/pathfinder@0.1.0 Page: /docs/components/popover Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=384-2 ```tsx import { useState } from 'react'; import { Button } from '@commenda/pathfinder/button'; import { Input } from '@commenda/pathfinder/input'; import { Popover } from '@commenda/pathfinder/popover'; export const PopoverDefault = () => { const [open, setOpen] = useState(false); return ( Publish workflow} open={open} onOpenChange={setOpen} footerConfig={{ primaryAction: { label: 'Publish', type: 'submit', form: 'publish-workflow', }, }} >
{ event.preventDefault(); setOpen(false); }} >
); }; ``` Import `Popover` and optional `PopoverGroup` from `@commenda/pathfinder/popover`. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `mode` | `"form" \| "preview"` | conditional | form; click versus hover/focus | | `trigger` | `ReactElement>` | required | Required; forward native props/ref | | `title` | `string` | required | Required accessible heading | | `description` | `ReactNode` | optional | Supporting text and content | | `children` | `ReactNode` | required | Supporting text and content | | `positionConfig` | `{ side?: "top" \| "right" \| "bottom" \| "left"; align?: "start" \| "center" \| "end"; }` | optional | bottom / start, collision-aware | | `open` | `boolean` | conditional | Application can control opening | | `defaultOpen` | `boolean` | optional | Application can control opening | | `onOpenChange` | `((open: boolean) => void)` | conditional | Application can control opening | | `footerConfig` | `OverlayFooterConfig` | optional | Form only | | `focusConfig` | `OverlayFocusConfig` | optional | Form only | | `loadingConfig` | `{ isLoading: boolean; label?: string; }` | optional | Preview only | | `className` | `string` | optional | Popup classes | ### positionConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `side` | `"top" \| "bottom" \| "left" \| "right"` | No | | | `align` | `"start" \| "end" \| "center"` | No | | ### footerConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `primaryAction` | `OverlayAction` | No | | | `secondaryAction` | `OverlayAction` | No | | | `leadingContent` | `ReactNode` | No | | | `cancelLabel` | `string` | No | | ### focusConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `initialFocusRef` | `RefObject` | No | | | `returnFocusRef` | `RefObject` | No | | ### loadingConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isLoading` | `boolean` | Yes | | | `label` | `string` | No | | Forms use Base UI Popover; previews use PreviewCard. Width is 320px, clamped to the viewport; offset is 4px, radius 10px. Use Dialog or Sheet for longer tasks. Form footer actions have the same shape as [Dialog](/docs/components/dialog), including native form submission. Cancel closes; primary actions stay open until the application closes them. ## Preview recipes [#preview-recipes] ```tsx import { Avatar } from '@commenda/pathfinder/avatar'; import { Badge } from '@commenda/pathfinder/badge'; import { DescriptionList } from '@commenda/pathfinder/description-list'; import { USFlag } from '@commenda/pathfinder/flags/USFlag'; import { Popover, PopoverGroup } from '@commenda/pathfinder/popover'; export const PopoverPreview = () => (
Acme Inc.} > , }, { id: 'status', label: 'Status', value: , }, ]} /> Annual report} > Certificate} >

PDF · 248 KB

Uploaded 12 October 2026

Avery Morgan} >

avery@example.com

Email Avery
); ``` Entity, record, file, and person previews compose existing components. Keep them read-only. The trigger stays a real link and touch activation follows its destination. General previews allow one destination; the Figma person recipe permits up to two. Preview hover open and close delays are 150ms; keyboard focus also opens the preview without moving focus into it. ## Loading [#loading] ```tsx import { Popover } from '@commenda/pathfinder/popover'; export const PopoverLoading = () => ( Hover for loading preview} loadingConfig={{ isLoading: true }} > Entity details ); ``` Applications own fetching, cancellation, errors, and cached data. Skeletons match the content area and announce the loading state. Dialog and Sheet provide a local scope automatically. Wrap page siblings with `PopoverGroup` to ensure opening one closes another; standalone popovers remain independent. Controlled callers must honor `onOpenChange(false)`. There is no global singleton or required app provider. Read the [overlay rules](/docs/components/popover#design-rules). ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Popover [#popover] - **POPOVER-001:** Width320, radius10, menu shadow, 0.5px pearl300 edge, offset4, no backdrop. Form header16px start/8px end, top12/bottom4; content16px horizontal, footer12px vertical. Forms contain one or two fields; use larger surfaces for longer tasks. - **POPOVER-002:** Read-only previews open/close after150ms hover delays and support keyboard focus. Keep trigger destination on touch. No edit controls in hover previews. Person permits two links as an explicit exception to the general one-link recommendation. - **POPOVER-003:** Coordinate within the nearest local overlay/PopoverGroup. Fetching, caching, loading, permissions and application state remain outside the component. ### Motion and edge cases [#motion-and-edge-cases] Occasional overlays use motion for spatial consistency. Motion paths and timings follow Kakashi: Dialog fades and moves from 95% scale with a 2% vertical offset in 200ms both ways; Popover fades from 95% scale and an 8px offset toward its trigger in 150ms both ways. Popover direction follows the resolved placement after collision handling. Right Sheet slides in for 500ms and out for 300ms; bottom Sheet slides for 500ms both ways. Sheet panels do not fade during normal motion, and modal backdrops follow the panel duration. Pathfinder retains its foundation ease-out curve for Dialog/Popover and drawer curve `(0.32,0.72,0,1)` for Sheets. These are deliberate curve choices; this is not a copy of Kakashi’s legacy Radix keyframe implementation. Base UI continues to own presence, focus and dismissal. Transitions use a single `transform` value plus opacity where relevant, so Tailwind v4’s separate scale/translate properties cannot bypass interpolation. Closing or reopening mid-transition reverses from the current rendered position. Keyboard opening and dismissal remain immediate, including Close and Cancel activation. Reduced motion removes scale and translation: Dialog/Popover keep their fade, while Sheets change position immediately and their modal backdrop can fade. Drag writes only the popup transform, never layout or inherited animation variables. Programmatic openings animate by default; explicit keyboard interactions suppress motion. --- # Progress Package: @commenda/pathfinder@0.1.0 Page: /docs/components/progress Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=269-2 ```tsx import { ProgressBar } from '@commenda/pathfinder/progress'; ; ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` Import `@commenda/pathfinder/styles.css` once after host CSS. Styles are precompiled with Tailwind v4; consumers need no Tailwind scanning. Interactive callbacks belong in client components in Next.js App Router. ## Usage [#usage] ```tsx import { ProgressBar } from '@commenda/pathfinder/progress'; ; ``` ## API reference [#api-reference] | Prop | Type | Default / behavior | | ------------- | ------------------------------ | --------------------------------------- | | `label` | `string` | Required stable task name | | `value` | `number \| null` | Required; null is indeterminate | | `max` | `number` | 100; invalid maxima normalize to 100 | | `status` | `active \| complete \| failed` | active; complete requires numeric value | | `size (bar)` | `sm \| md` | sm: 4 px; md: 8 px | | `size (ring)` | `16 \| 24 \| 32` | 16 | | `helperText` | `string` | Bar count or failure explanation | | `hideLabel` | `boolean` | false; accessible name stays | | `showValue` | `boolean` | true for determinate bars | | `valueText` | `string` | Optional human-readable ARIA value | Clamp values to 0–max; nonfinite numeric values become 0. Reaching max does not confirm success: explicitly set status="complete". Failure retains its last measured value. The application owns monotonic updates, task identity, retry and cancellation. ProgressRing retains its previous status-icons import path and accepts continuous values. Use a chart or meter for quotas and budgets. ## Examples [#examples] ### Statuses [#statuses] ```tsx <> ; ``` ### Sizes [#sizes] ```tsx <> ; ``` ### Ring [#ring] ```tsx import { ProgressRing } from '@commenda/pathfinder/progress'; <> {([16, 24, 32] as const).map((size) => ( ))} 3 of 9 documents ; ``` Read the [design guidance](/docs/components/progress#design-rules) before using these components. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Progress — 269:2 [#progress--2692] - **PROGRESS-01:** Long measured tasks such as upload, import, export or sync. Bar for one task; ring for a completion count beside text. Never use progress for budgets, limits or quotas. - **PROGRESS-02:** Keep the task label stable, and put counts or the current step in helper text. Avoid backwards progress and reaching 100% while still waiting. Success is explicit; numeric max alone does not confirm it. - **PROGRESS-03:** Failure preserves its last measured value, turns red and explains the problem; the application offers Retry. Offer cancellation for long work and explain what happens to completed work. Use null for unknown length, then switch to measured values when available. - **PROGRESS-04:** Bar heights 4 by default or 8 for a page's main task. Track pearl 300/full radius; active blue 700, complete green 700, failed red 700. Label/13 Strong, value Label/13 pearl 800, helper Copy/12 pearl 800 or red 800; gaps 8, status icon 16 with gap 6. Value changes ease out over 300 ms. Indeterminate segment is 30% and loops linearly over 1.2 seconds. Reduced motion removes spatial loops. Ring sizes 16/24/32 retain the source geometry and accept continuous counts. ### Explicit differences and boundaries [#explicit-differences-and-boundaries] - **Progress:** compose the existing 16px ProgressRing for the Figma ring pattern. `TableCell` also offers a linear progress renderer; these are named alternatives, not interchangeable artwork. --- # Question card Package: @commenda/pathfinder@0.1.0 Page: /docs/components/question-card Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=607-4462 ```tsx import { QuestionCard } from '@commenda/pathfinder/question-card'; ({ id: entity.id, label: entity.name }))} writeInConfig={{}} onSubmit={async (answer) => { const receipt = await saveAnswer(request.id, answer); return receipt.ok ? { status: 'accepted', recap: receipt.recap } : { status: 'failed', message: receipt.message }; }} onDismiss={() => temporarilyHide(request.id)} />; ``` ## Usage [#usage] ```tsx import { QuestionCard } from '@commenda/pathfinder/question-card'; ({ id: entity.id, label: entity.name }))} writeInConfig={{}} onSubmit={async (answer) => { const receipt = await saveAnswer(request.id, answer); return receipt.ok ? { status: 'accepted', recap: receipt.recap } : { status: 'failed', message: receipt.message }; }} onDismiss={() => temporarilyHide(request.id)} />; ``` ## Multiple answers [#multiple-answers] ```tsx import { QuestionCard } from '@commenda/pathfinder/question-card'; ({ id: entity.id, label: entity.name }))} writeInConfig={{}} onSubmit={async (answer) => { const receipt = await saveAnswer(request.id, answer); return receipt.ok ? { status: 'accepted', recap: receipt.recap } : { status: 'failed', message: receipt.message }; }} onDismiss={() => temporarilyHide(request.id)} />; ``` ## More options and search [#more-options-and-search] ```tsx import { QuestionCard } from '@commenda/pathfinder/question-card'; ({ id: entity.id, label: entity.name }))} writeInConfig={{}} onSubmit={async (answer) => { const receipt = await saveAnswer(request.id, answer); return receipt.ok ? { status: 'accepted', recap: receipt.recap } : { status: 'failed', message: receipt.message }; }} onDismiss={() => temporarilyHide(request.id)} />; ``` ## Inline placement [#inline-placement] ```tsx import { QuestionCard } from '@commenda/pathfinder/question-card'; ({ id: entity.id, label: entity.name }))} writeInConfig={{}} onSubmit={async (answer) => { const receipt = await saveAnswer(request.id, answer); return receipt.ok ? { status: 'accepted', recap: receipt.recap } : { status: 'failed', message: receipt.message }; }} onDismiss={() => temporarilyHide(request.id)} />; ``` ## Failed submission and retry [#failed-submission-and-retry] ```tsx // Return { status: 'failed', message: 'Please try again.' } from the handler to retain this card. import { QuestionCard } from '@commenda/pathfinder/question-card'; ({ id: entity.id, label: entity.name }))} writeInConfig={{}} onSubmit={async (answer) => { const receipt = await saveAnswer(request.id, answer); return receipt.ok ? { status: 'accepted', recap: receipt.recap } : { status: 'failed', message: receipt.message }; }} onDismiss={() => temporarilyHide(request.id)} />; ``` ## Loading [#loading] ```tsx import { QuestionCard } from '@commenda/pathfinder/question-card'; ({ id: entity.id, label: entity.name }))} writeInConfig={{}} onSubmit={async (answer) => { const receipt = await saveAnswer(request.id, answer); return receipt.ok ? { status: 'accepted', recap: receipt.recap } : { status: 'failed', message: receipt.message }; }} onDismiss={() => temporarilyHide(request.id)} />; ``` Reset restores the example. Simulate failure retains the card for retry; no backend request is made. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `options` | `readonly QuestionOption[]` | required | Stable unique `id`, `label`, optional `description`, decorative `icon`, `disabled` | | `mode` | `"single" \| "multiple"` | optional | Single by default; activating a single option submits immediately | | `defaultAnswer` | `QuestionAnswer` | optional | Initial draft; use a new React key for a new request | | `onAnswerChange` | `((answer: QuestionAnswer) => void)` | optional | Observe draft changes for host persistence | | `onSubmit` | `RequestHandler` | required | Accept or reject explicitly; do not clear the draft before acceptance | | `onSkip` | `RequestHandler` | optional | Optional explicit skip, distinct from temporary dismissal | | `writeInConfig` | `{ placeholder?: string; label?: string; maxLength?: number; }` | optional | Enables a written answer; single choice clears it when selecting an option | | `paginationConfig` | `{ index: number; count: number; onChange: (index: number) => void; }` | optional | Zero-based host-owned question navigation; pending disables navigation | | `title` | `string` | required | | | `description` | `string` | optional | Supporting guidance associated with the field. | | `placement` | `"docked" \| "inline"` | optional | | | `state` | `RequestState` | optional | | | `onDismiss` | `(() => void)` | optional | | | `className` | `string` | optional | Additional classes for the component root. | ### defaultAnswer fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `optionIds` | `readonly string[]` | Yes | | | `text` | `string` | Yes | | ### writeInConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `placeholder` | `string` | No | | | `label` | `string` | No | | | `maxLength` | `number` | No | | ### paginationConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `index` | `number` | Yes | | | `count` | `number` | Yes | | | `onChange` | `(index: number) => void` | Yes | | ### state fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `"ready" \| "pending" \| "loading"` | Yes | | #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | `"resolved"` | Yes | | | `recap` | `ResponseRecapProps` | Yes | | ### Shared request props [#shared-request-props] | Prop | Type | Behavior | | ------------- | ---------------------- | ------------------------------------------------------------------------ | | `title` | `string` | Required card title. | | `description` | `string` | Optional supporting text. | | `placement` | `'docked' \| 'inline'` | Docked by default; parent controls width and placement | | `state` | `RequestState` | Ready, loading skeleton, externally pending, or resolved with a recap | | `onDismiss` | `() => void` | Temporary dismissal via Not now or scoped Escape; not a submitted answer | | `className` | `string` | Layout override | `RequestHandler` returns `{ status: 'accepted', recap: ResponseRecapProps }` or `{ status: 'failed', message: string }`, directly or as a Promise. A thrown error shows a retry message and preserves the draft. The host persists accepted responses; the component displays the returned receipt. `state={{ status: 'resolved', recap }}` restores history. Use a stable request ID as the React key, and change the key when the request changes. External pending is a rendering state; authorization and idempotency remain server responsibilities. **REQUEST-01:** Docked tray uses pearl100, a raised shadow, radius16 and a white radius10 inset with 16px padding. Header40, footer at least44; controls wrap on narrow screens. **REQUEST-02:** The parent places the request 12px above Composer at the same width. Inline requests are flat and fill their message column. Escape only temporarily dismisses; no global listener is installed. **REQUEST-03:** Reading content does not animate. Existing Button and Popover motion respects reduced motion. Pending retains the draft and exposes a polite status. Acceptance never precedes the host result. ## Design rules [#design-rules] - **QUESTION-01:** Single pointer/Space/Enter or number activation submits; arrow keys only move selection. Multiple choices and written answers require Continue (Enter also submits written text outside IME composition). - **QUESTION-02:** Up to four options remain inline. Five or more show the first three and More options; eight or more enable search. Selected overflow labels remain visible outside the popup. Only the selected number lead turns blue. - **QUESTION-03:** Number shortcuts operate only inside this card, never inside text fields or the overflow dialog. They cannot approve an action. Escape dismisses the popup first, then the request on a subsequent Escape. - **QUESTION-04:** Failures retain the answer, pending blocks duplicate submission, and acceptance renders the supplied ResponseRecap. Use unique IDs rather than labels as values. Keep the option set stable for a request. RadioGroup primitives provide single-choice arrow navigation; Checkbox provides multiple-choice semantics. The overflow uses a Popover instead of an action-only Dropdown so choices retain radio/checkbox semantics. ## Integration and accessibility [#integration-and-accessibility] Buttons have explicit names and keyboard focus. No network transport or AI SDK provider is required. Requests, tool execution, permission checks, durable outcomes and composer coordination stay in the application. See [AI content integration](/docs/guides/ai-content) for migration mappings; application migration is outside this build. --- # Radio group Package: @commenda/pathfinder@0.1.0 Page: /docs/components/radio-group Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=199-3 ```tsx import { RadioGroup } from '@commenda/pathfinder/radio-group'; ; ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` Import `@commenda/pathfinder/styles.css` once at the app root. The stylesheet is precompiled with Tailwind v4; your application does not need Tailwind or source scanning. Event handlers belong in a client component in Next.js App Router. ## Usage [#usage] ```tsx import { RadioGroup } from '@commenda/pathfinder/radio-group'; ; ``` | Prop | Type | Default / behavior | | ---------------------- | ---------------------------- | ---------------------------------------------------------------- | | `label` | `string` | Required group name, visible by default | | `hideLabel` | `boolean` | `false`; hides only the visible group heading | | `description` | `string` | Optional group guidance | | `options` | `readonly RadioOption[]` | Required; stable, unique string values | | `value` | `string \| null` | Controlled selected value; `null` means intentionally unanswered | | `defaultValue` | `string` | Uncontrolled initial selection; omitted means unanswered | | `onValueChange` | `(value: string) => void` | Required with controlled `value`, optional when uncontrolled | | `variant` | `'default' \| 'card'` | `'default'` | | `orientation` | `'vertical' \| 'horizontal'` | `'vertical'`; horizontal layout wraps and cards share row height | | `disabled` | `boolean` | Disables interaction for the control or group. | | `readOnly` | `boolean` | Keeps the current value visible while preventing changes. | | `required` | `boolean` | Requires a value for native form validation. | | `name` | `string` | Name used when the control submits its value with a form. | | `form` | `string` | ID of the form associated with the control. | | `inputRef` | `Ref` | Access to the underlying form input. | | `className` | `string` | Additional class names for the outer layout. | | `ref` | `Ref` | Access to the interactive root element. | | Native events and ARIA | Base UI group props | Typed passthrough | `RadioOption`: | Property | Type | Purpose | | ---------------- | ------------------- | ------------------------------------------------------------------ | | `value` | `string` | Required stable, unique identity and submitted value | | `label` | `string` | Required option name | | `description` | `string` | Supporting explanation | | `details` | `readonly string[]` | Additional card bullets | | `disabled` | `boolean` | Unavailable option; keyboard navigation skips it | | `disabledReason` | `string` | Visible and associated while the option or whole group is disabled | Use two to five visible options. Prefer a sensible default, but permit an empty group when explicitly choosing is necessary. Do not auto-select the first item or silently replace a removed controlled value. The owner reconciles changing options. Labels select their option; arrow keys move selection, and only one value submits. For longer lists use Select. Never use one radio for a boolean setting. Radio cards use `variant="card"`; they are the same radio group, not an unrelated clickable card API. Figma calls its collection “Radio card” but includes checkbox cards; those belong to Checkbox to preserve multiple-selection semantics. Cards have 16px padding, 12px control/text gap, 8px radius and 13px medium labels. Their width adapts from the 320px Figma specimen. ## Examples [#examples] ### Descriptions [#descriptions] ```tsx ; ``` ### Radio cards [#radio-cards] ```tsx ; ``` ### Disabled group [#disabled-group] ```tsx ; ``` Read the [Figma choice-control rules](/docs/components/radio-group#design-rules) for design guidance, source discrepancies and accessibility requirements. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Radio and cards [#radio-and-cards] Source: [199:3](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=199-3), radio variants 200:46, card variants 201:413, guidance 205:320. - **RADIO-01** — One choice from two to five visible options. Use Select for longer lists or limited space. A lone boolean option uses Checkbox/Switch. Arrow keys move selection and skip disabled items; Tab enters/leaves the group. - **RADIO-02** — Prefer a sensible default. Leave the group unanswered only when deliberate choice is required. Never silently choose the first value or replace a removed controlled value. The application reconciles dynamic options. - **RADIO-03** — Order options by frequency, size or time rather than alphabetically by default. Circle 16px, selected white dot 6px; colors and focus match Checkbox. Group row gap 8px. - **CARD-01** — Use a card when each option needs title and description. Keep cards in one row equal-height. The “Radio card” Figma collection contains both radio and checkbox controls; Pathfinder exposes `RadioGroup variant="card"` for one choice and `Checkbox variant="card"` for independent choices. - **CARD-02** — Card padding 16px, control/text gap 12px, radius 8px. Title Label/13 Strong; description Copy/12 with 4px gap. Detail bullets have 2px gap and 4px top padding. Surface remains white when selected; border becomes blue-700. Focus surrounds the whole card. Disabled surface/border/text use soft-pearl-100/300/600. The 320px specimen width adapts to the container. ### Implementation decisions and source differences [#implementation-decisions-and-source-differences] The complete prop tables are on [Checkbox](/docs/components/checkbox), [RadioGroup](/docs/components/radio-group) and [Switch](/docs/components/switch). Source rule IDs are also kept in the component implementation comments and exported through `llms.txt`, `llms-full.txt` and the Markdown routes. - **CHOICE-01** — The sheets describe 8px list gaps but some documentation frames place rows 32px apart. Use the documented 8px component-list gap; documentation specimen whitespace is not a new size token. - **CHOICE-02** — Built-in text is plain, wrapping content. Do not place links, buttons or nested interactive controls inside labels or cards. Disabled reasons are visible descriptions, not tooltip-only explanations on controls removed from keyboard navigation. - **CHOICE-03** — Visual control size is not the hit target. Coarse-pointer rows provide at least 44px height, while preserving 16px marks and the 36×20px switch track. Cards already exceed this height. - **CHOICE-04** — Hover/focus are real browser states, not public cosmetic props. Retain native `name`, `value`, `form`, `required`, `readOnly`, disabled semantics and input refs. `hideLabel` preserves the required accessible label. - **CHOICE-05** — Use Pathfinder's existing Check/Minus optical artwork at 12px. Boxes, circles, tracks and thumbs are interactive primitive geometry, styled with the Figma tokens; they are not flattened screenshots. This keeps focus, RTL and switch movement functional. --- # Reasoning Package: @commenda/pathfinder@0.1.0 Page: /docs/components/reasoning Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=601-225 ```tsx import { Reasoning } from '@commenda/pathfinder/reasoning'; import { Markdown } from '@commenda/pathfinder/markdown'; ; ``` ## Usage [#usage] ```tsx import { Reasoning } from '@commenda/pathfinder/reasoning'; import { Markdown } from '@commenda/pathfinder/markdown'; ; ``` ## Live activity [#live-activity] ```tsx import { Reasoning } from '@commenda/pathfinder/reasoning'; import { Markdown } from '@commenda/pathfinder/markdown'; ; ``` Use Start/Finish activity to exercise streaming updates. New run resets disclosure intent. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `children` | `ReactNode` | required | Required app-provided summary; compose Markdown explicitly | | `isStreaming` | `boolean` | optional | Whether this reasoning part is live, not whether the whole answer is live | | `duration` | `number` | optional | Supplied elapsed seconds; invalid values use “Thought process” | | `label` | `string` | optional | Override the trigger for localization | | `disclosureConfig` | `ActivityDisclosureConfig` | optional | Manual intent overrides automation; controlled open is entirely host-owned | | `className` | `string` | optional | Layout override | ### disclosureConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `open` | `boolean` | No | | | `defaultOpen` | `boolean` | No | | | `onOpenChange` | `((open: boolean) => void)` | No | | ## Design rules [#design-rules] - **REASONING-01:** Only render summaries supplied by the application. Never invent or reconstruct private model reasoning. - **REASONING-02:** Live content opens automatically. Stop isStreaming when the next message part arrives; collapse after 1s. A user toggle, explicit defaultOpen, or controlled open wins over automation. History starts closed. Use a new React key for a new run. - **REASONING-03:** Copy13 pearl800, a pearl300 hairline rail, 12px header gap and 12px inset. Compose Pathfinder Markdown when needed; plain text stays lightweight. `disclosureConfig.onOpenChange` reports user toggle requests. A controlled `open` value delegates timing and persistence to the host. Automatic collapse also preserves a panel containing keyboard focus. ## Accessibility and integration [#accessibility-and-integration] Disclosure buttons support Enter and Space, preserve focus, and expose `aria-expanded`. Payload copy buttons are outside the disclosure trigger. Pointer disclosure uses the existing 200ms height transition; keyboard and reduced-motion interaction are immediate. Long text wraps while code scrolls inside its own region. Use one meaningful ThinkingIndicator status per active response rather than announcing every streamed token or tool payload. The application owns requests, cancellation, approval, retries, durations, and history persistence. No AI SDK provider is required. See [AI content integration](/docs/guides/ai-content) for migration mappings; consumer migration is outside this phase. --- # Response recap Package: @commenda/pathfinder@0.1.0 Page: /docs/components/response-recap Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=607-31552 ```tsx import { ResponseRecap } from '@commenda/pathfinder/response-recap'; ; ``` ## Usage [#usage] ```tsx import { ResponseRecap } from '@commenda/pathfinder/response-recap'; ; ``` Long answers and Unicode text wrap within the available width. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `items` | `readonly ResponseRecapItem[]` | required | Stable IDs and plain text; each pair renders as `dt`/`dd` | | `kind` | `"questions" \| "upload" \| "approval" \| "plan"` | conditional | Defaults to questions | | `attribution` | `{ name: string; dateTime: string; label: string; }` | conditional | Required by TypeScript for approval and plan; ISO dateTime plus localized display label | | `className` | `string` | optional | Layout override | ### attribution fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | `string` | Yes | | | `dateTime` | `string` | Yes | | | `label` | `string` | Yes | | ## Design rules [#design-rules] - **RECAP-01:** Flat white card, hairline border, radius16, padding16. Questions use Label12 pearl800; answers use Copy13 with 4px separation and 12px between groups. - **RECAP-02:** Render only accepted responses. Approval/plan recaps require actor and timestamp. Persist the receipt in its originating message so it survives reload and appears in history. The recap is informational, not editable. Use the original question for corrections or a new request. Long text wraps; no Markdown parser or AI provider is imported. ## Integration and accessibility [#integration-and-accessibility] Buttons have explicit names and keyboard focus. No network transport or AI SDK provider is required. Requests, tool execution, permission checks, durable outcomes and composer coordination stay in the application. See [AI content integration](/docs/guides/ai-content) for migration mappings; application migration is outside this build. --- # Run List Package: @commenda/pathfinder@0.1.0 Page: /docs/components/run-list Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=670-38136 ```tsx import { RunList } from '@commenda/pathfinder/run-list'; ; ``` ## Usage [#usage] ```tsx import { RunList } from '@commenda/pathfinder/run-list'; ; ``` The host provides newest-first runs and a localized timestamp label plus a machine-readable `dateTime`. Opening a row calls `onOpen` and requests `onMarkRead` only if it was unread. Update the controlled data after your app accepts that operation. Fetching, scheduling, routing, and unread persistence stay outside the component. ## Status and unread indicators [#status-and-unread-indicators] Each row has a stable 16px leading slot. Awaiting input is amber, failures red, running uses a blue arc, and an unread completed run has a blue dot. Canceled and skipped runs remain neutral even when unread; their trailing label explains the outcome. Queued and read completed runs use a neutral ring. Status is included in the button's accessible name, so it is not communicated by color alone. The header menu exposes only supplied actions: Run now, Mark all as read, and Agent settings. Its button is separate from the disclosure trigger. Output counts hide at zero and use compact formatting for large values. Unread counts describe the loaded items, not an assumed server total. ## Empty, loading, and collapsed [#empty-loading-and-collapsed] ```tsx import { RunList } from '@commenda/pathfinder/run-list'; ; ``` ```tsx import { RunList } from '@commenda/pathfinder/run-list'; ; ``` ```tsx import { RunList } from '@commenda/pathfinder/run-list'; ; ``` A collapsed section shows the loaded unread count. Initial loading uses skeleton rows; refreshing retains the existing history. Supply `nextRun` only when there is a schedule. Pagination preserves existing items, disables repeated requests while loading, and displays `error` next to the retry control. ## Responsive behavior [#responsive-behavior] Long dates, large counts, empty lists, row removal, and new-run insertion use the same controls as Output List. A mobile menu stays available without hover. The host should guard overlapping requests and retain IDs across page loads. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `items` | `readonly RunItem[]` | required | Newest-first run history | | `onOpen` | `((id: string) => void)` | required | Navigate to the run conversation | | `onMarkRead` | `((id: string) => void)` | optional | Request unread persistence | | `selectedId` | `string \| null` | optional | Controlled active run | | `onSelectionChange` | `((id: string) => void)` | optional | Notify selection intent | | `nextRun` | `{ label: string; dateTime?: string; }` | optional | Optional scheduled run | | `actionConfig` | `{ onRunNow?: () => void; onMarkAllRead?: () => void; onSettings?: () => void; }` | optional | Optional header actions | | `paginationConfig` | `{ hasMore: boolean; isLoading: boolean; onLoadMore: () => void; error?: string; }` | optional | Older history and errors | | `disclosureConfig` | `OutputDisclosureConfig` | optional | App-persisted or local disclosure | | `loadingConfig` | `{ isLoading: boolean; label?: string; }` | optional | Initial skeletons / busy state | | `title` | `string` | optional | Copy and host placement | | `emptyLabel` | `string` | optional | Copy and host placement | | `className` | `string` | optional | Copy and host placement | ### nextRun fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | Yes | | | `dateTime` | `string` | No | | ### actionConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `onRunNow` | `(() => void)` | No | | | `onMarkAllRead` | `(() => void)` | No | | | `onSettings` | `(() => void)` | No | | ### paginationConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `hasMore` | `boolean` | Yes | | | `isLoading` | `boolean` | Yes | | | `onLoadMore` | `() => void` | Yes | | | `error` | `string` | No | | ### disclosureConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `open` | `boolean` | Yes | | | `onOpenChange` | `(open: boolean) => void` | Yes | | Do not supply: `defaultOpen`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `defaultOpen` | `boolean` | No | | | `onOpenChange` | `((open: boolean) => void)` | No | | Do not supply: `open`. ### loadingConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isLoading` | `boolean` | Yes | | | `label` | `string` | No | | A `RunItem` has `id`, `timestamp: { dateTime, label }`, `status`, optional `outputCount`, `unread`, and `disabled`. Status accepts `queued`, `running`, `awaiting`, `completed`, `failed`, `canceled`, or `skipped`. Disabled rows cannot open. Pagination is explicit; this small rail does not virtualize an unbounded run history. ## Design rules and compatibility [#design-rules-and-compatibility] - **RUN-01:** Match Output List's compact card and 32px row geometry. The status indicator leads; the date is the primary label. - **RUN-02:** Canceled and skipped runs are neutral, never failures. Awaiting, failed, and running take precedence over unread; reduced motion stops the running animation. - **RUN-03:** Opening an unread run requests marking it read. Keep all mutations and routing in host callbacks; selected and unread state remain controlled. - **RUN-04:** Keep header actions independent from disclosure, retain existing rows during pagination, and preserve keyboard focus during removal. The existing enterprise `AgentRunsRail` can map its IDs, formatted timestamps, selected conversation, paging, and mutation callbacks into this API. Its current canceled-as-failed mapping must change intentionally to the neutral Figma behavior. No consumer migration is included here. Compose [Output List](/docs/components/output-list) beside it for the responsive panel and complete simulated workflow. --- # Search Package: @commenda/pathfinder@0.1.0 Page: /docs/components/search Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=635-13518 ```tsx import { Search } from '@commenda/pathfinder/search'; export const SearchDefault = () => ( ); ``` ## Installation [#installation] Import the component in your screen: ```tsx import { Search } from '@commenda/pathfinder/search'; ``` Load the stylesheet once in your app root, after host styles: ```tsx import '@commenda/pathfinder/styles.css'; ``` Pathfinder ships precompiled CSS; no Tailwind setup is required. In Next.js App Router, put interactive state and callbacks in a client component. See [installation](/docs/getting-started/installation) for the workspace setup. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `variant` | `"fixed" \| "expandable" \| "global"` | optional | fixed; heights 28/28/40px | | `loadingConfig` | `{ isLoading: boolean; label?: string; }` | optional | Spinner replaces search icon | | `shortcutConfig` | `{ key: string; label: string; scopeId: string; }` | optional | Opt-in scoped shortcut; ignores typing targets | | `onSearch` | `((query: string) => void)` | optional | Enter submission; ignored during IME composition | | `label` | `string` | conditional | Visible field label. Supply an accessible name when it is omitted. | | `hideLabel` | `boolean` | optional | Visually hide the label while preserving its accessible name. | | `aria-label` | `string` | conditional | Accessible name when the field has no visible label. | | `required` | `boolean` | conditional | Mark the field as required. See validation guidance for the component. | | `labelConfig` | `{ optionalLabel: string; help?: never; } \| { help: string; optionalLabel?: never; }` | optional | Choose either `optionalLabel` or `help`, never both. `never` excludes the other field. | | `description` | `string` | optional | Supporting guidance associated with the field. | | `errorConfig` | `{ message: string; reveal?: "after-blur" \| "immediate"; }` | optional | Validation message and when it becomes visible. | | `disableConfig` | `DisableConfig` | optional | Disabled state with an optional explanation. | | `value` | `string` | conditional | | | `onValueChange` | `((value: string) => void)` | conditional | | | `defaultValue` | `string` | optional | | ### loadingConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isLoading` | `boolean` | Yes | | | `label` | `string` | No | | ### shortcutConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `key` | `string` | Yes | | | `label` | `string` | Yes | | | `scopeId` | `string` | Yes | | ### labelConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `optionalLabel` | `string` | Yes | | Do not supply: `help`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `help` | `string` | Yes | | Do not supply: `optionalLabel`. ### errorConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `message` | `string` | Yes | | | `reveal` | `"after-blur" \| "immediate"` | No | | ### disableConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isDisabled` | `boolean` | Yes | | | `reason` | `string` | No | | Supports Input’s shared field presentation and native search-input attributes. Use hideLabel where the surrounding toolbar supplies visual context; an accessible label is still required. ## Keyboard and request behavior [#keyboard-and-request-behavior] Application code handles debounce and requests; onValueChange is immediate. onSearch is Enter submission, not each keystroke. Escape clears the query, then blurs a fixed/global input or collapses expandable search and restores trigger focus. The clear button clears and folds expandable search. Loading is application-owned. A result overlay handles Escape first through onKeyDown and preventDefault; the field then retains its query. Search result navigation is a documented composition, separate from selecting a saved value. Do not implement a second global shortcut listener on every instance: shortcutConfig binds only to the specified scope element and ignores editing targets. ### Variants [#variants] ```tsx import { Search } from '@commenda/pathfinder/search'; export const SearchVariants = () => (
); ``` ### States [#states] ```tsx import { Search } from '@commenda/pathfinder/search'; export const SearchStates = () => (
); ``` ### Results [#results] ```tsx import { useState } from 'react'; import { Search } from '@commenda/pathfinder/search'; export const SearchResults = () => { const [query, setQuery] = useState(''); const [open, setOpen] = useState(false); return (
{ setQuery(next); setOpen(true); }} onKeyDown={(event) => { if (event.key === 'Escape' && open) { event.preventDefault(); setOpen(false); } }} /> {open && query && ( )}
); }; ``` Read [selection and search rules](/docs/components/search#design-rules) for source provenance, API ownership, and constraints. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Source rules [#source-rules] | Rule | Figma source | Contract | | ---------- | -------------------- | ----------------------------------------------------------------------------------- | | SEARCH-001 | 636:31822 | Fixed/expandable 28px; expandable opens to 220px; global 40px/max720. | | SEARCH-002 | 637:32230, 637:32236 | Expandable in table toolbar; result counts belong in table range text. | | SEARCH-003 | 637:32193, 637:1779 | Escape closes results, clears, then blurs/collapses. Clear folds expandable search. | Sources: [Search](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=635-13518), [Select](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=179-2), [MultiSelect](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=865-3077). The supplied Select node 182:1182 is its dimensions frame; 180:1177 is the actual component set. ### Accessibility and forms [#accessibility-and-forms] Labels, descriptions, error reveal, and disable reasons follow the shared field contract. Single selection serializes one ID; multiple selection serializes repeated names. Trigger refs and form reset work for both primitive paths. Searchable popups provide an explicitly named query input. Load more and retry are keyboard-accessible buttons outside the option list. Tag remove buttons are siblings of the trigger, never nested inside it. Long selected labels remain available through tooltips. Search result compositions must handle overlay Escape before field clear/collapse through preventDefault. Flags, chevrons, and clear/remove icons sit in explicit square flex containers with both axes centered. Do not rely on inline SVG baseline alignment or compensate with pixel offsets. The selected value, tags, and trailing controls share the field’s vertical center. Row Checkbox instances are inert visual indicators; the option owns selection and accessibility semantics. --- # Segmented control Package: @commenda/pathfinder@0.1.0 Page: /docs/components/segmented-control Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=211-2 ```tsx import { SegmentedControl } from '@commenda/pathfinder/segmented-control'; ; ``` ## Usage [#usage] ```tsx import { SegmentedControl } from '@commenda/pathfinder/segmented-control'; ; ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` The package currently lives in the Commenda workspace. External installation requires a published version or packed tarball. Import the precompiled stylesheet once; no application Tailwind setup is required. ## Examples [#examples] ### Small [#small] ```tsx ; ``` ### Icon-only options [#icon-only-options] ```tsx , }, { value: 'annual', label: 'List view', iconOnly: true, leadingIcon: , }, ]} />; ``` ### Disabled [#disabled] ```tsx ; ``` ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `label` | `string` | required | Visible field label. Supply an accessible name when it is omitted. | | `options` | `SegmentedControlOption[]` | required | | | `size` | `"sm" \| "md"` | optional | | | `className` | `string` | optional | Additional classes for the component root. | | `disabledReason` | `string` | optional | | | `value` | `string` | conditional | | | `onValueChange` | `((value: string) => void)` | conditional | | | `defaultValue` | `string` | conditional | | Icon-only options use `iconOnly: true` and require `leadingIcon`; label remains required for the tooltip and accessible name. No public Root/Item parts or arbitrary row renderer. `SEGMENTED-SELECTION-01`: Use two to four short, equal-weight choices with exactly one selected. Changes update immediately without Apply. Use one or two words per label, similar lengths. Use Tabs for more options or content sections, Switch for on/off, Button for actions. No empty selection. The caller must supply a valid value and reconcile removal of a selected option. Tab enters the selected radio; arrows select enabled alternatives, including RTL behavior. Re-activating the selected option never deselects it. Name submits the selected string with the form. Disabled choices cannot be selected. Selected state remains visible when the whole group is disabled. The source uses content-width segments, 16px icons at both sizes, a soft-pearl tray, 2px padding/gaps, 8px outer and 6px inner radii. Selected segment uses white, a hairline and control elevation. Exact source geometry and prose discrepancies are in the rules reference. Import `@commenda/pathfinder/styles.css` once at the app root. Event handlers require a client component in Next.js App Router. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Menus and selection [#menus-and-selection] - `SEGMENTED-SELECTION-01` — Figma: two to four short, equal-weight modes, one always selected, immediate change with no Apply. Use Tabs for more options/content sections, Switch for binary on/off, Button for actions. One or two words per label with similar lengths. Icon-only choices need tooltip and accessible name. - `SEGMENTED-STYLE-01` — Figma: soft-pearl 200 tray, 300 hairline, radius 8, padding/gap 2. Segment radius 6, horizontal padding 8 sm/12 md, Label/13 Strong, optional 16px icon with gap 6. White selected segment uses control elevation. Hover fills 300; disabled dims the label. --- # Select Package: @commenda/pathfinder@0.1.0 Page: /docs/components/select Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=182-1182 ```tsx import { DEFlag, GBFlag, INFlag, SGFlag } from '@commenda/pathfinder/flags'; import { Select } from '@commenda/pathfinder/select'; const entityOptions = [ { value: 'gb', label: 'Meridian UK Ltd', leadingIcon: }, { value: 'de', label: 'Meridian GmbH', leadingIcon: }, { value: 'in', label: 'Meridian India', leadingIcon: }, { value: 'sg', label: 'Meridian Singapore', leadingIcon: , }, ]; export const SelectDefault = () => ( ); ``` ## Clearable [#clearable] ```tsx import { DEFlag, GBFlag, INFlag, SGFlag } from '@commenda/pathfinder/flags'; import { Select } from '@commenda/pathfinder/select'; const entityOptions = [ { value: 'gb', label: 'Meridian UK Ltd', leadingIcon: }, { value: 'de', label: 'Meridian GmbH', leadingIcon: }, { value: 'in', label: 'Meridian India', leadingIcon: }, { value: 'sg', label: 'Meridian Singapore', leadingIcon: , }, ]; export const SelectClearable = () => ( , value: 'gb', label: 'United Kingdom', keywords: ['Britain', 'UK'], }, { leadingIcon: , value: 'de', label: 'Germany', keywords: ['Deutschland'], }, { leadingIcon: , value: 'in', label: 'India' }, ]} /> ); ``` ### Groups [#groups] ```tsx import { DEFlag, GBFlag } from '@commenda/pathfinder/flags'; import { Select } from '@commenda/pathfinder/select'; export const SelectGroups = () => ( , value: 'gb', label: 'Meridian UK Ltd', }, ]} errorConfig={{ message: 'Choose an entity.', reveal: 'immediate' }} /> ); ``` ### Large [#large] ```tsx import { Select } from '@commenda/pathfinder/select'; export const SelectLarge = () => (

The page remains available while reviewing this filing.

); ``` Import `Sheet` from `@commenda/pathfinder/sheet`. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `side` | `"right" \| "bottom"` | conditional | right | | `size` | `"sm" \| "md" \| "lg"` | optional | md; right widths 400 / 520 / 720px | | `modal` | `boolean` | optional | false; page stays usable and page clicks do not dismiss | | `dragConfig` | `{ dismissible: boolean; }` | optional | Bottom only; true by default | | `title` | `string` | required | Required accessible title | | `description` | `ReactNode` | optional | Required accessible title | | `trigger` | `ReactElement>` | optional | Required accessible title | | `children` | `ReactNode` | required | Required accessible title | | `open` | `boolean` | conditional | Controlled or uncontrolled | | `defaultOpen` | `boolean` | optional | Controlled or uncontrolled | | `onOpenChange` | `((open: boolean) => void)` | conditional | Controlled or uncontrolled | | `footerConfig` | `OverlayFooterConfig` | optional | Shared action/focus/guard contracts | | `dismissConfig` | `OverlayDismissConfig` | optional | Shared action/focus/guard contracts | | `focusConfig` | `OverlayFocusConfig` | optional | Shared action/focus/guard contracts | | `className` | `string` | optional | Shared action/focus/guard contracts | ### dragConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `dismissible` | `boolean` | Yes | | ### footerConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `primaryAction` | `OverlayAction` | No | | | `secondaryAction` | `OverlayAction` | No | | | `leadingContent` | `ReactNode` | No | | | `cancelLabel` | `string` | No | | ### dismissConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `outsidePress` | `boolean` | No | | | `beforeClose` | `((reason: OverlayCloseReason) => boolean \| Promise)` | No | | | `onError` | `((error: unknown) => void)` | No | | ### focusConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `initialFocusRef` | `RefObject` | No | | | `returnFocusRef` | `RefObject` | No | | `open` with `onOpenChange` is controlled; otherwise use `defaultOpen`. Primary actions never imply success or automatically close the surface. Close it after the application completes its work. `footerConfig` accepts `primaryAction`, `secondaryAction`, `leadingContent`, and `cancelLabel`. Actions have `label`, `onClick`, native `type`/`form`, `disabled`, `disabledReason`, `loadingConfig`, `destructive`, and icons. Without a secondary action the footer renders Cancel. A footer is absent unless configured. Submit uses native form IDs; IDs must be unique. ## Bottom sheet [#bottom-sheet] ```tsx import { Button } from '@commenda/pathfinder/button'; import { Input } from '@commenda/pathfinder/input'; import { Sheet } from '@commenda/pathfinder/sheet'; export const SheetBottom = () => ( Filter filings} footerConfig={{}} >

Keep short filters in the centered content column.

); ``` The centered column is at most 640px; the sheet is at most 96dvh. Drag only the handle: a downward release over 0.4px/ms or distance over 25% of sheet height requests dismissal. Close, Escape, Cancel, and keyboard activation of the handle are equivalent alternatives. The body remains independently scrollable. ## Modal mode [#modal-mode] ```tsx import { Button } from '@commenda/pathfinder/button'; import { Input } from '@commenda/pathfinder/input'; import { Sheet } from '@commenda/pathfinder/sheet'; export const SheetModal = () => ( Open modal sheet} footerConfig={{}} > ); ``` Modal mode couples the backdrop, focus containment, and page scroll lock. Outside dismissal remains off unless `dismissConfig.outsidePress=true`. Nonmodal sheets always ignore page clicks. Right sheets have a 16px inset and clamp to the viewport. ## Unsaved changes [#unsaved-changes] ```tsx import { useRef, useState } from 'react'; import { Button } from '@commenda/pathfinder/button'; import { ConfirmDialog } from '@commenda/pathfinder/dialog'; import { Input } from '@commenda/pathfinder/input'; import { Sheet } from '@commenda/pathfinder/sheet'; export const SheetGuarded = () => { const [open, setOpen] = useState(false); const [confirmation, setConfirmation] = useState(false); const decision = useRef<((allow: boolean) => void) | null>(null); const finish = (allow: boolean) => { decision.current?.(allow); decision.current = null; setConfirmation(false); }; return ( Edit with discard protection } dismissConfig={{ beforeClose: () => new Promise((resolve) => { decision.current = resolve; setConfirmation(true); }), }} footerConfig={{}} > { if (!next) finish(false); }} title="Discard changes?" description="Your unsaved changes will be lost." cancelLabel="Keep editing" confirmAction={{ label: 'Discard changes', destructive: true, onClick: () => { finish(true); }, }} /> ); }; ``` `beforeClose` also receives `swipe`. The application can show a ConfirmDialog and resolve the pending decision. Keep a single Sheet open, with at most its confirmation above it. The application owns request cancellation and should resolve outstanding confirmations if it removes the sheet. Read the [overlay rules](/docs/components/sheet#design-rules). ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Sheet [#sheet] - **SHEET-001:** Right widths 400/520/720px, default 520, inset 16, viewport height minus 32. Bottom maximum 96dvh, pearl-100 surface, top radius12, centered640 column, handle48×6; safe-area bottom padding. - **SHEET-002:** Nonmodal by default, unlike Kakashi’s legacy default. Page remains usable and page clicks do not close it. Modal mode couples backdrop, focus containment, and scroll lock. Keep one Sheet, with its confirmation when necessary. - **SHEET-003:** Bottom handle only: distance over25% or downward release velocity over0.4px/ms, pointer capture, multi-pointer protection, cancel/reset. Body scroll does not drag. Guard swipe just like Close; keyboard, Escape, and Cancel alternatives remain available. Base UI Drawer1.9 uses a fixed0.5px/ms threshold and broader gesture handling, so the private gesture layer wraps Base UI Dialog rather than copying app logic. ### Motion and edge cases [#motion-and-edge-cases] Occasional overlays use motion for spatial consistency. Motion paths and timings follow Kakashi: Dialog fades and moves from 95% scale with a 2% vertical offset in 200ms both ways; Popover fades from 95% scale and an 8px offset toward its trigger in 150ms both ways. Popover direction follows the resolved placement after collision handling. Right Sheet slides in for 500ms and out for 300ms; bottom Sheet slides for 500ms both ways. Sheet panels do not fade during normal motion, and modal backdrops follow the panel duration. Pathfinder retains its foundation ease-out curve for Dialog/Popover and drawer curve `(0.32,0.72,0,1)` for Sheets. These are deliberate curve choices; this is not a copy of Kakashi’s legacy Radix keyframe implementation. Base UI continues to own presence, focus and dismissal. Transitions use a single `transform` value plus opacity where relevant, so Tailwind v4’s separate scale/translate properties cannot bypass interpolation. Closing or reopening mid-transition reverses from the current rendered position. Keyboard opening and dismissal remain immediate, including Close and Cancel activation. Reduced motion removes scale and translation: Dialog/Popover keep their fade, while Sheets change position immediately and their modal backdrop can fade. Drag writes only the popup transform, never layout or inherited animation variables. Programmatic openings animate by default; explicit keyboard interactions suppress motion. --- # Side nav Package: @commenda/pathfinder@0.1.0 Page: /docs/components/side-nav Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=714-2 ```tsx import { useState } from 'react'; import { Avatar } from '@commenda/pathfinder/avatar'; import { Dropdown } from '@commenda/pathfinder/dropdown'; import { BookOpen01Icon } from '@commenda/pathfinder/icons/BookOpen01Icon'; import { Building06Icon } from '@commenda/pathfinder/icons/Building06Icon'; import { CalendarIcon } from '@commenda/pathfinder/icons/CalendarIcon'; import { CheckDone01Icon } from '@commenda/pathfinder/icons/CheckDone01Icon'; import { CoinsStacked03Icon } from '@commenda/pathfinder/icons/CoinsStacked03Icon'; import { CoinsSwap01Icon } from '@commenda/pathfinder/icons/CoinsSwap01Icon'; import { CommendaBrandIcon } from '@commenda/pathfinder/icons/CommendaBrandIcon'; import { File06Icon } from '@commenda/pathfinder/icons/File06Icon'; import { FolderIcon } from '@commenda/pathfinder/icons/FolderIcon'; import { HelpCircleIcon } from '@commenda/pathfinder/icons/HelpCircleIcon'; import { HomeLineIcon } from '@commenda/pathfinder/icons/HomeLineIcon'; import { Inbox01Icon } from '@commenda/pathfinder/icons/Inbox01Icon'; import { PneumaIcon } from '@commenda/pathfinder/icons/PneumaIcon'; import { Settings01Icon } from '@commenda/pathfinder/icons/Settings01Icon'; import { ShoppingBag02Icon } from '@commenda/pathfinder/icons/ShoppingBag02Icon'; import { Users01Icon } from '@commenda/pathfinder/icons/Users01Icon'; import { CommendaLockupLogo } from '@commenda/pathfinder/logos/commenda/CommendaLockupLogo'; import { SideNav, type SideNavSection } from '@commenda/pathfinder/side-nav'; const workspaceSections: SideNavSection[] = [ { id: 'main', items: [ { kind: 'link', id: 'home', label: 'Home', icon: , linkConfig: { href: '#home' }, }, { kind: 'link', id: 'tasks', label: 'Tasks', icon: , count: 4, linkConfig: { href: '#tasks' }, }, { kind: 'link', id: 'inbox', label: 'Inbox', icon: , count: 3, unread: true, linkConfig: { href: '#inbox' }, }, ], }, { id: 'overview', label: 'Overview', collapsible: true, items: [ { kind: 'link', id: 'entities', label: 'Legal entities', icon: , linkConfig: { href: '#entities' }, }, { kind: 'link', id: 'people', label: 'Key persons', icon: , linkConfig: { href: '#people' }, }, { kind: 'link', id: 'calendar', label: 'Compliance calendar', icon: , linkConfig: { href: '#calendar' }, }, { kind: 'group', id: 'documents', label: 'Documents', icon: , linkConfig: { href: '#documents' }, children: [ { kind: 'link', id: 'formation', label: 'Formation', icon: , linkConfig: { href: '#formation' }, }, { kind: 'link', id: 'tax-docs', label: 'Tax', icon: , linkConfig: { href: '#tax-docs' }, }, ], }, ], }, { id: 'apps', label: 'My apps', collapsible: true, items: [ { kind: 'link', id: 'indirect-tax', label: 'Global indirect tax', icon: , linkConfig: { href: '#indirect-tax' }, }, { kind: 'link', id: 'reporting', label: 'Tax & reporting', icon: , linkConfig: { href: '#reporting' }, }, { kind: 'link', id: 'accounting', label: 'Accounting', icon: , linkConfig: { href: '#accounting' }, }, { kind: 'link', id: 'transfer', label: 'Transfer pricing', icon: , linkConfig: { href: '#transfer' }, }, { kind: 'link', id: 'marketplace', label: 'Marketplace', icon: , linkConfig: { href: '#marketplace' }, }, ], }, ]; const chatSections: SideNavSection[] = [ { id: 'chat-main', items: [ { kind: 'link', id: 'new-chat', label: 'New chat', icon: , linkConfig: { href: '#new-chat' }, }, { kind: 'link', id: 'agents', label: 'Agents', icon: , linkConfig: { href: '#agents' }, }, ], }, ]; const accountFooter = (collapsed: boolean) => (
{[ { label: 'Settings', href: '#settings', icon: , }, { label: 'Help & support', href: '#help', icon: , }, ].map((item) => ( {item.icon} {!collapsed && item.label} ))} {!collapsed && 'Sam Schulte'} } />
); export const SideNavDefault = () => { const [mode, setMode] = useState('home'); const [active, setActive] = useState('home'); return (
collapsed ? ( ) : ( ) } footer={accountFooter} modeConfig={{ label: 'Workspace mode', value: mode, onValueChange: setMode, options: [ { value: 'home', label: 'Home', leadingIcon: }, { value: 'chat', label: 'Chat', leadingIcon: }, ], }} renderLink={(props) => ( { event.preventDefault(); setActive(props.href?.slice(1) ?? null); }} > {props.children} )} />
); }; ``` Import `SideNav` from `@commenda/pathfinder/side-nav`. The application owns routes, active destination, permissions and persistence. No workspace providers, cookies or global shortcuts are required. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `label` | `string` | required | Required navigation landmark name | | `sections` | `readonly SideNavSection[]` | required | IDs, optional label/collapsible, recursive items | | `activeId` | `string \| null` | required | Current destination; receives aria-current=page | | `collapseConfig` | `SelectionProps` | optional | `{ value, onValueChange }` or `{ defaultValue?, onValueChange? }`; false initially | | `expansionConfig` | `SelectionProps` | optional | Section and group IDs; collapsible sections start open | | `header` | `ReactNode \| ((collapsed: boolean) => ReactNode)` | optional | Slots can adapt to the rail | | `footer` | `ReactNode \| ((collapsed: boolean) => ReactNode)` | optional | Slots can adapt to the rail | | `modeConfig` | `SegmentedControlProps` | optional | Optional view-mode switch; app supplies matching sections | | `renderLink` | `((props: React.ComponentPropsWithRef<"a">) => React.ReactElement)` | optional | Next Link integration; item renderer takes precedence | | `className` | `string` | optional | Root nav classes; parent supplies available height | ### collapseConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `value` | `boolean` | Yes | | | `onValueChange` | `(value: boolean) => void` | Yes | | Do not supply: `defaultValue`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `defaultValue` | `boolean` | No | | | `onValueChange` | `((value: boolean) => void)` | No | | Do not supply: `value`. ### expansionConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `value` | `readonly string[]` | Yes | | | `onValueChange` | `(value: readonly string[]) => void` | Yes | | Do not supply: `defaultValue`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `defaultValue` | `readonly string[]` | No | | | `onValueChange` | `((value: readonly string[]) => void)` | No | | Do not supply: `value`. An item has `id`, `label`, `icon`, optional `count`, `unread`, and `disableConfig: { isDisabled, reason? }`. Its `kind` distinguishes `link` with `linkConfig`, `action` with `onAction`, or `group` with recursive `children` and optional landing `linkConfig`. A landing link and its chevron are distinct targets. A group without a landing link expands on activation. Keep section and item IDs stable and globally distinct. `linkConfig` supports `href`, `target`, `rel`, and a ref-forwarding `render`. Renderers must preserve supplied accessibility props, handlers and refs. Actions and links are mutually exclusive. Counts and unread state stay accessible in the rail. ## Collapsed rail [#collapsed-rail] ```tsx import { Avatar } from '@commenda/pathfinder/avatar'; import { Dropdown } from '@commenda/pathfinder/dropdown'; import { BookOpen01Icon } from '@commenda/pathfinder/icons/BookOpen01Icon'; import { Building06Icon } from '@commenda/pathfinder/icons/Building06Icon'; import { CalendarIcon } from '@commenda/pathfinder/icons/CalendarIcon'; import { CheckDone01Icon } from '@commenda/pathfinder/icons/CheckDone01Icon'; import { CoinsStacked03Icon } from '@commenda/pathfinder/icons/CoinsStacked03Icon'; import { CoinsSwap01Icon } from '@commenda/pathfinder/icons/CoinsSwap01Icon'; import { CommendaBrandIcon } from '@commenda/pathfinder/icons/CommendaBrandIcon'; import { File06Icon } from '@commenda/pathfinder/icons/File06Icon'; import { FolderIcon } from '@commenda/pathfinder/icons/FolderIcon'; import { HelpCircleIcon } from '@commenda/pathfinder/icons/HelpCircleIcon'; import { HomeLineIcon } from '@commenda/pathfinder/icons/HomeLineIcon'; import { Inbox01Icon } from '@commenda/pathfinder/icons/Inbox01Icon'; import { Settings01Icon } from '@commenda/pathfinder/icons/Settings01Icon'; import { ShoppingBag02Icon } from '@commenda/pathfinder/icons/ShoppingBag02Icon'; import { Users01Icon } from '@commenda/pathfinder/icons/Users01Icon'; import { CommendaLockupLogo } from '@commenda/pathfinder/logos/commenda/CommendaLockupLogo'; import { SideNav, type SideNavSection } from '@commenda/pathfinder/side-nav'; const workspaceSections: SideNavSection[] = [ { id: 'main', items: [ { kind: 'link', id: 'home', label: 'Home', icon: , linkConfig: { href: '#home' }, }, { kind: 'link', id: 'tasks', label: 'Tasks', icon: , count: 4, linkConfig: { href: '#tasks' }, }, { kind: 'link', id: 'inbox', label: 'Inbox', icon: , count: 3, unread: true, linkConfig: { href: '#inbox' }, }, ], }, { id: 'overview', label: 'Overview', collapsible: true, items: [ { kind: 'link', id: 'entities', label: 'Legal entities', icon: , linkConfig: { href: '#entities' }, }, { kind: 'link', id: 'people', label: 'Key persons', icon: , linkConfig: { href: '#people' }, }, { kind: 'link', id: 'calendar', label: 'Compliance calendar', icon: , linkConfig: { href: '#calendar' }, }, { kind: 'group', id: 'documents', label: 'Documents', icon: , linkConfig: { href: '#documents' }, children: [ { kind: 'link', id: 'formation', label: 'Formation', icon: , linkConfig: { href: '#formation' }, }, { kind: 'link', id: 'tax-docs', label: 'Tax', icon: , linkConfig: { href: '#tax-docs' }, }, ], }, ], }, { id: 'apps', label: 'My apps', collapsible: true, items: [ { kind: 'link', id: 'indirect-tax', label: 'Global indirect tax', icon: , linkConfig: { href: '#indirect-tax' }, }, { kind: 'link', id: 'reporting', label: 'Tax & reporting', icon: , linkConfig: { href: '#reporting' }, }, { kind: 'link', id: 'accounting', label: 'Accounting', icon: , linkConfig: { href: '#accounting' }, }, { kind: 'link', id: 'transfer', label: 'Transfer pricing', icon: , linkConfig: { href: '#transfer' }, }, { kind: 'link', id: 'marketplace', label: 'Marketplace', icon: , linkConfig: { href: '#marketplace' }, }, ], }, ]; const accountFooter = (collapsed: boolean) => (
{[ { label: 'Settings', href: '#settings', icon: , }, { label: 'Help & support', href: '#help', icon: , }, ].map((item) => ( {item.icon} {!collapsed && item.label} ))} {!collapsed && 'Sam Schulte'} } />
); export const SideNavCollapsed = () => (
collapsed ? ( ) : ( ) } footer={accountFooter} />
); ``` Expanded width is 208px; the rail is 56px. Rows are 28px, icons 16px. In the rail, group activation expands navigation to reveal its children; groups with a destination preserve that link, and the Expand sidebar control reveals their descendants. Header/footer render functions prevent a long organization name from overflowing a rail. ## Controlled state [#controlled-state] ```tsx import { useState } from 'react'; import { BookOpen01Icon } from '@commenda/pathfinder/icons/BookOpen01Icon'; import { Building06Icon } from '@commenda/pathfinder/icons/Building06Icon'; import { CalendarIcon } from '@commenda/pathfinder/icons/CalendarIcon'; import { CheckDone01Icon } from '@commenda/pathfinder/icons/CheckDone01Icon'; import { CoinsStacked03Icon } from '@commenda/pathfinder/icons/CoinsStacked03Icon'; import { CoinsSwap01Icon } from '@commenda/pathfinder/icons/CoinsSwap01Icon'; import { File06Icon } from '@commenda/pathfinder/icons/File06Icon'; import { FolderIcon } from '@commenda/pathfinder/icons/FolderIcon'; import { HomeLineIcon } from '@commenda/pathfinder/icons/HomeLineIcon'; import { Inbox01Icon } from '@commenda/pathfinder/icons/Inbox01Icon'; import { ShoppingBag02Icon } from '@commenda/pathfinder/icons/ShoppingBag02Icon'; import { Users01Icon } from '@commenda/pathfinder/icons/Users01Icon'; import { SideNav, type SideNavSection } from '@commenda/pathfinder/side-nav'; const workspaceSections: SideNavSection[] = [ { id: 'main', items: [ { kind: 'link', id: 'home', label: 'Home', icon: , linkConfig: { href: '#home' }, }, { kind: 'link', id: 'tasks', label: 'Tasks', icon: , count: 4, linkConfig: { href: '#tasks' }, }, { kind: 'link', id: 'inbox', label: 'Inbox', icon: , count: 3, unread: true, linkConfig: { href: '#inbox' }, }, ], }, { id: 'overview', label: 'Overview', collapsible: true, items: [ { kind: 'link', id: 'entities', label: 'Legal entities', icon: , linkConfig: { href: '#entities' }, }, { kind: 'link', id: 'people', label: 'Key persons', icon: , linkConfig: { href: '#people' }, }, { kind: 'link', id: 'calendar', label: 'Compliance calendar', icon: , linkConfig: { href: '#calendar' }, }, { kind: 'group', id: 'documents', label: 'Documents', icon: , linkConfig: { href: '#documents' }, children: [ { kind: 'link', id: 'formation', label: 'Formation', icon: , linkConfig: { href: '#formation' }, }, { kind: 'link', id: 'tax-docs', label: 'Tax', icon: , linkConfig: { href: '#tax-docs' }, }, ], }, ], }, { id: 'apps', label: 'My apps', collapsible: true, items: [ { kind: 'link', id: 'indirect-tax', label: 'Global indirect tax', icon: , linkConfig: { href: '#indirect-tax' }, }, { kind: 'link', id: 'reporting', label: 'Tax & reporting', icon: , linkConfig: { href: '#reporting' }, }, { kind: 'link', id: 'accounting', label: 'Accounting', icon: , linkConfig: { href: '#accounting' }, }, { kind: 'link', id: 'transfer', label: 'Transfer pricing', icon: , linkConfig: { href: '#transfer' }, }, { kind: 'link', id: 'marketplace', label: 'Marketplace', icon: , linkConfig: { href: '#marketplace' }, }, ], }, ]; export const SideNavControlled = () => { const [collapsed, setCollapsed] = useState(false); const [expanded, setExpanded] = useState([ 'overview', 'documents', ]); return (

{collapsed ? 'Rail' : 'Expanded'} · {expanded.length} expanded groups

); }; ``` Expansion and collapse are independent of the active route. The app may persist them per user; Pathfinder does not read browser storage. Overflow fades follow actual scroll position. Use an app-owned mobile navigation surface when the persistent sidebar does not fit; the component does not guess viewport breakpoints or mutate routes. Chat conversation history remains a separately composed pattern. Read the [hierarchy rules](/docs/components/side-nav#design-rules). ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Side navigation [#side-navigation] - **NAV-001:** Expanded 208px, rail 56px, pearl100 background, 12px inset, 24px section gaps. Header mark/wordmark and collapse control; optional Home/Chat SegmentedControl; footer account composition. Rows 28px, radius 6, horizontal inset 8, gap 12, icons 16. - **NAV-002:** Label/13 pearl900; icon pearl800. Hover pearl200, selected pearl300, selected label1000 and icon blue700. The landing link and expansion chevron share one continuous hover/selected row surface. Weight never changes. The docs prose mentions selected 200, but the reviewed component 714:15 uses 300; the implementation follows the component. Count Label/12, optional unread dot; rail uses tooltip and accessible metadata. - **NAV-003:** Stable globally distinct IDs, one active destination, real anchors, separate group chevrons. Routing, auth, persistence and fetching belong to the application. Collapse retains group/section state. Chat history is a separate composed pattern; do not embed an application’s routes or conversation API in SideNav. - **NAV-004:** Scroll fades match CommendaNavbar: 48px from the Soft Pearl 100 surface (#FCFBF8) to transparent, appearing only at overflowing edges. The parent supplies available height. No global keyboard shortcut, cookie or workspace provider is installed by the component. --- # Skeleton Package: @commenda/pathfinder@0.1.0 Page: /docs/components/skeleton Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=262-2 ```tsx import { SkeletonGroup, SkeletonText } from '@commenda/pathfinder/skeleton'; ; ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` Import `@commenda/pathfinder/styles.css` once after host CSS. Styles are precompiled with Tailwind v4; consumers need no Tailwind scanning. Interactive callbacks belong in client components in Next.js App Router. ## Usage [#usage] ```tsx import { SkeletonGroup, SkeletonText } from '@commenda/pathfinder/skeleton'; ; ``` ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `shape` | `"circle" \| "text" \| "block"` | optional | text | | `width` | `Property.Width` | optional | Match incoming content | | `height` | `Property.Height` | optional | Match incoming content | | `animate` | `boolean` | optional | true; reduced motion is static | | `className` | `string` | optional | Additional classes for the component root. | The application owns the 300 ms delay to avoid flashes. After about 10 seconds, offer meaningful progress or recovery; never fabricate a percentage. Render stable headers, filters and buttons immediately. Match the content layout and show 5–8 list rows. Group placeholders into one loading region; each shape is decorative. ## Examples [#examples] ### Shapes [#shapes] ```tsx ; ``` ### List [#list] ```tsx {Array.from({ length: 5 }, (_, index) => ( ))} ; ``` ### Card [#card] ```tsx ; ``` ### Static [#static] ```tsx ; ``` Read the [design guidance](/docs/components/skeleton#design-rules) before using these components. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Skeleton — 262:2 [#skeleton--2622] - **SKELETON-01:** Match the arriving layout, including row height, columns, padding and card frames. Skeleton content, never stable headers, filters, tabs or buttons. Do not skeleton menus, toasts or dialog shells; loading content inside a dialog may use it. - **SKELETON-02:** One named loading region with decorative shapes. Show 5–8 list rows. Use one synchronized 1.5-second linear shimmer phase for shapes mounted together; reduced motion stays still. - **SKELETON-03:** Applications delay presentation by 300 ms to avoid flashes. After about 10 seconds, provide progress or useful recovery. Never invent a known percentage; unknown task length uses indeterminate Progress. Single controls use their own loading indicator. - **SKELETON-04:** Pearl 200; text height 10/radius 4, block radius 6, circle full. Text preset widths 100/92/64%, gap 8. List row: 32 px avatar, 10 px name, 8 px metadata, gap 8, trailing value width 48; padding 12/16 and gap 12. Card: white, 0.5 px border, radius 12, padding/gaps 16, header icon 24, two text lines, optional media height 120, two footer placeholders. --- # Split button Package: @commenda/pathfinder@0.1.0 Page: /docs/components/split-button Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=51-2 ```tsx import { SplitButton } from '@commenda/pathfinder/split-button'; ; ``` ## Usage [#usage] ```tsx import { SplitButton } from '@commenda/pathfinder/split-button'; ; ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` The package currently lives in the Commenda workspace. External installation requires a published version or packed tarball. Import the precompiled stylesheet once; no application Tailwind setup is required. ## Examples [#examples] ### Secondary [#secondary] ```tsx ; ``` ### Loading the main action [#loading-the-main-action] ```tsx ; ``` ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `className` | `string` | optional | Additional classes for the component root. | | `size` | `"sm" \| "md" \| "lg"` | optional | | | `loadingConfig` | `LoadingConfig` | optional | | | `leadingIcon` | `ReactNode` | optional | | | `disabledReason` | `string` | optional | | | `open` | `boolean` | conditional | | | `onOpenChange` | `((open: boolean) => void)` | conditional | | | `defaultOpen` | `boolean` | optional | | | `label` | `string` | required | Visible field label. Supply an accessible name when it is omitted. | | `variant` | `"primary" \| "secondary"` | optional | | | `completionConfig` | `CompletionConfig` | optional | | | `menuConfig` | `SplitButtonMenuConfig` | required | | ### loadingConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isLoading` | `boolean` | Yes | | | `label` | `string` | No | | ### completionConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isComplete` | `boolean` | Yes | | | `label` | `string` | Yes | | | `icon` | `ReactNode` | No | | ### menuConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `groups` | `DropdownGroup[]` | Yes | | | `label` | `string` | No | | | `disabled` | `boolean` | No | | | `disabledReason` | `string` | No | | | `side` | `Side` | No | | | `align` | `Align` | No | | | `sideOffset` | `number` | No | | | `portalContainer` | `HTMLElement \| ShadowRoot \| React.RefObject \| null` | No | | `SplitButtonMenuConfig`: required `groups: DropdownGroup[]`; optional `label`, `disabled`, `disabledReason`, `side`, `align`, `sideOffset`, `portalContainer`. Label defaults to `More options for ${label}`. Menu configuration disables only alternatives; component disabled disables both halves. There are no public Root/Trigger/Content parts or top-level menuLabel/menuDisabled props. `SPLIT-ACTION-01`: Use one obvious default with two to five related alternatives. The main half acts immediately; the chevron opens the menu. Each is a separate keyboard stop with independent hover/focus. Loading blocks only the main action; disable the menu explicitly when alternatives conflict with ongoing work. Escape closes the menu and restores trigger focus. The menu trigger never submits a form. No tertiary or destructive default. Destructive alternatives belong last in the menu and generally require confirmation. Without a default action use Dropdown with a normal Button. Do not put two SplitButtons beside each other in a row. Inner corners are square; outer corners follow Button radius. Recursive menus use the same model as Dropdown. Import `@commenda/pathfinder/styles.css` once at the app root. Event handlers require a client component in Next.js App Router. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Action rules [#action-rules] - `CONTROL-STATE-01` — Pathfinder: loadingConfig and completionConfig are explicit application signals. Loading wins over completion; disabled blocks activation. Ending loading never implies success. Applications own the 1.5-second completion reset recipe from Figma. - `CONTROL-SIZE-01` — Figma: Button and SplitButton sm/md/lg have 28/32/36px visual heights; md default. IconButton adds xs 24. Button icons are 16 through md and 20 at lg; pass size to generated icons to select optical artwork. Link styles have text-sized geometry. Long content can increase minimum height for accessibility. - `CONTROL-FOCUS-01` — Figma: show focus from keyboard, not pointer click. Button family uses a blue ring separated by a white gap. Open triggers retain hover styling. Disabled reason is a Pathfinder extension: retain action name, expose description, and allow focus without activation where the native wrapper supports it. - `SPLIT-ACTION-01` — Figma: one obvious default and two to five related alternatives; no tertiary/destructive default. Each half reacts independently. Main-only loading; trigger-only open styling. No default means a normal dropdown trigger. Avoid two SplitButtons side by side. Square inner corners, 1px join, rounded outer corners. --- # Stepper Package: @commenda/pathfinder@0.1.0 Page: /docs/components/stepper Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=676-2 ```tsx import { Stepper, type StepperItem } from '@commenda/pathfinder/stepper'; const filingSteps: StepperItem[] = [ { id: 'compose', label: 'Compose', description: 'Write the title and message', state: 'done', }, { id: 'audience', label: 'Audience', description: 'Who sees this alert', state: 'current', }, { id: 'schedule', label: 'Schedule', description: 'When it goes live', state: 'upcoming', }, ]; export const StepperDefault = () => ( ); ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` Import `@commenda/pathfinder/styles.css` once after host CSS. Pathfinder ships compiled, prefixed Tailwind v4 CSS; the application needs no Tailwind scanning. Import from `@commenda/pathfinder/stepper`. Interactive callbacks belong in a client component in Next.js App Router. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `label` | `string` | required | Required; ordered-list name | | `items` | `readonly StepperItem[]` | required | Required; id, label, state, optional description | | `orientation` | `"vertical" \| "horizontal"` | optional | horizontal | | `navigationConfig` | `{ onStepChange: (id: string) => void; }` | optional | Optional; only done steps request navigation | | `stateLabels` | `Partial>` | optional | Localized Upcoming, Current step, Completed, Unavailable | | `id` | `string` | optional | Stable HTML identifier for the component. | | `className` | `string` | optional | Additional classes for the component root. | ### navigationConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `onStepChange` | `(id: string) => void` | Yes | | Root IDs, className, appropriate ARIA attributes, and typed refs are supported. Item IDs/values must be unique and stable. ## Usage rules [#usage-rules] Each step explicitly declares `state: upcoming | current | done | disabled`. At most one is current; a finished flow can have none. The component does not duplicate that state with a separate current index. Done steps become native buttons when navigationConfig is supplied. Upcoming and disabled steps do not navigate. The application owns validation, retained answers, Back/Continue/Submit actions, and focus after navigation. A failed field remains a field error; it does not create a red step marker. Keep later completed steps done when revisiting earlier answers unless those answers invalidate them. Prefer three to six steps on a full page. Use the vertical layout when a narrow container cannot fit clear horizontal labels. The page owns sticky positioning. The Navigation example illustrates navigation only, not a production validation engine. ## Examples [#examples] ### Vertical [#vertical] ```tsx import { Stepper, type StepperItem } from '@commenda/pathfinder/stepper'; const filingSteps: StepperItem[] = [ { id: 'compose', label: 'Compose', description: 'Write the title and message', state: 'done', }, { id: 'audience', label: 'Audience', description: 'Who sees this alert', state: 'current', }, { id: 'schedule', label: 'Schedule', description: 'When it goes live', state: 'upcoming', }, ]; export const StepperVertical = () => ( ); ``` ### Navigation [#navigation] ```tsx import { useState } from 'react'; import { Button } from '@commenda/pathfinder/button'; import { Stepper, type StepperItem } from '@commenda/pathfinder/stepper'; const filingSteps: StepperItem[] = [ { id: 'compose', label: 'Compose', description: 'Write the title and message', state: 'done', }, { id: 'audience', label: 'Audience', description: 'Who sees this alert', state: 'current', }, { id: 'schedule', label: 'Schedule', description: 'When it goes live', state: 'upcoming', }, ]; export const StepperNavigation = () => { const [current, setCurrent] = useState(0); const [completed, setCompleted] = useState([]); return (
({ ...step, state: index === current ? 'current' : completed.includes(step.id) ? 'done' : 'upcoming', }))} navigationConfig={{ onStepChange: (id) => { setCurrent(filingSteps.findIndex((step) => step.id === id)); }, }} />
); }; ``` ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Stepper [#stepper] Source: [676:104](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=676-104), items 676:61, behavior 676:35169, guidance 676:35190. - `STEPPER-01` — Source/Pathfinder: explicit upcoming/current/done/disabled states, at most one current. Only done steps with navigationConfig become buttons; callbacks request navigation without changing caller-owned state. Current exposes aria-current=step. - `STEPPER-02` — Source: current-step validation, saved answers, footer buttons, and focus belong to the application. Field errors never invent a red marker. Final primary action says Publish, Submit, or Create. Keep completed steps when revisiting unless the application invalidates them. - `STEPPER-03` — Source: three to six steps on full pages. Short labels, optional descriptions. Parent owns sticky positioning and responsive choice of horizontal/vertical layout. - `STEPPER-04` — Source: 20 markers, upcoming pearl-300/900, current blue-700/white, done green-700/check, disabled pearl-100/600. Horizontal text gap 8, connector at marker center with 12 side gaps. Vertical 240, padding 8, gap 12, row gap 2, radius 8. Weight never shifts; completion enters without bounce and never animates on initial mount. --- # Suggested prompt group Package: @commenda/pathfinder@0.1.0 Page: /docs/components/suggested-prompt-group Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=1058-3 ```tsx import { SuggestedPromptGroup } from '@commenda/pathfinder/suggested-prompt-group'; { setDraft(prompt); editorRef.current?.focus(); }} />; ``` ## Usage [#usage] ```tsx import { SuggestedPromptGroup } from '@commenda/pathfinder/suggested-prompt-group'; { setDraft(prompt); recordPromptSelection(item.id); }} />; ``` ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `items` | `readonly SuggestedPromptItem[]` | required | Stable unique IDs, labels, optional prompt text and disabled state | | `onSelect` | `((prompt: string, item: SuggestedPromptItem) => void)` | required | Text plus original item for analytics or host behavior | | `label` | `string` | optional | Accessible group label; defaults to `Suggested prompts` | | `disabled` | `boolean` | optional | Disables every prompt | | `className` | `string` | optional | Host layout | ## Design rules [#design-rules] - **PROMPT-GROUP-01:** Recommend three or four relevant prompts with an 8px wrapping gap. Never force a horizontally overflowing row. - Empty input renders nothing. Existing item order is preserved; no selection, randomization, fetching, or sending happens internally. - The group uses native button keyboard behavior, not radio or tab semantics. Each prompt remains independently reachable. For AI Elements migration, map each existing Suggestion's text and click callback to a SuggestedPrompt item and `onSelect`. Keep request handling and analytics in the application. --- # Suggested prompt Package: @commenda/pathfinder@0.1.0 Page: /docs/components/suggested-prompt Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=1058-3 ```tsx import { SuggestedPrompt } from '@commenda/pathfinder/suggested-prompt'; ; ``` ## Usage [#usage] ```tsx import { SuggestedPrompt } from '@commenda/pathfinder/suggested-prompt'; ; ``` `onSelect` receives `prompt`, or `label` when no separate prompt is provided. The application decides whether selection fills the Composer or sends a first message. The example inserts text and focuses the editor; it does not send without review. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `label` | `string` | required | Required visible and accessible text | | `prompt` | `string` | optional | Optional full text; defaults to label | | `onSelect` | `((prompt: string) => void)` | required | Required selection callback | ## Design rules [#design-rules] - **PROMPT-01:** Figma 1058:8: Copy13, one line, full radius, 12px horizontal/6px vertical inset, pearl200 fill and pearl300 hover/border. The hairline makes the exported frame 33px high around 32px content. - **PROMPT-02:** Selection emits text; the library does not own requests or mutate a draft. - **PROMPT-03:** Long labels truncate within their container while preserving the full accessible name and title. Prefer short, concrete tasks. This uses native button semantics with its own prompt recipe; it is not a Button variant. Enter and Space activate it, and focus remains visible. For several starters use [Suggested prompt group](/docs/components/suggested-prompt-group). --- # Switch Package: @commenda/pathfinder@0.1.0 Page: /docs/components/switch Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=199-4 ```tsx import { Switch } from '@commenda/pathfinder/switch'; ; ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` Import `@commenda/pathfinder/styles.css` once at the app root. The stylesheet is precompiled with Tailwind v4; your application does not need Tailwind or source scanning. Event handlers belong in a client component in Next.js App Router. ## Usage [#usage] ```tsx import { Switch } from '@commenda/pathfinder/switch'; ; ``` | Prop | Type | Default / behavior | | ---------------------- | ---------------------------- | ------------------------------------------------------------ | | `label` | `string` | Required stable setting name | | `hideLabel` | `boolean` | `false`; accessible name retained | | `description` | `string` | Optional supporting explanation | | `checked` | `boolean` | Controlled state | | `defaultChecked` | `boolean` | `false`; uncontrolled initial state | | `onCheckedChange` | `(checked: boolean) => void` | Next state; application owns persistence | | `disabled` | `boolean` | Disables interaction for the control or group. | | `readOnly` | `boolean` | Keeps the current value visible while preventing changes. | | `required` | `boolean` | Requires a value for native form validation. | | `disabledReason` | `string` | Visible, associated explanation while disabled | | `name` | `string` | Name used when the control submits its value with a form. | | `value` | `string` | Value submitted when checked; defaults to 'on'. | | `uncheckedValue` | `string` | Value submitted when unchecked; omitted when not configured. | | `form` | `string` | ID of the form associated with the control. | | `id` | `string` | ID of the underlying form input. | | `inputRef` | `Ref` | Access to the underlying form input. | | `className` | `string` | Additional class names for the outer layout. | | `ref` | `Ref` | Access to the interactive root element. | | Native events and ARIA | Base UI root props | Typed passthrough | A switch applies immediately. Keep its label the same in both states, such as “Weekly summary”, rather than “Turn on weekly summary”. On a failed asynchronous save, the application reverts the controlled state and explains the failure; this component does not invent persistence or loading/completion states. For form-save behavior use Checkbox, and for non-boolean choices use RadioGroup or SegmentedControl. Track 36×20px, thumb 16px inset 2px, control elevation and 200ms ease-out movement. Honor reduced motion and RTL; coarse-pointer layouts provide at least a 44px row target without enlarging the visual control. ## Examples [#examples] ### Off and on [#off-and-on] ```tsx <> ; ``` ### Description [#description] ```tsx ; ``` ### Disabled states [#disabled-states] ```tsx <> ; ``` Read the [Figma choice-control rules](/docs/components/switch#design-rules) for design guidance, source discrepancies and accessibility requirements. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Switch [#switch] Source: [199:4](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=199-4), variants 200:5083, anatomy 207:148, guidance 207:184. - **SWITCH-01** — Apply immediately with no separate Save action. Use Checkbox for changes saved later. Never use a switch to choose between two non-boolean options. - **SWITCH-02** — The label names the setting and remains unchanged between states. Show results immediately; if persistence fails, the application reverts the controlled state and explains why. No invented loading/completion status is part of this API. - **SWITCH-03** — Track 36×20px, radius full. Thumb 16px with 2px inset and Elevation/control. Off soft-pearl-400, on blue-700; hover soft-pearl-500/blue-800. Move the thumb 200ms ease-out; turn movement off for reduced motion and reverse travel in RTL. Label starts 8px from track, with 2px top alignment. - **SWITCH-04** — Disabled off uses soft-pearl-100 track with soft-pearl-300 edge; disabled on uses soft-pearl-300 track. Thumb uses soft-pearl-200 without shadow. Keep the label readable with soft-pearl-600. ### Implementation decisions and source differences [#implementation-decisions-and-source-differences] The complete prop tables are on [Checkbox](/docs/components/checkbox), [RadioGroup](/docs/components/radio-group) and [Switch](/docs/components/switch). Source rule IDs are also kept in the component implementation comments and exported through `llms.txt`, `llms-full.txt` and the Markdown routes. - **CHOICE-01** — The sheets describe 8px list gaps but some documentation frames place rows 32px apart. Use the documented 8px component-list gap; documentation specimen whitespace is not a new size token. - **CHOICE-02** — Built-in text is plain, wrapping content. Do not place links, buttons or nested interactive controls inside labels or cards. Disabled reasons are visible descriptions, not tooltip-only explanations on controls removed from keyboard navigation. - **CHOICE-03** — Visual control size is not the hit target. Coarse-pointer rows provide at least 44px height, while preserving 16px marks and the 36×20px switch track. Cards already exceed this height. - **CHOICE-04** — Hover/focus are real browser states, not public cosmetic props. Retain native `name`, `value`, `form`, `required`, `readOnly`, disabled semantics and input refs. `hideLabel` preserves the required accessible label. - **CHOICE-05** — Use Pathfinder's existing Check/Minus optical artwork at 12px. Boxes, circles, tracks and thumbs are interactive primitive geometry, styled with the Figma tokens; they are not flattened screenshots. This keeps focus, RTL and switch movement functional. ### Explicit differences and boundaries [#explicit-differences-and-boundaries] | Reference | Coverage | | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Editing 442:767](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=442-767) | Account mapping demonstrates select, numeric validation, outline toggle, failed-save recovery and Add line. The companion example composes date, Switch and attachment selection. | --- # Table Package: @commenda/pathfinder@0.1.0 Page: /docs/components/table Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=428-2 ```tsx import { Table } from '@commenda/pathfinder/table'; export const TableDefault = () => ( row.id, getLabel: (row) => row.id }} columns={[ { id: 'invoice', header: 'Invoice', value: (row) => row.id }, { id: 'customer', header: 'Customer', width: 240, value: (row) => row.customer, }, { id: 'amount', header: 'Amount (USD)', align: 'end', value: (row) => row.amount, }, ]} /> ); ``` Use Table for records people find, open and act on. Start with `rows`, typed `columns`, and stable row identities, then add the features your screen needs. Use [DataGrid](/docs/components/data-grid) for financial matrices with grouped headers and keyboard cell navigation. ## Headers and alignment [#headers-and-alignment] ```tsx import { Table, TableNumberCell } from '@commenda/pathfinder/table'; const records = [ { id: 'INV-001', customer: 'Meridian GmbH', entity: 'Germany', owner: 'Lucas Moreau', status: 'Paid', amount: 4800, }, { id: 'INV-002', customer: 'Meridian US Inc.', entity: 'United States', owner: 'Priya Shah', status: 'Open', amount: 2150, }, { id: 'INV-003', customer: 'Meridian UK Ltd.', entity: 'United Kingdom', owner: 'Aisha Khan', status: 'Open', amount: 3200, }, ]; export const TableLayoutHeaders = () => (
row.id, getLabel: (row) => row.id }} selectionConfig={{}} sortingConfig={{ defaultValue: { columnId: 'amount', direction: 'asc' } }} columns={[ { id: 'id', header: 'Invoice', value: (row) => row.id, sortable: true }, { id: 'status', header: 'Status', align: 'center', value: (row) => row.status, sortable: true, }, { id: 'amount', header: 'Amount', description: 'Invoice total in USD, including tax.', align: 'end', value: (row) => row.amount, sortable: true, cell: (row) => , }, ]} /> ); ``` Headers are 36px high with 12px medium labels and 12px icons. Hover or focus an unsorted header to reveal the neutral sort indicator; the active column keeps its direction visible. Click to cycle sorting. End-aligned numeric columns place sorting before the label. Supply `description` for a separate, keyboard-accessible information tooltip. Selection headers use the same horizontal inset as row checkboxes. ## Usage [#usage] ```tsx 'use client'; import { Table } from '@commenda/pathfinder/table'; const invoices = [ { id: 'inv-001', number: 'INV-001', customer: 'Meridian UK Ltd' }, { id: 'inv-002', number: 'INV-002', customer: 'Meridian US Inc.' }, ]; export default function InvoicesTable() { return (
invoice.id }} columns={[ { id: 'number', header: 'Invoice', value: (invoice) => invoice.number }, { id: 'customer', header: 'Customer', value: (invoice) => invoice.customer, }, ]} /> ); } ``` Import `@commenda/pathfinder/styles.css` once in your app layout. Pathfinder ships its own compiled styles. Open **Code** on any example for the complete, formatted implementation; the snippet comes from the same source as its preview. Columns define a stable `id`, a visible `header`, and a `value(row)` accessor. Add `cell(row)` when rendering richer content. Keep the accessor when sorting or searching should use the underlying value rather than its formatted label. `rowConfig.getId` must return a stable record ID, not an array index. ## Sorting [#sorting] ```tsx import { useState } from 'react'; import { Table, TableNumberCell, type TableSort, } from '@commenda/pathfinder/table'; const invoices = [ { id: 'INV-001', customer: 'Meridian GmbH', status: 'Paid', amount: 2400 }, { id: 'INV-002', customer: 'Meridian US Inc.', status: 'Pending', amount: 1800, }, { id: 'INV-003', customer: 'Meridian UK Ltd.', status: 'Overdue', amount: 950, }, { id: 'INV-004', customer: 'Meridian Singapore', status: 'Pending', amount: 3200, }, ]; export const TableInteractionsDefault = () => { const [sort, setSort] = useState({ columnId: 'amount', direction: 'desc', }); return (
row.id, getLabel: (row) => row.id }} columns={[ { id: 'invoice', header: 'Invoice', value: (row) => row.id, sortable: true, }, { id: 'customer', header: 'Customer', width: 240, value: (row) => row.customer, sortable: true, }, { id: 'amount', header: 'Amount', align: 'end', value: (row) => row.amount, sortable: true, cell: (row) => ( ), }, ]} sortingConfig={{ value: sort, onValueChange: setSort }} />

{sort ? `Sorted by ${sort.columnId}, ${sort.direction === 'asc' ? 'ascending' : 'descending'}` : 'Original order'}

); }; ``` Click Amount to cycle descending, unsorted and ascending order. The initial sort is controlled by `sortingConfig`; the status below the example reflects the current value. Numeric accessors sort numerically even when the cell formats money. Local sorting runs before pagination. ## Search and filters [#search-and-filters] ```tsx import { useState } from 'react'; import { Table, TableCell } from '@commenda/pathfinder/table'; const invoices = [ { id: 'INV-001', customer: 'Meridian GmbH', status: 'Paid', amount: 2400 }, { id: 'INV-002', customer: 'Meridian US Inc.', status: 'Pending', amount: 1800, }, { id: 'INV-003', customer: 'Meridian UK Ltd.', status: 'Overdue', amount: 950, }, { id: 'INV-004', customer: 'Meridian Singapore', status: 'Pending', amount: 3200, }, ]; export const TableInteractionsFiltering = () => { const [query, setQuery] = useState(''); const [filters, setFilters] = useState>({}); return (
row.id, getLabel: (row) => row.id }} columns={[ { id: 'invoice', header: 'Invoice', value: (row) => row.id }, { id: 'customer', header: 'Customer', width: 240, value: (row) => row.customer, }, { id: 'status', header: 'Status', align: 'end', value: (row) => row.status, cell: (row) => ( ), }, ]} searchConfig={{ value: query, onValueChange: setQuery, getText: (row) => `${row.id} ${row.customer}`, }} filterConfig={{ value: filters, onValueChange: setFilters, filters: [ { id: 'status', label: 'Status', options: ['Paid', 'Pending', 'Overdue'].map((value) => ({ value, label: value, })), matches: (row, values) => values.includes(row.status), }, ], }} /> ); }; ``` Open search and enter **Meridian UK**, or choose **Pending** from Status. Search and filters combine. Search opens in place; tools stay together and excess filter buttons move into +N. A query with no matches offers Clear filters. This example supplies `getText` explicitly so invoice numbers and customer names are searchable. For a local collection, `matches(row, values)` defines filter membership. In remote mode the server applies it; use controlled search/filter state as request inputs. Reset the page when an externally controlled filter changes. ## Single-choice filters [#single-choice-filters] ```tsx import { useState } from 'react'; import { Table } from '@commenda/pathfinder/table'; export const TableFiltersSingle = () => { const [value, setValue] = useState>({}); return (
row.id }, { id: 'status', header: 'Status', value: (row) => row.status }, ]} rowConfig={{ getId: (row) => row.id, getLabel: (row) => row.id }} filterConfig={{ value, onValueChange: setValue, filters: [ { id: 'status', label: 'Status', type: 'single', options: ['Paid', 'Open', 'Overdue'].map((label) => ({ value: label, label, })), getValue: (row) => row.status, }, ], }} /> ); }; ``` Choose a status. Single choice replaces the previous selection, shows a checkmark and closes its menu. Clear resets that field. The selected value becomes an editable chip below the toolbar. ## Complex filters [#complex-filters] ```tsx import { useState } from 'react'; import { DEFlag } from '@commenda/pathfinder/flags/DEFlag'; import { USFlag } from '@commenda/pathfinder/flags/USFlag'; import { Building07Icon } from '@commenda/pathfinder/icons/Building07Icon'; import { CalendarIcon } from '@commenda/pathfinder/icons/CalendarIcon'; import { RefreshCw05Icon } from '@commenda/pathfinder/icons/RefreshCw05Icon'; import { Table, TableCell, type TableFilterOperator, TableNumberCell, } from '@commenda/pathfinder/table'; export const TableFiltersDefault = () => { const [value, setValue] = useState>({ status: ['Overdue', 'Due soon'], }); const [operators, setOperators] = useState< Readonly> >({}); const [notice, setNotice] = useState(''); const rows = [ { id: '1', filing: 'VAT return Q3', entity: 'DE', status: 'Filed', due: '2026-10-07', amount: 0, owner: 'Lucas Moreau', }, { id: '2', filing: 'Trade tax return', entity: 'DE', status: 'In review', due: '2026-10-30', amount: 180, owner: 'Priya Shah', }, { id: '3', filing: 'Annual accounts', entity: 'DE', status: 'Due soon', due: '2026-11-14', amount: 95, owner: 'Aisha Khan', }, { id: '4', filing: 'Delaware annual report', entity: 'US', status: 'Overdue', due: '2026-10-01', amount: 450, owner: 'Jun Tan', }, { id: '5', filing: 'BOI report update', entity: 'US', status: 'Due soon', due: '2026-11-14', amount: 0, owner: 'Priya Shah', }, ]; return (
row.id, getLabel: (row) => row.filing }} searchConfig={{ getText: (row) => `${row.filing} ${row.owner}`, placeholder: 'Search filings', }} columns={[ { id: 'filing', header: 'Filing', width: 240, value: (row) => row.filing, }, { id: 'entity', header: 'Entity', width: 180, cell: (row) => ( ) : ( ) } /> ), }, { id: 'status', header: 'Status', width: 130, cell: (row) => ( ), }, { id: 'due', header: 'Due', width: 140, value: (row) => row.due }, { id: 'amount', header: 'Amount', width: 140, align: 'end', cell: (row) => , }, ]} filterConfig={{ value, onValueChange: setValue, operators: { value: operators, onValueChange: setOperators }, filters: [ { id: 'status', label: 'Status', icon: , options: ['Overdue', 'Due soon', 'In review', 'Filed'].map( (label) => ({ value: label, label }) ), getValue: (row) => row.status, }, { id: 'entity', label: 'Entity', icon: , searchable: true, options: [ { value: 'DE', label: 'Acme GmbH', leading: , }, { value: 'US', label: 'Acme Inc.', leading: , }, ], getValue: (row) => row.entity, }, { id: 'due', label: 'Due date', type: 'date', icon: , getValue: (row) => row.due, presets: [ { id: 'october', label: 'October 2026', start: '2026-10-01', end: '2026-10-31', }, { id: 'quarter', label: 'Q4 2026', start: '2026-10-01', end: '2026-12-31', }, ], }, { id: 'owner', label: 'Assignee', type: 'single', initiallyVisible: false, options: [ 'Lucas Moreau', 'Priya Shah', 'Aisha Khan', 'Jun Tan', ].map((label) => ({ value: label, label })), getValue: (row) => row.owner, }, { id: 'amount', label: 'Amount', type: 'number', initiallyVisible: false, getValue: (row) => row.amount, }, ], }} toolbarConfig={{ onExport: () => { setNotice('Export requested for the current filter state'); }, }} />

{notice || 'Fields combine with AND. Selected values within a field combine with OR. Date presets use a fixed demonstration period.'}

); }; ``` This implements **Option A** from Figma: one compound chip per active filter, with **field · operator · value · remove**. Change Status from “is either of” to “is not”, search Entity options, or open Due date to choose a preset or calendar range. Use More filters to add Assignee or Amount; the field editor opens immediately. Resize the container to reveal the +N filter overflow menu. The toolbar keeps quiet field buttons. Applied chips wrap below it. Multiple choices stay checked while searching, selected options appear first, and country flags lead entity names. Each menu clears only its own field; Clear filters resets the whole filter map. Date changes apply once a complete range exists. Numeric conditions apply after validation. Fields combine with **AND**; multiple selected choices within one field combine with **OR**. “is not” negates membership. The app controls the value map and can control operators independently. For remote tables, serialize both into your API request and query-selection key; the table never re-filters a server page. Missing dates/numbers do not match a numeric/date condition. Arbitrarily nested AND/OR expressions are not part of this field-filter API. ## Pagination [#pagination] ```tsx import { useState } from 'react'; import { Table } from '@commenda/pathfinder/table'; const invoices = Array.from({ length: 12 }, (_, index) => ({ id: `INV-${String(index + 1).padStart(3, '0')}`, customer: ['Meridian GmbH', 'Meridian US Inc.', 'Meridian UK Ltd.'][ index % 3 ], amount: (index + 1) * 250, })); export const TableDataDefault = () => { const [page, setPage] = useState({ page: 1, pageSize: 5 }); return (
row.id, getLabel: (row) => row.id }} columns={[ { id: 'invoice', header: 'Invoice', value: (row) => row.id }, { id: 'customer', header: 'Customer', width: 240, value: (row) => row.customer, }, { id: 'amount', header: 'Amount (USD)', align: 'end', value: (row) => row.amount, }, ]} paginationConfig={{ value: page, onValueChange: setPage, pageSizes: [5, 10], }} /> ); }; ``` Move through twelve records in pages of five, then change Rows per page. The example controls `{ page, pageSize }` and offers 5/10 for demonstration. Defaults are page **1**, 25 rows, and 25/50/100 choices. Filtering and sorting apply to the full local collection before it is sliced. ## Column visibility [#column-visibility] ```tsx import { useState } from 'react'; import { Table } from '@commenda/pathfinder/table'; const invoices = Array.from({ length: 12 }, (_, index) => ({ id: `INV-${String(index + 1).padStart(3, '0')}`, customer: ['Meridian GmbH', 'Meridian US Inc.', 'Meridian UK Ltd.'][ index % 3 ], amount: (index + 1) * 250, })); export const TableDataVisibility = () => { const [hidden, setHidden] = useState(['amount']); return (
row.id, getLabel: (row) => row.id }} columns={[ { id: 'invoice', header: 'Invoice', hideable: false, value: (row) => row.id, }, { id: 'customer', header: 'Customer', width: 240, value: (row) => row.customer, }, { id: 'amount', header: 'Amount (USD)', align: 'end', value: (row) => row.amount, }, ]} viewConfig={{ hiddenColumns: { value: hidden, onValueChange: setHidden }, }} />

{hidden.length ? `Hidden columns: ${hidden.join(', ')}` : 'All columns visible'}

); }; ``` Open Columns and enable Amount. Invoice cannot be hidden because it identifies the record. The controlled `viewConfig.hiddenColumns` array can be stored in application preferences; column IDs should survive label changes and translations. Density and visibility can be controlled independently. ## Column pinning [#column-pinning] ```tsx import { useState } from 'react'; import { Button } from '@commenda/pathfinder/button'; import { Table, TableCell, TableNumberCell } from '@commenda/pathfinder/table'; const records = [ { id: 'INV-001', customer: 'Meridian GmbH', entity: 'Germany', owner: 'Lucas Moreau', status: 'Paid', amount: 4800, }, { id: 'INV-002', customer: 'Meridian US Inc.', entity: 'United States', owner: 'Priya Shah', status: 'Open', amount: 2150, }, { id: 'INV-003', customer: 'Meridian UK Ltd.', entity: 'United Kingdom', owner: 'Aisha Khan', status: 'Open', amount: 3200, }, ]; export const TableLayoutDefault = () => { const [pins, setPins] = useState< Readonly>> >({ id: 'start', amount: 'end' }); return (
row.id, getLabel: (row) => row.id }} selectionConfig={{}} viewConfig={{ columnPinning: { value: pins, onValueChange: setPins } }} columns={[ { id: 'id', header: 'Invoice', width: 140, hideable: false, value: (row) => row.id, }, { id: 'customer', header: 'Customer', width: 280, cell: (row) => , }, { id: 'entity', header: 'Entity', width: 180, value: (row) => row.entity, }, { id: 'owner', header: 'Owner', width: 180, value: (row) => row.owner, }, { id: 'status', header: 'Status', width: 120, value: (row) => row.status, }, { id: 'amount', header: 'Amount', width: 160, align: 'end', cell: (row) => , }, ]} />

Scroll horizontally. Invoice starts pinned; Amount ends pinned. Change pins from Columns.

); }; ``` Scroll horizontally: Invoice stays at the logical start and Amount at the end. Open Columns, then a column's pin control to move it or unpin it. Selection and action cells remain at their edges alongside pinned data. Hidden columns do not leave gaps. Pinning uses logical edges in RTL and releases in a narrow container when the pinned regions would obscure readable data. Use `column.pin` for a fixed default or `viewConfig.columnPinning` for interactive preferences. Its value is a map of column IDs to `'start'`, `'end'`, or `null` (explicitly unpinned). `pinnable: false` locks a column's configured position. ## Drag column widths [#drag-column-widths] ```tsx import { useState } from 'react'; import { Button } from '@commenda/pathfinder/button'; import { Table, TableCell, TableNumberCell } from '@commenda/pathfinder/table'; const records = [ { id: 'INV-001', customer: 'Meridian GmbH', entity: 'Germany', owner: 'Lucas Moreau', status: 'Paid', amount: 4800, }, { id: 'INV-002', customer: 'Meridian US Inc.', entity: 'United States', owner: 'Priya Shah', status: 'Open', amount: 2150, }, { id: 'INV-003', customer: 'Meridian UK Ltd.', entity: 'United Kingdom', owner: 'Aisha Khan', status: 'Open', amount: 3200, }, ]; export const TableLayoutResizing = () => { const [widths, setWidths] = useState< Readonly>> >({}); return (
row.id, getLabel: (row) => row.id }} viewConfig={{ columnWidths: { value: widths, onValueChange: setWidths }, columnPinning: { defaultValue: { id: 'start' } }, }} columns={[ { id: 'id', header: 'Invoice', width: 140, minWidth: 100, maxWidth: 240, value: (row) => row.id, }, { id: 'customer', header: 'Customer', width: 280, minWidth: 160, maxWidth: 440, cell: (row) => , }, { id: 'owner', header: 'Owner', width: 200, minWidth: 160, maxWidth: 320, value: (row) => row.owner, }, { id: 'amount', header: 'Amount', width: 160, minWidth: 140, maxWidth: 240, align: 'end', cell: (row) => , }, ]} />

Customer width: {widths.customer ?? 280}px. Drag a header edge, or focus it and use Left/Right (10px), Shift (50px), Home/End.

); }; ``` Drag a header's trailing edge to increase or decrease its width. Focus the handle to use Left/Right in 10px steps, Shift for 50px, or Home/End for the bounds. Escape during a drag restores its starting width. Resize does not trigger sorting; pinned offsets follow the updated widths. Enable handles with `viewConfig.columnWidths`. Set each column's `width`, `minWidth`, `maxWidth`, and optionally `resizable: false`. The default bounds are 80–800px; choose bounds appropriate to the content, especially money and dates. Controlled width and pin maps can be saved in application preferences. Explicit layout widths preserve accurate drag distances and pin offsets; a table can leave unused space when its configured total is narrower than its container. ## Row selection [#row-selection] ```tsx import { useState } from 'react'; import { Table, type TableSelection } from '@commenda/pathfinder/table'; const invoices = [ { id: 'INV-001', customer: 'Meridian GmbH', status: 'Paid', amount: 2400 }, { id: 'INV-002', customer: 'Meridian US Inc.', status: 'Pending', amount: 1800, }, { id: 'INV-003', customer: 'Meridian UK Ltd.', status: 'Overdue', amount: 950, }, { id: 'INV-004', customer: 'Meridian Singapore', status: 'Pending', amount: 3200, }, ]; export const TableInteractionsSelection = () => { const [selection, setSelection] = useState({ kind: 'ids', ids: [], }); const [notice, setNotice] = useState( 'Select unpaid invoices to request a reminder.' ); return (
row.id, getLabel: (row) => row.id }} columns={[ { id: 'invoice', header: 'Invoice', value: (row) => row.id }, { id: 'customer', header: 'Customer', width: 240, value: (row) => row.customer, }, { id: 'status', header: 'Status', value: (row) => row.status }, ]} paginationConfig={{ defaultValue: { page: 1, pageSize: 2 }, pageSizes: [2, 4], }} selectionConfig={{ value: selection, onValueChange: setSelection, canSelect: (row) => row.status !== 'Paid', allMatching: { queryKey: 'demo-unpaid-invoices', total: 3 }, actions: [ { id: 'remind', label: 'Request reminder', onAction: () => { const count = selection.kind === 'all' ? 3 - selection.excludedIds.length : selection.ids.length; setNotice( `Reminder requested for ${String(count)} invoices. No messages were sent by this demo.` ); }, }, ], }} />

{notice}

); }; ``` Select an unpaid invoice, move to the next page, or choose **Select all 3** in the bulk bar. Paid invoices are ineligible. Selection persists by stable ID. Request reminder updates the status message only; this demo does not send messages. `TableSelection` is `{ kind: 'ids', ids }` or `{ kind: 'all', queryKey, excludedIds }`. The header checkbox selects the current eligible page. `allMatching` explicitly enables the entire eligible query, including unloaded pages. Pass a new query key when filter membership or authorization scope changes. The server must interpret that query/exclusion model and recheck permissions. ## Row actions and copying [#row-actions-and-copying] ```tsx import { useState } from 'react'; import { Table, TableCell } from '@commenda/pathfinder/table'; const invoices = [ { id: 'INV-001', customer: 'Meridian GmbH', status: 'Paid', amount: 2400 }, { id: 'INV-002', customer: 'Meridian US Inc.', status: 'Pending', amount: 1800, }, { id: 'INV-003', customer: 'Meridian UK Ltd.', status: 'Overdue', amount: 950, }, { id: 'INV-004', customer: 'Meridian Singapore', status: 'Pending', amount: 3200, }, ]; export const TableInteractionsActions = () => { const [notice, setNotice] = useState('Open a row or use its menu.'); return (
row.id, getLabel: (row) => row.id, onActivate: (row) => { setNotice(`Opening ${row.id}`); }, menu: (row) => ({ groups: [ { id: 'record', items: [ { id: 'view', type: 'action', label: 'View invoice', onClick: () => { setNotice(`Opening ${row.id}`); }, }, { id: 'download', type: 'action', label: 'Download receipt', disabled: row.status !== 'Paid', disabledReason: 'A receipt is available after payment.', onClick: () => { setNotice(`Receipt download requested for ${row.id}`); }, }, ], }, ], }), }} columns={[ { id: 'invoice', header: 'Invoice', value: (row) => row.id, cell: (row) => ( { void navigator.clipboard.writeText(row.id).then( () => { setNotice(`${row.id} copied`); }, () => { setNotice( 'Clipboard unavailable. Select the invoice number to copy it.' ); } ); }} /> ), }, { id: 'customer', header: 'Customer', width: 240, value: (row) => row.customer, }, { id: 'status', header: 'Status', value: (row) => row.status }, ]} />

{notice}

); }; ``` Click a record to open it, use its overflow menu, or hover/focus the invoice number to copy it. Those controls do not also activate the row. Download receipt is unavailable before payment. The example reports requested actions locally; real navigation/downloads belong to the application. Use specific action names and at most a primary icon, secondary icon and overflow per row. Keep whole-record editing in a Sheet; reserve inline editing for frequently changed fields. ## Custom cells [#custom-cells] ```tsx import { useState } from 'react'; import { DEFlag } from '@commenda/pathfinder/flags/DEFlag'; import { USFlag } from '@commenda/pathfinder/flags/USFlag'; import { EyeIcon } from '@commenda/pathfinder/icons/EyeIcon'; import { Table, TableCell, TableRowActions, type TableSelection, } from '@commenda/pathfinder/table'; const filingRows = [ { id: '1', filing: 'Delaware annual report', description: 'Annual compliance · 2026', entity: 'Meridian US Inc.', country: 'US', assignee: 'Priya Shah', status: 'In progress', due: '2026-10-15', }, { id: '2', filing: 'VAT return Q3', description: 'Quarterly tax return', entity: 'Meridian GmbH', country: 'DE', assignee: 'Lucas Moreau', status: 'To do', due: '2026-10-20', }, { id: '3', filing: 'Corporate income tax', description: 'Financial year 2025', entity: 'Meridian US Inc.', country: 'US', assignee: 'Aisha Khan', status: 'Done', due: '2026-10-25', }, { id: '4', filing: 'Financial statements', description: 'Annual financial reporting', entity: 'Meridian GmbH', country: 'DE', assignee: 'Jun Tan', status: 'To do', due: '2026-10-30', }, ]; export const TableOverview = () => { const [filters, setFilters] = useState>({}); const [selection, setSelection] = useState({ kind: 'ids', ids: [], }); const [notice, setNotice] = useState(''); return (
row.id, getLabel: (row) => row.filing, onActivate: (row) => { setNotice(`Opened ${row.filing}`); }, }} columns={[ { id: 'filing', header: 'Filing', width: 260, value: (row) => row.filing, sortable: true, hideable: false, cell: (row) => ( ), }, { id: 'entity', header: 'Entity', width: 200, value: (row) => row.entity, cell: (row) => ( : } /> ), }, { id: 'assignee', header: 'Assignee', width: 180, value: (row) => row.assignee, cell: (row) => ( ), }, { id: 'status', header: 'Status', align: 'center', width: 120, value: (row) => row.status, cell: (row) => ( ), }, { id: 'due', header: 'Due date', width: 120, value: (row) => row.due, sortable: true, }, { id: 'actions', header: 'Actions', width: 56, hideable: false, cell: (row) => ( , onAction: () => { setNotice(`Opened ${row.filing}`); }, }} /> ), }, ]} searchConfig={{ getText: (row) => `${row.filing} ${row.entity} ${row.assignee}`, }} filterConfig={{ filters: [ { id: 'status', label: 'Status', options: ['To do', 'In progress', 'Done'].map((value) => ({ value, label: value, })), matches: (row, values) => values.includes(row.status), }, ], value: filters, onValueChange: setFilters, }} selectionConfig={{ value: selection, onValueChange: setSelection, actions: [ { id: 'assign', label: 'Assign', onAction: () => { setNotice('Assignment requested for selected records'); }, }, ], }} toolbarConfig={{ onExport: () => { setNotice('Export requested'); }, }} /> {notice &&

{notice}

} ); }; ``` This filing table combines descriptions, country flags, assignees, status badges and row actions in a real record layout. `cell(row)` composes existing Pathfinder components without changing the underlying record shape. Entity names use flags; people use Avatar. System status uses Badge, while selected removable values use Tag. Text aligns at the leading edge, numbers at the trailing edge, and badges align with their header. Missing values use an en dash, not zero. Use `TableNumberCell` for consistent decimals and ISO currency labels. Long names can grow a row rather than lose essential content. ## Grouped records [#grouped-records] ```tsx import { useState } from 'react'; import { DEFlag } from '@commenda/pathfinder/flags/DEFlag'; import { USFlag } from '@commenda/pathfinder/flags/USFlag'; import { Table, TableCell, TableNumberCell } from '@commenda/pathfinder/table'; export const TableWorkflowsDefault = () => { const rows = [ { id: '1', filing: 'VAT return Q3', description: 'Monthly · Federal Central Tax Office', entity: 'DE', assignee: 'Lucas Moreau', status: 'Filed', fee: 0, }, { id: '2', filing: 'Trade tax return', description: 'Annual · Munich', entity: 'DE', assignee: 'Priya Shah', status: 'In review', fee: 180, }, { id: '3', filing: 'Annual accounts', description: 'Annual · Bundesanzeiger', entity: 'DE', assignee: 'Aisha Khan', status: 'Not started', fee: 95, }, { id: '4', filing: 'Delaware annual report', description: 'Annual · Delaware', entity: 'US', assignee: 'Jun Tan', status: 'Overdue', fee: 450, }, { id: '5', filing: 'BOI report update', description: 'One-time · FinCEN', entity: 'US', assignee: 'Mira de Vries', status: 'Due soon', fee: 0, }, ]; const [notice, setNotice] = useState(''); return (
row.id, getLabel: (row) => row.filing, menu: (row) => ({ groups: [ { id: 'filing', items: [ { id: 'review', type: 'action', label: 'Review filing', onClick: () => { setNotice(`Reviewing ${row.filing}`); }, }, ], }, ], }), }} groupingConfig={{ getId: (row) => row.entity, getLabel: (id) => (id === 'DE' ? 'Acme GmbH' : 'Acme Inc.'), leading: (id) => id === 'DE' ? : , summary: (_id, pageRows) => `${String(pageRows.filter((row) => row.status === 'Filed').length)} of ${String(pageRows.length)} filed`, }} selectionConfig={{}} columns={[ { id: 'filing', header: 'Filing', width: 280, cell: (row) => ( ), }, { id: 'assignee', header: 'Assignee', width: 180, cell: (row) => ( ), }, { id: 'status', header: 'Status', width: 128, cell: (row) => ( ), }, { id: 'fee', header: 'Fee', width: 120, align: 'end', cell: (row) => ( ), }, ]} />

{notice || 'Collapse a group, select records, then reopen it. Selection persists; counts describe the current page.'}

); }; ``` Group filings by entity with a flag, count and read-only summary. Activate a group header to collapse it. Selection survives collapse, and the header checkbox still selects eligible records on the current page. Search, sort and pagination operate before grouping; page counts describe records, never group headers. A large entity may span pages. The summary callback receives the current page's group records; pass app-owned aggregates if the summary must describe the entire query. Use `groupingConfig.getId`, `getLabel`, optional `leading`, `summary`, and controlled/uncontrolled `collapsedGroups`. For recursive financial rows, use [DataGrid hierarchy](/docs/components/data-grid#expandable-rows-and-totals). ## Toolbar actions [#toolbar-actions] ```tsx import { useState } from 'react'; import { FilePdfIcon } from '@commenda/pathfinder/file-types/FilePdfIcon'; import { PlusIcon } from '@commenda/pathfinder/icons/PlusIcon'; import { Table, TableCell } from '@commenda/pathfinder/table'; export const TableWorkflowsActions = () => { const [notice, setNotice] = useState(''); return (
( } /> ), }, ]} rowConfig={{ getId: (row) => row.id, getLabel: (row) => row.name }} searchConfig={{ getText: (row) => row.name }} toolbarConfig={{ onExport: () => { setNotice('Export requested'); }, secondaryAction: { id: 'folder', label: 'New folder', onAction: () => { setNotice('New folder requested'); }, }, primaryAction: { id: 'upload', label: 'Upload', icon: , onAction: () => { setNotice('Upload files requested'); }, menu: { groups: [ { id: 'upload', items: [ { id: 'folder', type: 'action', label: 'Upload folder', onClick: () => { setNotice('Upload folder requested'); }, }, ], }, ], }, }, moreMenu: { groups: [ { id: 'extra', items: [ { id: 'refresh', type: 'action', label: 'Refresh documents', onClick: () => { setNotice('Refresh requested'); }, }, ], }, ], }, }} />

{notice || 'Demonstration actions update this message. Your application supplies the upload, export and folder workflows.'}

); }; ``` The right side contains tools, overflow, a divider, then at most one secondary and one primary action. Export becomes an icon when actions are present. A primary action with `menu` renders a SplitButton: the main button performs the default action, while the arrow exposes alternatives. Selection actions belong in the bulk bar. Use named `primaryAction` and `secondaryAction`; put extra actions in `moreMenu`. These examples report callbacks locally. Your application supplies upload, export and navigation workflows. ## Documents [#documents] ```tsx import { useState } from 'react'; import { FilePdfIcon } from '@commenda/pathfinder/file-types/FilePdfIcon'; import { DEFlag } from '@commenda/pathfinder/flags/DEFlag'; import { USFlag } from '@commenda/pathfinder/flags/USFlag'; import { Table, TableCell } from '@commenda/pathfinder/table'; export const TableWorkflowsDocuments = () => { const [notice, setNotice] = useState(''); const rows = [ { id: 'HRB 284731', name: 'Delaware annual report.pdf', country: 'DE', entity: 'Acme GmbH', person: 'Priya Shah', date: '2026-10-02', }, { id: 'DE-7745120', name: 'Articles of association.pdf', country: 'US', entity: 'Acme Inc.', person: 'Lucas Moreau', date: '2026-09-28', }, ]; return (
row.id, getLabel: (row) => row.name }} sortingConfig={{ defaultValue: { columnId: 'uploaded', direction: 'desc' }, }} columns={[ { id: 'name', header: 'Document', width: 300, cell: (row) => ( } /> ), }, { id: 'entity', header: 'Entity', width: 180, cell: (row) => ( ) : ( ) } /> ), }, { id: 'reference', header: 'Reference', width: 170, cell: (row) => ( { void navigator.clipboard.writeText(row.id).then( () => { setNotice(`Copied ${row.id}`); }, () => { setNotice('Clipboard permission is unavailable'); } ); }} /> ), }, { id: 'person', header: 'Uploaded by', width: 170, cell: (row) => ( ), }, { id: 'uploaded', header: 'Uploaded', width: 140, sortable: true, value: (row) => row.date, }, ]} />

{notice || 'Copy a reference or change the upload-date sort.'}

); }; ``` File icons identify documents, country flags identify entities, and avatars identify uploaders. References expose a copy action on hover, focus or touch. The demo writes the selected reference to the clipboard and reports failures. ## Team members [#team-members] ```tsx import { useState } from 'react'; import { Table, TableCell } from '@commenda/pathfinder/table'; export const TablePeopleDefault = () => { const [notice, setNotice] = useState(''); const people = [ { id: 'priya', name: 'Priya Shah', email: 'priya@acme.com', role: 'Admin', status: 'Active', twoFactor: true, active: 'Today', }, { id: 'lucas', name: 'Lucas Moreau', email: 'lucas@acme.com', role: 'Member', status: 'Active', twoFactor: true, active: 'Yesterday', }, { id: 'aisha', name: 'Aisha Khan', email: 'aisha@acme.com', role: 'Read only', status: 'Invited', twoFactor: false, active: null, }, { id: 'jun', name: 'Jun Tan', email: 'jun@acme.com', role: 'Member', status: 'Deactivated', twoFactor: false, active: '12 Aug 2026', }, ]; return (
`${row.name} ${row.email}` }} rowConfig={{ getId: (row) => row.id, getLabel: (row) => row.name, menu: (row) => ({ groups: [ { id: 'member', items: [ { id: 'details', type: 'action', label: 'View member', onClick: () => { setNotice(`Viewing ${row.name}`); }, }, ], }, ], }), }} columns={[ { id: 'name', header: 'Member', width: 280, cell: (row) => ( ), }, { id: 'role', header: 'Role', width: 150, cell: (row) => ( ), }, { id: 'status', header: 'Status', width: 120, cell: (row) => ( ), }, { id: 'twoFactor', header: '2FA', width: 80, align: 'center', cell: (row) => ( ), }, { id: 'active', header: 'Last active', width: 140, value: (row) => row.active, }, ]} />

{notice || 'Roles are values (Tags); account statuses are read-only Badges.'}

); }; ``` People include their email, role as a Tag, account status as a Badge and an accessible yes/no symbol for two-factor authentication. Missing activity is an en dash. Row-specific actions live in the overflow menu. ## Density and containers [#density-and-containers] ```tsx import { useState } from 'react'; import { Button } from '@commenda/pathfinder/button'; import { Table, TableCell } from '@commenda/pathfinder/table'; export const TablePatternsDefault = () => { const [density, setDensity] = useState<'compact' | 'relaxed'>('compact'); const [container, setContainer] = useState<'card' | 'flat'>('card'); return (
row.id, getLabel: (row) => row.label }} columns={[ { id: 'filing', header: 'Filing', width: 300, cell: (row) => ( ), }, { id: 'status', header: 'Status', width: 140, align: 'end', cell: () => ( ), }, ]} /> ); }; ``` Use the view menu to compare compact and relaxed density, and the button to switch card/flat containers. The second record includes a description to show the two-line behavior. | Measurement | Compact | Relaxed | | ----------------------- | ------- | ------- | | Header | 36px | 36px | | Single-line row minimum | 40px | 52px | | Two-line text row | 52px | 64px | | Horizontal cell padding | 12px | 12px | A card adds a 12px radius and hairline border. Use flat inside an existing surface. Wrapped content, text enlargement and taller custom controls can increase these heights. ## Presets and summary [#presets-and-summary] ```tsx import { useState } from 'react'; import { Table, TableCell, TableNumberCell } from '@commenda/pathfinder/table'; export const TablePatternsPresets = () => { const rows = [ { id: '1', label: 'Annual accounts', status: 'Overdue', amount: 2400 }, { id: '2', label: 'VAT return Q3', status: 'Due soon', amount: 1800 }, { id: '3', label: 'Trade tax return', status: 'Filed', amount: 950 }, { id: '4', label: 'Corporate income tax', status: 'Filed', amount: 1250 }, ]; const [filters, setFilters] = useState<{ status?: readonly string[] }>({}); const selected = filters.status?.length === 1 ? filters.status[0] : filters.status?.length ? undefined : 'All'; const visible = rows.filter( (row) => !filters.status?.length || filters.status.includes(row.status) ); return (
{['All', 'Overdue', 'Due soon', 'Filed'].map((label) => ( ))}
row.id, getLabel: (row) => row.label }} columns={[ { id: 'filing', header: 'Filing', width: 260, value: (row) => row.label, }, { id: 'amount', header: 'Amount', width: 160, align: 'end', cell: (row) => , }, { id: 'status', header: 'Status', width: 140, align: 'end', cell: (row) => ( ), }, ]} searchConfig={{}} filterConfig={{ value: filters, onValueChange: setFilters, filters: [ { id: 'status', label: 'Status', options: ['Overdue', 'Due soon', 'Filed'].map((label) => ({ value: label, label, })), matches: (row, values) => values.includes(row.status), }, ], }} summary={
Matching records {visible.length} Total due{' '} {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(visible.reduce((sum, row) => sum + row.amount, 0))}
} /> ); }; ``` Choose Overdue, then clear its toolbar filter. Preset cards and filters share state, so the selected preset updates with the query. The summary is read-only. Cards sit above the table; summary content lives inside it. This example's summary follows the status filter. Production summaries should use aggregates for the same reporting query, including search when applicable, rather than summing one loaded API page. Preset definitions, counts and persistence are app-owned. ## Editable cells and totals [#editable-cells-and-totals] ```tsx import { useState } from 'react'; import { Table, TableEditCell, TableNumberCell, } from '@commenda/pathfinder/table'; export const TablePatternsEditing = () => { const [rows, setRows] = useState([ { id: '1', name: 'Software subscriptions', amount: '2400.00' }, { id: '2', name: 'Office rent', amount: '1800.00' }, ]); const [notice, setNotice] = useState('All changes saved'); return (
row.id, getLabel: (row) => row.name }} columns={[ { id: 'name', header: 'Account', width: 300, hideable: false, cell: (row) => ( value.trim() ? undefined : 'Enter an account name.' } onCommit={(name) => { setRows((current) => current.map((item) => item.id === row.id ? { ...item, name } : item ) ); setNotice('Saved just now'); }} /> ), }, { id: 'amount', header: 'Amount', width: 180, align: 'end', cell: (row) => ( } validate={(value) => value.trim() ? undefined : 'Enter an amount.' } onCommit={(amount) => { setRows((current) => current.map((item) => item.id === row.id ? { ...item, amount } : item ) ); setNotice('Saved just now'); }} /> ), }, ]} footerConfig={{ total: { label: 'Total', cells: { amount: ( sum + Number(row.amount), 0)} unit="USD" format={{ minimumFractionDigits: 2, maximumFractionDigits: 2, }} /> ), }, }, addAction: { id: 'add', label: 'Add line', onAction: () => { setRows((current) => [ ...current, { id: String(current.length + 1), name: 'New account', amount: '0.00', }, ]); setNotice('Line added locally'); }, }, }} />

{notice}

); }; ``` Hover a row, click an account or amount, and edit it. Enter saves, Escape cancels, and Add line appends a record. Totals recompute from application data and align by column ID through `footerConfig.total.cells`. The first visible column contains the total label. The example saves in memory. An async `onCommit` must resolve only after persistence succeeds and reject on failure; the editor retains rejected drafts and offers Retry save. Text/number fields use 28px compact geometry. Select and DatePicker preserve their existing accessible dimensions and can increase row height. ## Account mapping and validation [#account-mapping-and-validation] ```tsx import { useState } from 'react'; import { Checkbox } from '@commenda/pathfinder/checkbox'; import { Table, TableEditCell, TableNumberCell, } from '@commenda/pathfinder/table'; export const TableEditingDefault = () => { const [rows, setRows] = useState([ { id: '6100', name: 'Office rent', mapping: 'OPEX', allocation: '60', amount: 48000, }, { id: '6200', name: 'Software subscriptions', mapping: 'OPEX', allocation: '100', amount: 21340.5, }, { id: '5000', name: 'Cost of goods sold', mapping: 'COGS', allocation: '100', amount: 412880, }, { id: '6400', name: 'Shared services fee', mapping: 'OPEX', allocation: '140', amount: 96000, }, ]); const [outlined, setOutlined] = useState(false); const [fail, setFail] = useState(false); const [notice, setNotice] = useState( 'The last row has an invalid allocation. Edit it to correct the value.' ); const save = ( id: string, patch: { mapping?: string; allocation?: string } ) => { if (fail) return Promise.reject( new Error('Save failed. Your draft is retained; try again.') ); setRows((current) => current.map((row) => (row.id === id ? { ...row, ...patch } : row)) ); setNotice('Saved just now'); return Promise.resolve(); }; return (
row.id, getLabel: (row) => row.name }} columns={[ { id: 'id', header: 'Account', width: 96, value: (row) => row.id }, { id: 'name', header: 'Name', width: 260, value: (row) => row.name }, { id: 'mapping', header: 'Mapping', width: 200, cell: (row) => ( ({ value: label, label, })), }} outlined={outlined} onCommit={(mapping) => save(row.id, { mapping })} /> ), }, { id: 'allocation', header: 'Allocation', width: 160, align: 'end', cell: (row) => ( 100 ? 'Allocation cannot exceed 100%.' : undefined } validate={(value) => Number(value) < 0 || Number(value) > 100 || !value.trim() ? 'Enter an allocation from 0 to 100.' : undefined } onCommit={(allocation) => save(row.id, { allocation })} /> ), }, { id: 'amount', header: 'Amount', width: 160, align: 'end', cell: (row) => ( ), }, ]} footerConfig={{ total: { label: 'Total', cells: { amount: ( sum + row.amount, 0)} unit="EUR" /> ), }, }, addAction: { id: 'add', label: 'Add line', onAction: () => { setRows((current) => [ ...current, { id: `new-${String(current.length)}`, name: 'New account', mapping: 'OPEX', allocation: '0', amount: 0, }, ]); }, }, }} />

{notice}

); }; ``` This account-mapping example combines Select and numeric editors, an existing invalid allocation, aligned money totals and Add line. Try Keep fields outlined for form-style tables. Simulate save failure retains the draft and exposes retry; disable the simulation to complete the save. The app owns validation and financial calculations. ## Dates, switches and attachments [#dates-switches-and-attachments] ```tsx import { useRef, useState } from 'react'; import { Button } from '@commenda/pathfinder/button'; import { FilePdfIcon } from '@commenda/pathfinder/file-types/FilePdfIcon'; import { IconButton } from '@commenda/pathfinder/icon-button'; import { Upload01Icon } from '@commenda/pathfinder/icons/Upload01Icon'; import { Switch } from '@commenda/pathfinder/switch'; import { Table, TableCell, TableEditCell } from '@commenda/pathfinder/table'; export const TableEditingControls = () => { const [date, setDate] = useState('2026-10-30'); const [enabled, setEnabled] = useState(true); const [filename, setFilename] = useState(''); const fileInput = useRef(null); return (
row.id, getLabel: (row) => row.name }} columns={[ { id: 'name', header: 'Filing', width: 240, value: (row) => row.name, }, { id: 'due', header: 'Due date', width: 220, cell: () => ( ), }, { id: 'enabled', header: 'Reminders', width: 120, cell: () => ( ), }, { id: 'attachment', header: 'Attachment', width: 260, cell: () => filename ? ( } /> ) : ( fileInput.current?.click()} > ), }, ]} /> { setFilename(event.currentTarget.files?.[0]?.name ?? ''); }} /> {filename && ( )}

{filename ? `Selected ${filename} locally; no file was uploaded.` : 'Date, reminder and attachment changes are local to this example.'}

); }; ``` Compose DatePicker editing, an immediate Switch and an upload action directly in cells. The picker keeps Pathfinder's approved DateField geometry. Once a PDF is chosen, its filename replaces the upload action. File selection is local in this example; no bytes leave the browser. Real uploads need application-side file validation, progress, persistence and failure handling. ## API-controlled data [#api-controlled-data] ```tsx import { useState } from 'react'; import { Table } from '@commenda/pathfinder/table'; const filingRows = [ { id: '1', filing: 'Delaware annual report', description: 'Annual compliance · 2026', entity: 'Meridian US Inc.', country: 'US', assignee: 'Priya Shah', status: 'In progress', due: '2026-10-15', }, { id: '2', filing: 'VAT return Q3', description: 'Quarterly tax return', entity: 'Meridian GmbH', country: 'DE', assignee: 'Lucas Moreau', status: 'To do', due: '2026-10-20', }, { id: '3', filing: 'Corporate income tax', description: 'Financial year 2025', entity: 'Meridian US Inc.', country: 'US', assignee: 'Aisha Khan', status: 'Done', due: '2026-10-25', }, { id: '4', filing: 'Financial statements', description: 'Annual financial reporting', entity: 'Meridian GmbH', country: 'DE', assignee: 'Jun Tan', status: 'To do', due: '2026-10-30', }, ]; export const TableRemote = () => { const [page, setPage] = useState({ page: 1, pageSize: 25 }); const [query, setQuery] = useState(''); // Replace this synchronous fixture with an API response. The app owns request cancellation. const matches = filingRows.filter((row) => row.filing.toLowerCase().includes(query.toLowerCase()) ); return (
row.filing }]} rowConfig={{ getId: (row) => row.id, getLabel: (row) => row.filing }} paginationConfig={{ value: page, onValueChange: setPage, totalRows: matches.length, }} searchConfig={{ value: query, onValueChange: setQuery }} /> ); }; ``` `dataMode="remote"` displays exactly the rows returned by the app. Table never sorts, filters or slices that received page again. The example uses synchronous fixture filtering to demonstrate state ownership, not a production API adapter. Use search, filters, sort and pagination as a request key. Keep typing responsive, debounce requests rather than the displayed value, cancel obsolete requests, and prevent stale responses from replacing newer results. Remote numbered pages require `paginationConfig.totalRows`. URL persistence, exports and server authorization remain application responsibilities. ### Cursor pagination [#cursor-pagination] ```tsx import { useState } from 'react'; import { Table } from '@commenda/pathfinder/table'; const invoices = Array.from({ length: 12 }, (_, index) => ({ id: `INV-${String(index + 1).padStart(3, '0')}`, customer: ['Meridian GmbH', 'Meridian US Inc.', 'Meridian UK Ltd.'][ index % 3 ], amount: (index + 1) * 250, })); export const TableDataCursor = () => { // Simulates server pages; a real API owns opaque cursors and request cancellation. const [pageIndex, setPageIndex] = useState(0); const pageSize = 4; const pageRows = invoices.slice( pageIndex * pageSize, (pageIndex + 1) * pageSize ); return (
row.id, getLabel: (row) => row.id }} columns={[ { id: 'invoice', header: 'Invoice', value: (row) => row.id }, { id: 'customer', header: 'Customer', width: 240, value: (row) => row.customer, }, ]} paginationConfig={{ mode: 'cursor', pageSize, hasPrevious: pageIndex > 0, hasNext: (pageIndex + 1) * pageSize < invoices.length, onPrevious: () => { setPageIndex((current) => current - 1); }, onNext: () => { setPageIndex((current) => current + 1); }, summary: `Showing ${String(pageRows.length)} records`, }} /> ); }; ``` Use Previous and Next without an invented total. The demo simulates server pages; real applications hold opaque API cursors. Supply `hasNext`, `hasPrevious`, callbacks and an honest summary. Pair cursor configuration with `dataMode="remote"`. ## Loading, empty and error [#loading-empty-and-error] ```tsx import { useState } from 'react'; import { Button } from '@commenda/pathfinder/button'; import { Table } from '@commenda/pathfinder/table'; export const TableStates = () => { const [state, setState] = useState<'loading' | 'empty' | 'error'>('loading'); return (
{(['loading', 'empty', 'error'] as const).map((value) => ( ))}
'', getLabel: () => '' }} stateConfig={{ isLoading: state === 'loading', error: state === 'error' ? { message: 'The request failed. Your filters are preserved.', onRetry: () => { setState('loading'); }, } : undefined, empty: { title: 'No filings yet', description: 'Add your first filing to start tracking compliance.', }, }} /> ); }; ``` Switch between loading skeletons, first-use emptiness and a retryable error. Search/filter emptiness instead offers Clear filters. Keep toolbar and column geometry stable while requests run. Retry should repeat the current query, not discard the user's scope. ## Accessibility and responsive behavior [#accessibility-and-responsive-behavior] Table retains native table semantics. Checkbox selection is separate from row activation; a focused activatable row opens with Enter. Search shortcuts apply while focus is in table tools. The scroll region supports keyboard access. Narrow layouts scroll within the component and release pinned columns if they would hide readable values. Read the [Figma rules and explicit differences](/docs/components/table#design-rules) before changing dimensions or behavior. ## API reference [#api-reference] `groupingConfig` groups the current page: `{ getId(row), getLabel(id), leading?(id), summary?(id, pageRows), collapsedGroups? }`. Collapsed state is a list of group IDs using the same `value` / `onValueChange` or `defaultValue` convention as other preferences. `TableFilter` is discriminated by `type`: | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `label` | `string` | required | Visible field label. Supply an accessible name when it is omitted. | | `rows` | `readonly TRow[]` | required | | | `columns` | `readonly TableColumn[]` | required | | | `rowConfig` | `{ getId: (row: TRow) => string; getLabel: (row: TRow) => string; onActivate?: ((row: TRow) => void) \| undefined; menu?: ((row: TRow) => DropdownMenu) \| undefined; }` | required | | | `viewConfig` | `{ density?: SelectionProps<"compact" \| "relaxed">; hiddenColumns?: SelectionProps; columnWidths?: SelectionProps>>>; columnPinning?: SelectionProps>>>; }` | optional | | | `groupingConfig` | `{ getId: (row: TRow) => string; getLabel: (id: string) => string; leading?: (id: string) => ReactNode; summary?: ((id: string, pageRows: readonly TRow[]) => ReactNode) \| undefined; collapsedGroups?: SelectionProps; }` | optional | | | `container` | `"card" \| "flat"` | optional | | | `sortingConfig` | `SelectionProps` | optional | | | `searchConfig` | `SelectionProps & { getText?: ((row: TRow) => string) \| undefined; placeholder?: string; }` | optional | | | `filterConfig` | `{ filters: readonly TableFilter[]; operators?: SelectionProps>>; value: Readonly>; onValueChange: (value: Record) => void; }` | optional | | | `selectionConfig` | `SelectionProps & { canSelect?: ((row: TRow) => boolean) \| undefined; allMatching?: { queryKey: string; total: number; }; actions?: readonly TableAction[]; moreMenu?: DropdownMenu; }` | optional | | | `toolbarConfig` | `TableToolbarConfig` | optional | | | `stateConfig` | `TabularStateConfig` | optional | | | `summary` | `ReactNode` | optional | | | `footerConfig` | `{ total?: { label: string; cells: Readonly>; }; addAction?: TableAction; }` | optional | | | `className` | `string` | optional | Additional classes for the component root. | | `dataMode` | `"local" \| "remote"` | conditional | | | `paginationConfig` | `{ mode?: "pages"; totalRows?: number; pageSizes?: readonly number[]; } & SelectionProps \| { mode: "cursor"; hasNext: boolean; hasPrevious: boolean; onNext: () => void; onPrevious: () => void; summary?: string; pageSize: number; onPageSizeChange?: (size: number) => void; } \| ({ mode?: "pages"; totalRows?: number; pageSizes?: readonly number[]; } & SelectionProps) & { totalRows: number; }` | conditional | | ### rowConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `getId` | `(row: TRow) => string` | Yes | | | `getLabel` | `(row: TRow) => string` | Yes | | | `onActivate` | `((row: TRow) => void)` | No | | | `menu` | `((row: TRow) => DropdownMenu)` | No | | ### viewConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `density` | `SelectionProps<"compact" \| "relaxed">` | No | | | `hiddenColumns` | `SelectionProps` | No | | | `columnWidths` | `SelectionProps>>>` | No | | | `columnPinning` | `SelectionProps>>>` | No | | ### viewConfig.density fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `value` | `"compact" \| "relaxed"` | Yes | | | `onValueChange` | `(value: "compact" \| "relaxed") => void` | Yes | | Do not supply: `defaultValue`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `defaultValue` | `"compact" \| "relaxed"` | No | | | `onValueChange` | `((value: "compact" \| "relaxed") => void)` | No | | Do not supply: `value`. ### viewConfig.hiddenColumns fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `value` | `readonly string[]` | Yes | | | `onValueChange` | `(value: readonly string[]) => void` | Yes | | Do not supply: `defaultValue`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `defaultValue` | `readonly string[]` | No | | | `onValueChange` | `((value: readonly string[]) => void)` | No | | Do not supply: `value`. ### viewConfig.columnWidths fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `value` | `Readonly>>` | Yes | | | `onValueChange` | `(value: Readonly>>) => void` | Yes | | Do not supply: `defaultValue`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `defaultValue` | `Readonly>>` | No | | | `onValueChange` | `((value: Readonly>>) => void)` | No | | Do not supply: `value`. ### viewConfig.columnPinning fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `value` | `Readonly>>` | Yes | | | `onValueChange` | `(value: Readonly>>) => void` | Yes | | Do not supply: `defaultValue`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `defaultValue` | `Readonly>>` | No | | | `onValueChange` | `((value: Readonly>>) => void)` | No | | Do not supply: `value`. ### groupingConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `getId` | `(row: TRow) => string` | Yes | | | `getLabel` | `(id: string) => string` | Yes | | | `leading` | `((id: string) => ReactNode)` | No | | | `summary` | `((id: string, pageRows: readonly TRow[]) => ReactNode)` | No | | | `collapsedGroups` | `SelectionProps` | No | | ### groupingConfig.collapsedGroups fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `value` | `readonly string[]` | Yes | | | `onValueChange` | `(value: readonly string[]) => void` | Yes | | Do not supply: `defaultValue`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `defaultValue` | `readonly string[]` | No | | | `onValueChange` | `((value: readonly string[]) => void)` | No | | Do not supply: `value`. ### sortingConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `value` | `TableSort \| null` | Yes | | | `onValueChange` | `(value: TableSort \| null) => void` | Yes | | Do not supply: `defaultValue`. ### sortingConfig.value fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `columnId` | `string` | Yes | | | `direction` | `"desc" \| "asc"` | Yes | | #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `defaultValue` | `TableSort \| null` | No | | | `onValueChange` | `((value: TableSort \| null) => void)` | No | | Do not supply: `value`. ### sortingConfig.defaultValue fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `columnId` | `string` | Yes | | | `direction` | `"desc" \| "asc"` | Yes | | ### filterConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `filters` | `readonly TableFilter[]` | Yes | | | `operators` | `SelectionProps>>` | No | | | `value` | `Readonly>` | Yes | | | `onValueChange` | `(value: Record) => void` | Yes | | ### filterConfig.operators fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `value` | `Readonly>` | Yes | | | `onValueChange` | `(value: Readonly>) => void` | Yes | | Do not supply: `defaultValue`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `defaultValue` | `Readonly>` | No | | | `onValueChange` | `((value: Readonly>) => void)` | No | | Do not supply: `value`. ### toolbarConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `actions` | `readonly TableAction[]` | No | Prefer named actions. Legacy actions use first=primary, second=secondary, remainder=overflow. | | `primaryAction` | `(TableAction & { menu?: DropdownMenu; icon?: ReactNode; })` | No | | | `secondaryAction` | `TableAction` | No | | | `onExport` | `(() => void)` | No | | | `moreMenu` | `DropdownMenu` | No | | ### toolbarConfig.secondaryAction fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | `string` | Yes | | | `label` | `string` | Yes | | | `onAction` | `() => void` | Yes | | | `disabled` | `boolean` | No | | ### toolbarConfig.moreMenu fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `groups` | `DropdownGroup[]` | Yes | | ### stateConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isLoading` | `boolean` | No | | | `error` | `{ message: string; onRetry: () => void; }` | No | | | `empty` | `{ title: string; description?: string; action?: TableAction; }` | No | | ### stateConfig.error fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `message` | `string` | Yes | | | `onRetry` | `() => void` | Yes | | ### stateConfig.empty fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | `string` | Yes | | | `description` | `string` | No | | | `action` | `TableAction` | No | | ### stateConfig.empty.action fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | `string` | Yes | | | `label` | `string` | Yes | | | `onAction` | `() => void` | Yes | | | `disabled` | `boolean` | No | | ### footerConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `total` | `{ label: string; cells: Readonly>; }` | No | | | `addAction` | `TableAction` | No | | ### footerConfig.total fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | Yes | | | `cells` | `Readonly>` | Yes | | ### footerConfig.addAction fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | `string` | Yes | | | `label` | `string` | Yes | | | `onAction` | `() => void` | Yes | | | `disabled` | `boolean` | No | | ### paginationConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `mode` | `"cursor"` | Yes | | | `hasNext` | `boolean` | Yes | | | `hasPrevious` | `boolean` | Yes | | | `onNext` | `() => void` | Yes | | | `onPrevious` | `() => void` | Yes | | | `summary` | `string` | No | | | `pageSize` | `number` | Yes | | | `onPageSizeChange` | `((size: number) => void)` | No | | All filters have `id`, `label`, optional `icon`, and `initiallyVisible: false` for fields added through More filters. `filterConfig.operators` uses a controlled/uncontrolled map of `is`, `isNot`, `isAnyOf`, `within`, `greaterThan` or `lessThan`; menus expose only operators meaningful for that field type. Range filters use inclusive endpoints. A custom choice `matches` predicate describes positive membership; `isNot` is applied by Table. | Prop | Type | Behavior | | ------------------ | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `label` | string | Required accessible table name | | `rows` | readonly TRow[] | Immutable local collection or one received API page | | `columns` | readonly TableColumn<TRow>[] | IDs, headers, accessors and renderers | | `rowConfig` | `{ getId, getLabel, onActivate?, menu? }` | Stable unique IDs; activation opens the record, checkbox selects it | | `dataMode` | `'local'` or `'remote'` | Local by default; remote never reprocesses the received page | | `viewConfig` | `{ density?, hiddenColumns?, columnWidths?, columnPinning? }` | Each uses controlled value/onValueChange or defaultValue; compact and all columns visible by default | | `container` | `'card'` or `'flat'` | Card by default | | `sortingConfig` | SelectionProps<TableSort or null> | Controlled value/onValueChange or defaultValue | | `searchConfig` | `{ value?, defaultValue?, onValueChange?, getText?, placeholder? }` | Enables search; default local searchable text joins column values | | `filterConfig` | `{ filters, value, onValueChange, operators? }` | Controlled filter map; typed choice/date/number definitions, optional controlled operators | | `paginationConfig` | Page or cursor configuration | Local page 1 and 25 rows by default | | `selectionConfig` | SelectionProps plus selection options | Stable IDs, optional eligibility, query-scoped select-all and bulk actions | | `toolbarConfig` | `{ primaryAction?, secondaryAction?, onExport?, moreMenu?, actions? }` | App callbacks; exports never imply just the loaded rows | | `stateConfig` | `{ isLoading?, error?, empty? }` | Skeleton rows, retryable failure or empty state | | `summary` | ReactNode | Read-only summary inside the container; put app preset controls above Table | | `footerConfig` | `{ total?: { label, cells }, addAction? }` | App-owned totals keyed by column ID and an explicit add-row action; first visible column contains the total label | | `className` | string | Root classes | `TableColumn` has `id`, string `header`, optional `description` tooltip, `value(row)`, `cell(row)`, `width`, `minWidth`, `maxWidth` in pixels, `resizable`, `pinnable`, `align: 'start' | 'center' | 'end'`, `sortable`, `compare(left, right)`, `hideable`, and `pin: 'start' | 'end'`. `value` supplies sortable/searchable data even when `cell` renders rich content. Pinned columns are grouped at their logical edge; horizontal pinning releases when it would leave insufficient space for a value column. Do not hide every column. Column IDs and row IDs must remain stable, nonempty and unique when updating data. Invalid IDs, empty column collections and invalid page numbers fail with configuration errors. `viewConfig.density` uses `compact` or `relaxed`; `viewConfig.hiddenColumns` uses column IDs. Control either independently to persist user preferences. Non-hideable columns stay visible, and the first column remains as a safeguard if all hideable columns were requested hidden. `TableSort` is `{ columnId, direction: 'asc' | 'desc' }`; the sort control cycles ascending, descending, unsorted. Local search/filter/sort run before pagination. Search and sort changes return to the first page. App-controlled filters should reset the page when changed externally. ### Cell renderers [#cell-renderers] `TableCell` is a discriminated content component; the owning table supplies padding and height: | `type` | Content props | | ----------------- | ------------------------------------------------------------------- | | `text` (default) | `value`, `description`, `leadingIcon`, `badge`, `tooltip`, `struck` | | `entity` / `file` | `label`, `icon`, optional `description` | | `person` | Avatar `identity`, optional `description` | | `people` | `label`, AvatarGroup `people` | | `badge` / `tag` | `items` using existing Badge/Tag props | | `boolean` | `value`, optional accessible `label` | | `progress` | existing ProgressBar `progress` props | | `link` | `label`, `linkConfig` | | `copy` | `value`, `onCopy`, optional `copyLabel` | | `loading` | Skeleton content | | `number` | All TableNumberCell props | `TableNumberCell` accepts `value`, optional `locale`, Intl.NumberFormat `format`, `unit`, `suffix`, `tone`, `struck` and `trend: { direction: 'up' | 'down', intent: 'positive' | 'critical' | 'neutral' }`. Numbers align right without truncation; unit labels stay separate. Empty and non-finite numeric values show an en dash. Numeric strings preserve their supplied formatting. Trend direction does not determine business meaning: rising costs may be critical. `TableEditCell` accepts `label`, string `value`, `editor`, optional `display`, `outlined`, `disabled`, `validationMessage`, `validate(value)` and async `onCommit(value)`. Editors are `{ type: 'text', placeholder? }`, `{ type: 'number', min?, max?, step?, unit?, suffix? }`, `{ type: 'select', options, placeholder? }`, or `{ type: 'date', min?, max? }`. Number drafts remain strings; validate and parse at the application boundary. Enter or leaving the editor saves; Escape cancels. Failed saves retain the draft and show Retry save. Select/calendar portals belong to the same editing session. DatePicker retains the approved Kakashi DateField padding. DataGrid manages `editingConfig` internally; ordinary Table renderers use the local editing state. ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### API references [#api-references] The API review used [shadcn's Data Table guide](https://ui.shadcn.com/docs/components/base/data-table), [TanStack controlled state](https://tanstack.com/table/v8/docs/framework/react/guide/table-state) and [typed column definitions](https://tanstack.com/table/v8/docs/guide/column-defs). Pathfinder adopts independently controlled state and reusable boundaries while retaining direct props, Figma styling and precompiled CSS. It keeps TanStack 8.21.3 implementation types private; the current shadcn guide's v9 code is not copied into this v8 integration. Matrix keyboard behavior follows the [ARIA grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/). ### Table [#table] - **TABLE-001:** White card, radius 12px, pearl-300 hairline 0.5px; optional flat container. Header 36px, compact row minimum 40px, relaxed 52px, horizontal padding 12px. Two-line text rows are 52px compact and 64px relaxed; longer wrapping and accessible controls can grow further. Toolbar and pagination minimum 48px. - **TABLE-002:** Hover and selected rows share pearl-200. Checkbox distinguishes selection; clicking the row opens the record. Links, editors, menus and checkboxes do not also activate the row. Enter opens a focused activatable row. - **TABLE-003:** Local processing is filter/search, sort, then page. Remote processing belongs entirely to the app; no double filtering, sorting or slicing. The app owns URL state, fetching, cancellation, permissions, exports and API cursors. Slash opens search while focus is within table tools. Escape clears then closes search. Search opens in place and its clear action clears and closes. Tools remain together; excess filter buttons use +N. Active compound filter chips wrap below the toolbar. - **TABLE-004:** Selection persists by stable IDs. All-matching selection requires a query key and eligible total, with explicit excluded IDs. Changing the query key invalidates stale all-selection. Bulk actions float 24px above the viewport bottom; three actions show directly and additional actions use overflow. - **TABLE-005:** Totals are supplied by column ID through footerConfig. The first visible column labels the total; Add line is a separate explicit application action. - **TABLE-006:** Column width handles support pointer capture, bounded drag, RTL, 10px arrow steps, Shift 50px, Home/End and Escape rollback. Widths and pinning can be controlled independently. Logical pin offsets include selection/action columns and follow resized or hidden columns. - **TABLE-007:** Group bars use a 2px inset, pearl-100 and radius 6px. Collapse preserves selection. Group only the current page; page counts and select-current-page refer to records, including collapsed descendants. - **TABLE-008:** Toolbar tools precede overflow and a divider; at most one secondary and one primary action follow. Export becomes an icon when actions are present. Primary plus alternatives uses SplitButton. Bulk actions never move into the ordinary toolbar. - **FILTER-001:** AND across fields, OR within multiple choices; `isNot` negates membership. Remote tables forward controlled values/operators to the application and never filter the received page. - **FILTER-002:** Menus align to their trigger at 4px offset. Single choice replaces; multiple choices retain selection while searching and show selected values first. Date presets/ranges reuse DateCalendar. Numeric ranges require valid ascending endpoints. Clear resets only the owning field. - **FILTER-003:** Option A chips are bordered, radius 6px and minimum 28px. Field/value reopen the editor; operator opens only compatible operators; remove clears that field. Separate chips wrap beneath the toolbar. - **FILTER-004:** More filters adds a secondary field and opens its editor immediately. +N exposes filters that do not fit. No active field becomes inaccessible when the container narrows. - **TABLE-009:** Header master `430:13536` / `428:76`: 36px high, Label/12 Strong, 12px icons, 4px gaps, pearl-800 at rest and pearl-1000 for interactive hover/focus. The neutral selector appears on unsorted hover/focus; sorted direction stays visible. End-aligned headers place sorting before the label. Optional information has an independent keyboard-focusable tooltip. Table backgrounds are white; DataGrid headers retain pearl-100 and vertical rules. ### Shared cells [#shared-cells] - **CELL-001:** Text starts at the logical leading edge; numbers align at the trailing edge and use tabular figures. Empty values show an en dash. Entity flags are 16px, person avatars 20px. Entity names wrap; do not truncate monetary amounts. Cells compose existing Badge, Tag, Avatar, ProgressBar and exact icon assets. - **CELL-002:** Currency units remain quiet and separate from values. Fixed decimals are supplied via Intl format options. Trend direction and semantic intent are separate. A cost increase can be critical even though its arrow points up. - **CELL-003:** At most primary bordered icon, secondary icon and overflow in a row action group. Do not repeat text CTAs in every row. - **CELL-EDIT-001:** Compact text/number editor minimum 28px and radius 6px; calm border at rest, outline on hover/focus, persistent invalid treatment. Existing Select and DatePicker keep their accessible field geometry; the DatePicker retains the user's approved Kakashi DateField padding rather than shrinking its segmented inputs. - **CELL-EDIT-002:** Enter or leaving the editor commits one cell; Escape cancels. Pending work cannot duplicate; errors retain drafts and offer retry. React portal focus stays within the same editing session. Application code validates and persists canonical values. The Figma cell master depicts combinations, not 32 separate exported component classes. The shared content family composes those visual patterns. Toggle and upload actions use existing Switch/FileUpload application workflows in custom cells; they do not pretend to be string editors. TableCell content, TableNumberCell, TableRowActions and TableEditCell work in both owning components. ### Explicit differences and boundaries [#explicit-differences-and-boundaries] - **Selection:** the Table prototype's row-click selection is superseded by its written best practice: row activation opens a record; checkbox selects. The implementation follows that distinction. - **Accessibility:** quiet text uses pearl-800 for contrast. Long content and text zoom can grow rows. Narrow viewports release horizontal pinning. Copy controls remain visible on touch, and long status labels expose their full text on focus/hover. - **Application patterns:** presets, summary aggregates, “Saved just now”, network requests, exports, permissions and financial calculations are owned by the app. Demos perform local updates or simulated requests and say so. - **States and scale:** matrix loading/error fallbacks reuse existing primitives. No virtualization, range operations or spreadsheet formulas are claimed. Stress fixtures do not establish an unlimited-data performance guarantee. AI consumers should read this page together with the component API and integration pages. Do not infer a public prop from a Figma variant name, or treat a prototype's sample amounts as domain validation rules. ### Header and cell master reconciliation [#header-and-cell-master-reconciliation] | Reference | Coverage | | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [Header master 430:13536](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=430-13536) | Start/center/end alignment, unsorted/ascending/descending, hover/focus, optional information, checkbox and empty action header; Headers, Selection and Pinning examples. | | [Groups and add row 445:13858](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=445-13858) | Grouped filings show flags, counts, collapse and page-selection persistence; editable lines show totals and Add line. Application callbacks own actual persistence. | Long content and zoom can grow rows beyond Figma's nominal heights. Linear progress and ring progress use their existing Pathfinder primitives; cell recipes do not introduce a new progress implementation. A file-picker example selects a local file and displays its name; production upload, progress and retry belong to the application's upload workflow. These are deliberate compositional boundaries, not built-in backend features. --- # Tabs Package: @commenda/pathfinder@0.1.0 Page: /docs/components/tabs Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=352-2 ```tsx import { type TabItem, Tabs } from '@commenda/pathfinder/tabs'; const entityTabs: TabItem[] = [ { value: 'details', label: 'Details', content: 'Your workspace name, logo and default entity.', }, { value: 'filings', label: 'Filings', count: 12, content: 'Review upcoming and completed filings.', }, { value: 'documents', label: 'Documents', count: 3, content: 'Manage the documents for this entity.', }, { value: 'activity', label: 'Activity', content: 'Recent changes to this entity.', }, ]; export const TabsDefault = () => ( ); ``` ## Installation [#installation] ```sh pnpm --filter your-app add "@commenda/pathfinder@workspace:*" ``` Import `@commenda/pathfinder/styles.css` once after host CSS. Pathfinder ships compiled, prefixed Tailwind v4 CSS; the application needs no Tailwind scanning. Import from `@commenda/pathfinder/tabs`. Interactive callbacks belong in a client component in Next.js App Router. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `label` | `string` | required | Required; tablist accessible name | | `items` | `readonly TabItem[]` | required | Required; value, label, content; optional count and disableConfig | | `value` | `string \| null` | conditional | Controlled / initial selection; first enabled item if omitted | | `defaultValue` | `string \| null` | optional | Controlled / initial selection; first enabled item if omitted | | `onValueChange` | `((value: string \| null) => void)` | conditional | Required with value | | `orientation` | `"vertical" \| "horizontal"` | optional | horizontal | | `activationMode` | `"manual" \| "automatic"` | optional | automatic; use manual for slow remote content | | `divider` | `boolean` | optional | true; horizontal only | | `panelConfig` | `{ keepMounted?: boolean; className?: string; }` | optional | keepMounted false | | `overflowConfig` | `{ label?: string; }` | optional | More | | `id` | `string` | optional | Stable HTML identifier for the component. | | `className` | `string` | optional | Additional classes for the component root. | ### panelConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `keepMounted` | `boolean` | No | | | `className` | `string` | No | | ### overflowConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | `string` | No | | Root IDs, className, appropriate ARIA attributes, and typed refs are supported. Controlled value requires onValueChange and cannot be combined with defaultValue. Item IDs/values must be unique and stable. ## Usage rules [#usage-rules] `disableConfig: { isDisabled: boolean; reason?: string }` keeps disabled state and its explanation together. Counts are plain numbers, including zero; do not use a Badge or icons inside the tab label. Horizontal overflow is measured against the container and moves hidden options into More. The active tab stays visible; choosing an overflow item returns focus to its promoted tab. Before measurement, a local scrolling row keeps options reachable. Extremely long active labels remain horizontally scrollable without overflowing the page. Arrow keys navigate visible tabs; Home/End reach their ends. Automatic mode also selects on arrow focus. Manual mode activates with Enter/Space. More is a separate menu button, reached with Tab. Hidden options remain available in that menu. Disabled options cannot activate. `keepMounted` retains inactive content without leaving it focusable. Uncontrolled tabs fall back to the first enabled item after removal/disablement. Controlled callers reconcile changed items and own URL synchronization. `null` means no active panel. These tabs model related panels, not an unrelated route-only sidebar. ## Examples [#examples] ### Vertical [#vertical] ```tsx import { type TabItem, Tabs } from '@commenda/pathfinder/tabs'; const entityTabs: TabItem[] = [ { value: 'details', label: 'Details', content: 'Your workspace name, logo and default entity.', }, { value: 'filings', label: 'Filings', count: 12, content: 'Review upcoming and completed filings.', }, { value: 'documents', label: 'Documents', count: 3, content: 'Manage the documents for this entity.', }, { value: 'activity', label: 'Activity', content: 'Recent changes to this entity.', }, ]; export const TabsVertical = () => ( ); ``` ### Overflow [#overflow] ```tsx import { type TabItem, Tabs } from '@commenda/pathfinder/tabs'; const entityTabs: TabItem[] = [ { value: 'details', label: 'Details', content: 'Your workspace name, logo and default entity.', }, { value: 'filings', label: 'Filings', count: 12, content: 'Review upcoming and completed filings.', }, { value: 'documents', label: 'Documents', count: 3, content: 'Manage the documents for this entity.', }, { value: 'activity', label: 'Activity', content: 'Recent changes to this entity.', }, ]; export const TabsOverflow = () => (
); ``` ### Controlled [#controlled] ```tsx import { useState } from 'react'; import { type TabItem, Tabs } from '@commenda/pathfinder/tabs'; const entityTabs: TabItem[] = [ { value: 'details', label: 'Details', content: 'Your workspace name, logo and default entity.', }, { value: 'filings', label: 'Filings', count: 12, content: 'Review upcoming and completed filings.', }, { value: 'documents', label: 'Documents', count: 3, content: 'Manage the documents for this entity.', }, { value: 'activity', label: 'Activity', content: 'Recent changes to this entity.', }, ]; export const TabsControlled = () => { const [value, setValue] = useState('details'); return (

Selected view: {value}

); }; ``` ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Tabs [#tabs] Source: [352:10826](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=352-10826), horizontal 352:95, vertical 352:121, docs 354:278. - `TABS-01` — Source: horizontal height 28, gap 12, Label/12 Strong, 0.5 divider, 2px blue-700 indicator. Counts are plain Label/12 numbers, gap 4; never Badge or icons. Vertical width 180, padding 6/8, radius 6, gap 2, Label/13 Strong, selected pearl-200. - `TABS-02` — Source/Pathfinder: related panels for one subject. First enabled item is the uncontrolled default. Caller controls URL synchronization; unrelated routes use navigation. Indicator 200ms ease-out, no panel animation, reduced motion immediate. - `TABS-03` — Source/Pathfinder: More exposes options that do not fit. Measurement reserves More and the active tab; selected hidden tabs become visible and receive focus. Inert copies measure widths; pre-measurement and exceptionally narrow layouts retain local horizontal scrolling. Never discard a tab to solve overflow. - `TABS-04` — Pathfinder: automatic or manual keyboard activation; disabled items cannot activate. Controlled callers reconcile missing selections; uncontrolled removal/disablement selects the first enabled item. null permits no active panel. keepMounted retains inactive content without focusability; zero counts remain visible. ### Source discrepancies and implementation decisions [#source-discrepancies-and-implementation-decisions] --- # Tag input Package: @commenda/pathfinder@0.1.0 Page: /docs/components/tag-input Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=179-3 ```tsx import { TagInput } from '@commenda/pathfinder/tag-input'; export const TagInputDefault = () => ( ); ``` ## Usage [#usage] Import `TagInput` from `@commenda/pathfinder/tag-input`. Import the compiled stylesheet once. Use Tag input for free-form values; use Multi select when values come from a known set of options. ## Examples [#examples] ### Editing and paste [#editing-and-paste] ```tsx import { useState } from 'react'; import { TagInput } from '@commenda/pathfinder/tag-input'; export const TagInputEditing = () => { const [tags, setTags] = useState([ 'Finance', 'Operations', ]); return ( ); }; ``` ### Validation [#validation] ```tsx import { TagInput } from '@commenda/pathfinder/tag-input'; export const TagInputValidation = () => ( /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? undefined : 'Enter an email address.', }} description="This example checks email syntax only; the application owns recipient validation." /> ); ``` ### States [#states] ```tsx import { TagInput } from '@commenda/pathfinder/tag-input'; export const TagInputStates = () => (
); ``` ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `label` | `string` | conditional | Required accessible field name; alternatively use `aria-label` | | `hideLabel` | `boolean` | optional | Required accessible field name; alternatively use `aria-label` | | `value` | `readonly string[]` | conditional | Controlled values; callback is required with `value` | | `onValueChange` | `((value: readonly string[]) => void)` | conditional | Controlled values; callback is required with `value` | | `defaultValue` | `readonly string[]` | optional | Initial uncontrolled values | | `entryConfig` | `{ editable?: boolean; addOnPaste?: boolean; addOnTab?: boolean; delimiter?: string; maxTags?: number; validate?: (tag: string) => string \| undefined; }` | optional | `editable`, `addOnPaste`, `addOnTab`, `delimiter`, `maxTags`, `validate` | | `name` | `string` | optional | Submit each tag as a repeated named value; read with `FormData.getAll(name)` | | `form` | `string` | optional | Submit each tag as a repeated named value; read with `FormData.getAll(name)` | | `readOnly` | `boolean` | optional | Values submit, editing and removal are blocked | | `disableConfig` | `DisableConfig` | optional | Blocks editing and omits values from form submission | | `description` | `string` | optional | Associated guidance and validation | | `errorConfig` | `{ message: string; reveal?: "after-blur" \| "immediate"; }` | optional | Associated guidance and validation | | `required` | `boolean` | conditional | Associated guidance and validation | | `placeholder` | `string` | optional | The ref targets the entry input | | `onBlur` | `FocusEventHandler` | optional | The ref targets the entry input | | `aria-label` | `string` | conditional | Accessible name when the field has no visible label. | | `labelConfig` | `{ optionalLabel: string; help?: never; } \| { help: string; optionalLabel?: never; }` | optional | Choose either `optionalLabel` or `help`, never both. `never` excludes the other field. | | `id` | `string` | optional | Stable HTML identifier for the component. | | `className` | `string` | optional | Additional classes for the component root. | ### entryConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `editable` | `boolean` | No | | | `addOnPaste` | `boolean` | No | | | `addOnTab` | `boolean` | No | | | `delimiter` | `string` | No | | | `maxTags` | `number` | No | | | `validate` | `((tag: string) => string \| undefined)` | No | Return a message to reject a tag; undefined accepts it. | ### disableConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `isDisabled` | `boolean` | Yes | | | `reason` | `string` | No | | ### errorConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `message` | `string` | Yes | | | `reveal` | `"after-blur" \| "immediate"` | No | | ### labelConfig fields Choose one shape; excluded fields cannot be combined. #### Option 1 | Field | Type | Required | Description | | --- | --- | --- | --- | | `optionalLabel` | `string` | Yes | | Do not supply: `help`. #### Option 2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `help` | `string` | Yes | | Do not supply: `optionalLabel`. ## Keyboard and validation [#keyboard-and-validation] Enter or the configured delimiter adds a trimmed tag. Duplicates are rejected. Arrow keys navigate tags, Backspace removes a highlighted tag, and editable tags support keyboard editing. Tab retains normal focus navigation unless `addOnTab` is enabled. Composition input does not create a tag until composition is complete. Provide unique, nonempty initial values. Validation belongs to the application; the component never treats an email-shaped value as proof of an address or identity. Uncontrolled native resets restore defaults. Controlled resets remain application-owned. ## Design rules [#design-rules] - **TAG-INPUT-001:** Reuse the Dice UI primitive used by Kakashi TagInput; ship Pathfinder’s compiled styling without a runtime Kakashi dependency. - **TAG-INPUT-002:** Use the existing field shell and Tag geometry. Wrap long tags without pushing the entry field outside its container. - **TAG-INPUT-003:** Announce invalid entries and retain keyboard focus during editing and removal. - **TAG-INPUT-004:** Read-only and disabled modes prevent mutations; only enabled values participate in form submission. Tag input is a Kakashi-derived composition of Input and Tag, not a newly extracted standalone Figma component. --- # Tag Package: @commenda/pathfinder@0.1.0 Page: /docs/components/tag Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=179-3 ```tsx import { Tag } from '@commenda/pathfinder/tag'; export const TagDefault = () => ; ``` ## Installation [#installation] Import the component in your screen: ```tsx import { Tag } from '@commenda/pathfinder/tag'; ``` Load the stylesheet once in your app root, after host styles: ```tsx import '@commenda/pathfinder/styles.css'; ``` Pathfinder ships precompiled CSS; no Tailwind setup is required. In Next.js App Router, put interactive state and callbacks in a client component. See [installation](/docs/getting-started/installation) for the workspace setup. ## API reference [#api-reference] | Prop | Type | Requirement | Behavior | | --- | --- | --- | --- | | `label` | `string` | required | Required value name | | `tone` | `"neutral" \| "blue"` | optional | neutral; blue marks added context | | `leadingIcon` | `ReactElement>` | optional | Decorative 12px slot | | `removeConfig` | `{ onRemove: () => void; label?: string; isDisabled?: boolean; }` | optional | Named separate button; app owns removal and undo | | `className` | `string` | optional | Additional classes for the component root. | | `id` | `string` | optional | Stable HTML identifier for the component. | ### removeConfig fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `onRemove` | `() => void` | Yes | | | `label` | `string` | No | | | `isDisabled` | `boolean` | No | | ## Usage rules [#usage-rules] 20px minimum height, radius 4, Label 12. Long labels truncate and expose full text on hover and keyboard focus. Removal stops bubbling to a parent selection control. Do not nest Tag inside a button. Use Badge for system status. Overflow counts have no remove action. ### Tones [#tones] ```tsx import { Tag } from '@commenda/pathfinder/tag'; export const TagTones = () => (
); ``` ### Removal [#removal] ```tsx import { useState } from 'react'; import { Tag } from '@commenda/pathfinder/tag'; export const TagRemoval = () => { const [removed, setRemoved] = useState(false); return removed ? ( Entity removed ) : ( { setRemoved(true); }, }} /> ); }; ``` ## Design rules [#design-rules] Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references. ### Source rules [#source-rules] | Rule | Source | Contract | | ------- | ----------------- | ----------------------------------------------------------- | | TAG-001 | 179:22 | 20px value chip, radius 4, 12px artwork, optional removal. | | TAG-002 | 184 documentation | Keep label single-line; full text available on hover/focus. | All source IDs refer to [The System](https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=68-2). ### Explicit implementation decisions [#explicit-implementation-decisions] Tag's full-label span is keyboard-focusable for its tooltip and remains text; it is not a fake action button. Only its optional remove button changes data. App code owns undo and selection state. --- # Text area Package: @commenda/pathfinder@0.1.0 Page: /docs/components/text-area Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=84-2 ```tsx import { TextArea } from '@commenda/pathfinder/text-area'; export const TextAreaDefault = () => (