@dreamtree-org/twreact-ui 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/components/core/Accordion.d.ts.map +1 -1
- package/dist/components/core/Table/Table.d.ts.map +1 -1
- package/dist/components/core/menu/MenuPopover.d.ts.map +1 -1
- package/dist/components/navigation/Navbar.d.ts.map +1 -1
- package/dist/esm/components/core/Accordion.js +3 -1
- package/dist/esm/components/core/Table/MobileCard.js +2 -2
- package/dist/esm/components/core/Table/MobileFilters.js +1 -1
- package/dist/esm/components/core/Table/Table.js +28 -26
- package/dist/esm/components/core/menu/MenuPopover.js +4 -2
- package/dist/esm/components/navigation/Navbar.js +6 -3
- package/dist/index.css +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +49 -40
- package/dist/index.js +49 -40
- package/mcp/catalog.snapshot.json +8 -8
- package/package.json +3 -2
|
@@ -460,7 +460,7 @@
|
|
|
460
460
|
"importFrom": "@dreamtree-org/twreact-ui",
|
|
461
461
|
"peerDependencies": [],
|
|
462
462
|
"hasDoc": true,
|
|
463
|
-
"description": "Data table with sorting, filtering, pagination, row selection, expandable details, and responsive mobile card view. The toolbar **Reload** button is tinted `bg-primary-100 text-primary-700` with `dark:bg-primary-900/40 dark:text-primary-300` in dark mode.",
|
|
463
|
+
"description": "Data table with sorting, filtering, pagination, row selection, expandable details, and responsive mobile card view. The toolbar **Reload** button is tinted `bg-primary-100 text-primary-700` with `dark:bg-primary-900/40 dark:text-primary-300` in dark mode. Column headers, mobile-card labels and filter labels use `text-primary-700` / `dark:text-primary-300`; mobile-card values use `text-primary-900` / `dark:text-foreground`.",
|
|
464
464
|
"props": [
|
|
465
465
|
{
|
|
466
466
|
"prop": "data",
|
|
@@ -526,13 +526,13 @@
|
|
|
526
526
|
"prop": "onSort",
|
|
527
527
|
"type": "`function`",
|
|
528
528
|
"default": "",
|
|
529
|
-
"description": "Sort callback"
|
|
529
|
+
"description": "Sort callback `(key, direction)`. Called once per sort, after the state update (not during render), so it may set parent state."
|
|
530
530
|
},
|
|
531
531
|
{
|
|
532
532
|
"prop": "onFilter",
|
|
533
533
|
"type": "`function`",
|
|
534
534
|
"default": "",
|
|
535
|
-
"description": "Filter callback"
|
|
535
|
+
"description": "Filter callback `(filters)`. Called once per change, outside render, like `onSort`."
|
|
536
536
|
},
|
|
537
537
|
{
|
|
538
538
|
"prop": "onFetch",
|
|
@@ -3697,9 +3697,9 @@
|
|
|
3697
3697
|
},
|
|
3698
3698
|
{
|
|
3699
3699
|
"prop": "rightMenuContent",
|
|
3700
|
-
"type": "`(props) => ReactNode`",
|
|
3700
|
+
"type": "`ReactNode` | `(props) => ReactNode`",
|
|
3701
3701
|
"default": "",
|
|
3702
|
-
"description": "Custom right menu.
|
|
3702
|
+
"description": "Custom right menu. An element is rendered as is; a function receives `{ notifications }` and any Navbar `...props` (e.g. `emitter`, `onFetch`). If omitted or `null`, the default notifications dropdown renders."
|
|
3703
3703
|
},
|
|
3704
3704
|
{
|
|
3705
3705
|
"prop": "onLoginClick",
|
|
@@ -7182,7 +7182,7 @@
|
|
|
7182
7182
|
}
|
|
7183
7183
|
},
|
|
7184
7184
|
"docs": {
|
|
7185
|
-
"Accordion": "# Accordion\n\nExpandable/collapsible content panels. Only one panel open at a time by default, or multiple when `allowMultiple` is true. Supports keyboard navigation (Arrow keys, Home, End, Enter, Space). The bordered wrapper and each item use the semantic `bg-surface` / `divide-line` / `text-foreground` tokens, so the panels follow dark mode.\n\n## Import\n\n```jsx\nimport { Accordion } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `items` | `Array` | `[]` | Array of `{ id?, title, subtitle?, content, disabled?, leftIcon? }`. `leftIcon` can be ReactNode or `(index, item) => ReactNode`. |\n| `allowMultiple` | `boolean` | `false` | If true, multiple panels can be open at once. |\n| `defaultOpenIndexes` | `number[]` | `[]` | Initial open panel index(es). |\n| `onChange` | `(openIndexes: number[]) => void` | — | Called when open panels change. |\n| `className` | `string` | `\"\"` | Wrapper CSS classes. |\n| `headerClassName` | `string` | `\"\"` | Header button CSS classes. |\n| `contentClassName` | `string` | `\"\"` | Panel content CSS classes. |\n| `chevronPosition` | `\"left\"` \\| `\"right\"` | `\"right\"` | Position of chevron icon. |\n| `chevronIcon` | `ReactNode` | default SVG | Custom chevron icon. |\n| `bordered` | `boolean` | `true` | Whether to show border/background. |\n| `rounded` | `\"none\"` \\| `\"sm\"` \\| `\"md\"` \\| `\"lg\"` \\| `\"xl\"` | `\"md\"` | Border radius of wrapper. |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Header and content padding/text size. |\n| `transitionDuration` | `number` | `220` | Max-height transition duration in ms. Ignored (no animation) when the user prefers reduced motion. |\n\n## Accessibility & behavior\n\n- Header/panel ids are prefixed with a per-instance `useId()`, so several accordions on one page never collide.\n- Collapsed panels carry the `inert` attribute, so Tab never lands on focusable content inside a closed panel.\n- Header focus ring uses `focus-visible:ring-primary-500` (keyboard focus only).\n- Once an open transition finishes, the panel's `max-height` is released to `none`, so content that grows after opening is never clipped.\n- The height transition is disabled under `prefers-reduced-motion: reduce`.\n\n## Example\n\n```jsx\n<Accordion\n items={[\n { id: '1', title: 'Section 1', content: <p>Content for section 1</p> },\n { id: '2', title: 'Section 2', subtitle: 'Optional', content: <p>Content 2</p> },\n { title: 'Disabled', content: <p>Content</p>, disabled: true },\n ]}\n allowMultiple={false}\n defaultOpenIndexes={[0]}\n onChange={(indexes) => console.log('Open:', indexes)}\n size=\"md\"\n chevronPosition=\"right\"\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
7185
|
+
"Accordion": "# Accordion\n\nExpandable/collapsible content panels. Only one panel open at a time by default, or multiple when `allowMultiple` is true. Supports keyboard navigation (Arrow keys, Home, End, Enter, Space). The bordered wrapper and each item use the semantic `bg-surface` / `divide-line` / `text-foreground` tokens, so the panels follow dark mode.\n\n## Import\n\n```jsx\nimport { Accordion } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `items` | `Array` | `[]` | Array of `{ id?, title, subtitle?, content, disabled?, leftIcon? }`. `leftIcon` can be ReactNode or `(index, item) => ReactNode`. |\n| `allowMultiple` | `boolean` | `false` | If true, multiple panels can be open at once. |\n| `defaultOpenIndexes` | `number[]` | `[]` | Initial open panel index(es). |\n| `onChange` | `(openIndexes: number[]) => void` | — | Called when open panels change. |\n| `className` | `string` | `\"\"` | Wrapper CSS classes. |\n| `headerClassName` | `string` | `\"\"` | Header button CSS classes. |\n| `contentClassName` | `string` | `\"\"` | Panel content CSS classes. |\n| `chevronPosition` | `\"left\"` \\| `\"right\"` | `\"right\"` | Position of chevron icon. |\n| `chevronIcon` | `ReactNode` | default SVG | Custom chevron icon. |\n| `bordered` | `boolean` | `true` | Whether to show border/background. |\n| `rounded` | `\"none\"` \\| `\"sm\"` \\| `\"md\"` \\| `\"lg\"` \\| `\"xl\"` | `\"md\"` | Border radius of wrapper. |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Header and content padding/text size. |\n| `transitionDuration` | `number` | `220` | Max-height transition duration in ms. Ignored (no animation) when the user prefers reduced motion. |\n\n## Accessibility & behavior\n\n- Header/panel ids are prefixed with a per-instance `useId()`, so several accordions on one page never collide.\n- Collapsed panels carry the `inert` attribute, so Tab never lands on focusable content inside a closed panel.\n- Header focus ring uses `focus-visible:ring-primary-500` (keyboard focus only).\n- Once an open transition finishes, the panel's `max-height` is released to `none`, so content that grows after opening is never clipped.\n- The height transition is disabled under `prefers-reduced-motion: reduce`.\n- Server rendering (Next.js) is warning-free: panels are sized in a layout effect on the client and a plain effect on the server.\n\n## Example\n\n```jsx\n<Accordion\n items={[\n { id: '1', title: 'Section 1', content: <p>Content for section 1</p> },\n { id: '2', title: 'Section 2', subtitle: 'Optional', content: <p>Content 2</p> },\n { title: 'Disabled', content: <p>Content</p>, disabled: true },\n ]}\n allowMultiple={false}\n defaultOpenIndexes={[0]}\n onChange={(indexes) => console.log('Open:', indexes)}\n size=\"md\"\n chevronPosition=\"right\"\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
7186
7186
|
"ActionSheet": "# ActionSheet\n\niOS-style action sheet (the `UIAlertController` action-sheet pattern) on the bottom `Sheet`: an optional title and message, a card of actions — destructive ones in the error tone — and a separate Cancel button. Use it for \"what do you want to do with this?\" choices on phones; it is the same implementation `ThreeDotPopover` and `Table`'s row menu use for their phone presentation.\n\n## Import\n\n```jsx\nimport { ActionSheet } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `open` | `boolean` | — | Controls visibility |\n| `isOpen` | `boolean` | — | Alias of `open` |\n| `onClose` | `(reason) => void` | — | Called when the sheet should close; `reason` is `'select'` (after an action's `onSelect`), `'cancel'`, `'escape'`, `'backdrop'` or `'drag'` |\n| `title` | `ReactNode` | — | Small heading above the actions; names the sheet (`aria-labelledby`) unless `aria-label` is set |\n| `message` | `ReactNode` | — | Muted text under the title; wired to `aria-describedby` |\n| `actions` | `Array` | `[]` | `[{ key?, label, icon?, tone?: 'default' or 'destructive', onSelect, disabled? }]`. `icon` is a node (`<Trash2 />`) or a component (`Trash2`) |\n| `cancelLabel` | `ReactNode` | `\"Cancel\"` | Text of the separate Cancel button (always shown) |\n| `dismissible` | `boolean` | `true` | `false` disables Escape, backdrop click and drag dismissal |\n| `className` | `string` | — | Classes merged onto the sheet panel |\n\nOther props (`aria-label`, `data-*`, …) are forwarded to the sheet panel (`role=\"dialog\"`), and the ref points at it.\n\n### Behavior\n\n- Selecting an enabled action calls its `onSelect`, then `onClose('select')`. Disabled actions do nothing.\n- Cancel, Escape, a backdrop tap and a downward drag all call `onClose` with their reason; none of them calls an action.\n- You own the open state: set `open` to `false` in `onClose`.\n- With no `title` and no `aria-label`, the sheet is named \"Actions\".\n\n## Accessibility\n\n- The panel is a modal `role=\"dialog\"`; focus moves to the first action on open, Tab stays inside, and Escape closes.\n- Focus returns to the element that was focused before the sheet opened (usually the button that opened it).\n- Rows are at least 48px tall with a focus-visible ring; destructive rows pair the error colour with their label text.\n- The slide-in and slide-out animations are off under `prefers-reduced-motion`.\n\n## Examples\n\n```jsx\nfunction PhotoMenu({ photo }) {\n const [open, setOpen] = useState(false);\n return (\n <>\n <Button onClick={() => setOpen(true)}>Options</Button>\n <ActionSheet\n open={open}\n onClose={() => setOpen(false)}\n title=\"Photo options\"\n message=\"Choose what to do with this photo.\"\n actions={[\n { key: 'share', label: 'Share', icon: Share2, onSelect: () => share(photo) },\n { key: 'archive', label: 'Archive', onSelect: () => archive(photo), disabled: photo.archived },\n { key: 'delete', label: 'Delete', icon: Trash2, tone: 'destructive', onSelect: () => remove(photo) },\n ]}\n />\n </>\n );\n}\n```\n\n```jsx\n// Custom cancel text, no title (named by aria-label)\n<ActionSheet\n open={open}\n onClose={() => setOpen(false)}\n aria-label=\"Sort by\"\n cancelLabel=\"Never mind\"\n actions={[\n { label: 'Newest first', onSelect: () => setSort('new') },\n { label: 'Oldest first', onSelect: () => setSort('old') },\n ]}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
7187
7187
|
"Alert": "# Alert\n\nInline alert box for success, error, warning, info or neutral messages. Optional title, an actions slot after the description, and a dismiss button. Every variant has a dark tone, and the alert fades in (off under `prefers-reduced-motion`).\n\n## Import\n\n```jsx\nimport { Alert } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `variant` | `string` | `\"info\"` | `success`, `error`, `warning`, `info`, `neutral` (role tokens: surface, line, muted). Unknown values fall back to `info` |\n| `title` | `string` | — | Optional title |\n| `children` | `ReactNode` | — | Alert body content (the description) |\n| `actions` | `ReactNode` | — | Rendered after the description in a wrapping row (buttons, links) |\n| `dismissible` | `boolean` | `false` | Show close button |\n| `onDismiss` | `function` | — | Called when dismissed |\n| `dismissLabel` | `string` | `\"Dismiss\"` | Accessible name of the dismiss button |\n| `className` | `string` | — | Extra CSS classes |\n\nOther props are forwarded to the root `div`; the ref points at it.\n\n## Accessibility\n\n- `error` and `warning` render with `role=\"alert\"` (announced immediately); `info`, `success` and `neutral` use `role=\"status\"` (polite), so routine messages don't interrupt a screen reader. Pass `role` to override.\n- The dismiss button is labelled \"Dismiss\" (`dismissLabel`), has a focus-visible ring and a 44px hit area on touch screens.\n- The enter animation (`animate-fade-in`) is off under `prefers-reduced-motion`.\n\n## Examples\n\n```jsx\n<Alert variant=\"info\" title=\"Note\">Your session will expire in 5 minutes.</Alert>\n<Alert variant=\"success\">Saved successfully.</Alert>\n<Alert variant=\"error\" title=\"Error\">Something went wrong.</Alert>\n<Alert variant=\"warning\" dismissible onDismiss={() => {}}>\n Please review your input.\n</Alert>\n<Alert variant=\"neutral\" title=\"Heads up\">Billing moves to the 1st of each month.</Alert>\n\n<Alert\n variant=\"error\"\n title=\"Upload failed\"\n actions={\n <>\n <Button size=\"sm\" onClick={retry}>Retry</Button>\n <Button size=\"sm\" variant=\"ghost\" onClick={openHelp}>Get help</Button>\n </>\n }\n>\n The file is larger than 10 MB.\n</Alert>\n```\n\n[← Component overview](README.md)\n",
|
|
7188
7188
|
"AppShell": "# AppShell\n\nPage-level block: the product app shell — a sidebar on large screens, a top bar with a navigation drawer below `lg`, and an optional bottom nav on phones. Replaces the shell 12 of 13 products hand-rolled (extracted from procurementpro `components/app-shell.tsx`). Colours come from the `shell` and role tokens (`bg-background`, `bg-surface`, `text-foreground`, `text-muted`, `border-line`), so a brand file restyles it and dark mode needs no extra classes.\n\nThe shell fills the dynamic viewport (`min-h-dvh`; the sidebar is `h-dvh`), so mobile browser toolbars don't cut it off. It pads the safe areas: the top bar has `pt-safe` and `px-safe`, the sidebar `pt-safe pb-safe pl-safe`, the content column `px-safe`. Add `viewport-fit=cover` to your viewport meta.\n\n## Import\n\n```jsx\nimport { AppShell } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `brand` | `ReactNode` | — | Product mark at the top of the sidebar |\n| `nav` | `AppShellNavItem[]` | — | `{ id, label, href?, icon?, active?, count?, group?, onClick? }`. Items with `href` render as links; `active` sets `aria-current=\"page\"` |\n| `sidebarFooter` | `ReactNode` | — | Pinned to the bottom of the sidebar (user menu, workspace switcher) |\n| `topbar` | `ReactNode` | — | Right side of the top bar (search, notifications, avatar) |\n| `pageTitle` | `string` | — | Title shown in the mobile top bar |\n| `bottomNav` | `ReactNode` | — | Bottom navigation for phones, usually a [`FootNav`](FootNav.md). Rendered below `md` only (hidden from `md` up, where the sidebar / drawer take over). When set, a `FootNav.Spacer` is added after `<main>` so content doesn't scroll under the bar |\n| `linkAs` | `ElementType` | `\"a\"` | Link element for nav items, e.g. Next.js `Link` |\n| `navOpen` | `boolean` | — | Controls the mobile navigation drawer, e.g. so a `FootNav` \"More\" tab can open it. Unset, the shell's menu button owns it |\n| `onNavOpenChange` | `(open: boolean) => void` | — | Called when the drawer should open or close (menu button, close button, Escape, backdrop, choosing a nav item). Called in uncontrolled mode too |\n| `topbarFrom` | `\"always\"` \\| `\"below-lg\"` | `\"always\"` | `\"below-lg\"` hides the top bar from `lg` up, where the sidebar carries the brand and nav, so a desktop page has no empty strip |\n| `className` | `string` | — | Extra CSS classes for the root (merged with `cn`) |\n| `children` | `ReactNode` | — | Page content, rendered in `<main id=\"main\">` |\n| ...rest | `HTMLAttributes<HTMLDivElement>` | — | Forwarded to the root `<div>`; the ref is forwarded to it |\n\n## Example\n\n```jsx\n<AppShell\n brand={<span className=\"text-lg font-bold\">PracticePro</span>}\n nav={[\n { id: 'overview', label: 'Overview', href: '/overview', icon: <Home className=\"h-5 w-5\" /> },\n { id: 'compliance', label: 'Compliance', href: '/compliance', active: true },\n { id: 'approvals', label: 'Approvals', href: '/approvals', count: 4 },\n ]}\n topbar={<Avatar name=\"Ananya Rao\" size=\"sm\" />}\n linkAs={Link}\n>\n <PageHeader title=\"Compliance calendar\" />\n</AppShell>\n```\n\n### Bottom nav on phones\n\n```jsx\n<AppShell\n brand={<Logo />}\n nav={nav}\n linkAs={Link}\n bottomNav={\n <FootNav\n linkAs={Link}\n items={[\n { id: 'home', label: 'Home', icon: <Home />, href: '/', active: true },\n { id: 'tasks', label: 'Tasks', icon: <CheckSquare />, href: '/tasks', badge: 3 },\n { id: 'inbox', label: 'Inbox', icon: <Inbox />, href: '/inbox' },\n ]}\n />\n }\n>\n {page}\n</AppShell>\n```\n\nKeep the bottom nav to the 3–5 most used destinations; the full `nav` stays in the drawer (menu button) below `lg` and in the sidebar on desktop.\n\n### Opening the drawer from a \"More\" tab\n\nPass `navOpen` / `onNavOpenChange` to control the drawer, and `topbarFrom=\"below-lg\"` when the desktop page has no top bar:\n\n```jsx\nconst [navOpen, setNavOpen] = useState(false);\n\n<AppShell\n brand={<Logo />}\n nav={nav}\n navOpen={navOpen}\n onNavOpenChange={setNavOpen}\n topbarFrom=\"below-lg\"\n bottomNav={\n <FootNav\n items={[\n { id: 'today', label: 'Today', icon: <Home />, href: '/', active: true },\n { id: 'more', label: 'More', icon: <Menu />, onClick: () => setNavOpen(true) },\n ]}\n />\n }\n>\n {page}\n</AppShell>\n```\n\n## Mobile drawer accessibility\n\nBelow `lg`, the \"Open navigation\" button (`aria-haspopup=\"dialog\"`, `aria-expanded`) opens the sidebar in a left [`Sheet`](Sheet.md) (`role=\"dialog\"`, `aria-modal=\"true\"`, `aria-label=\"Navigation\"`), coloured with the shell tokens. While it is open, focus moves into the drawer, Tab / Shift+Tab stay inside it, **Escape** closes it and returns focus to the menu button, and body scroll is locked. The lock is ref-counted and shared with `Dialog`, `Drawer`, `Sheet` and `Sidebar`, so stacked overlays release the page only when the last one closes. Clicking the backdrop, the close button or a nav item, or dragging the drawer to the left, also closes it. It slides in and out (instant under `prefers-reduced-motion`), so it stays mounted for its short exit transition. The menu, close and nav controls grow to 44px on coarse pointers and show a `focus-visible` ring.\n\n[← Component overview](README.md)\n",
|
|
@@ -7218,7 +7218,7 @@
|
|
|
7218
7218
|
"LocationPicker": "# LocationPicker\n\nInput for latitude/longitude. User can type `lat, lng` (e.g. `12.34, 56.78`) or click the icon to use browser geolocation. Value is `{ lat, lng }` or `null`. The input uses the semantic `bg-surface` / `border-line` / `text-foreground` tokens, so it follows dark mode.\n\n## Import\n\n```jsx\nimport { LocationPicker } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `{ lat: number, lng: number }` \\| `null` | `null` | Current coordinates. |\n| `onChange` | `(coords: { lat, lng } \\| null) => void` | — | Called when value changes (typed or from geolocation). |\n| `inputProps` | `object` | `{}` | Props spread to the underlying input (e.g. `placeholder`). A supplied `inputProps.onChange` is forwarded **in addition to** (not instead of) the component's coord-parsing handler — both fire. `inputProps.className` is merged via `cn()`. |\n| `className` | `string` | — | Classes merged (via `cn()`) onto the root wrapper. |\n| `icon` | `ReactNode` | default pin SVG | Icon for \"use my location\" button. |\n| `iconProps` | `object` | `{}` | Props for the icon button (e.g. `className`, `onClick`). |\n| `iconPosition` | `\"left\"` \\| `\"right\"` | `\"right\"` | Position of the icon button. |\n| `placeholder` | `string` | `\"lat, lng\"` | Input placeholder. |\n| `fullWidth` | `boolean` | `false` | Stretch the root and input to the container width (drops the input's 200 px minimum). |\n\n## Behavior\n\n- Display format is `lat.toFixed(6), lng.toFixed(6)`.\n- Typing a valid \"lat, lng\" string calls `onChange` with `{ lat, lng }` (e.g.\n typing `\"1.5, 2.5\"` calls `onChange({ lat: 1.5, lng: 2.5 })`). A\n consumer-supplied `inputProps.onChange` still fires alongside it.\n- Clearing the input calls `onChange(null)`.\n- Clicking the icon calls `navigator.geolocation.getCurrentPosition` and passes the result to `onChange`. Requires HTTPS in production.\n- **Loading:** while the request is in flight the button shows a spinner (still\n under `prefers-reduced-motion`), has `aria-busy=\"true\"`, ignores repeat clicks,\n and a \"Finding your location…\" `role=\"status\"` line appears under the input.\n- **Errors** are shown in the UI as a `role=\"alert\"` line under the input (the\n input border turns `error`): permission denied, position unavailable, timeout,\n or geolocation unsupported — each with its own message. A later success clears\n it. The error is still logged with `console.error` as before. The status / error\n line is linked to the input and button with `aria-describedby`.\n- The location button grows to a 44×44 px touch target on coarse pointers, has a\n `focus-visible` ring and press feedback. The input renders at 16 px on touch\n screens (no iOS zoom) and has dark-mode styles.\n- Forwards `ref` to the underlying `<input>` and spreads unknown props (`...rest`) onto the root wrapper.\n\n## Example\n\n```jsx\nconst [location, setLocation] = useState(null);\n<LocationPicker\n value={location}\n onChange={setLocation}\n placeholder=\"Enter lat, lng or use location\"\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
7219
7219
|
"MIGRATION-2.0": "# Migrating to 2.0\n\n2.0.0 is the one breaking release planned in the [UI/UX and mobile roadmap](../docs/roadmap/uiux-mobile-2026.md) (epic E6). It removes the APIs that 1.x deprecated and renames the icon props that broke the shared `leftIcon` / `rightIcon` vocabulary. Nothing else changes: there are no new peer dependencies, the Tailwind setup is the same, and no default behaviour was flipped.\n\n## Upgrade in three steps\n\n1. Install the new version:\n\n ```bash\n npm install @dreamtree-org/twreact-ui@^2.0.0\n ```\n\n2. Run the codemod over your source folder. It rewrites JSX on components imported from `@dreamtree-org/twreact-ui` and prints the spots it can't fix by itself:\n\n ```bash\n npx jscodeshift -t node_modules/@dreamtree-org/twreact-ui/codemods/v2.cjs src/\n ```\n\n The codemod parses `.js`, `.jsx`, `.ts` and `.tsx` files (its default parser is `tsx`). Add `--extensions=js,jsx,ts,tsx` if jscodeshift skips your files, `--dry --print` to preview the result, or `--parser=babel` for Flow code.\n\n3. Fix each line the codemod reports (lines that start with `REP`), then type-check. The TypeScript types drop every removed prop, so `tsc` flags any the codemod couldn't see, such as props passed through a spread.\n\n## What the codemod does\n\n| Change | Automated | Manual |\n|---|---|---|\n| [Input `icon` / `iconPosition`](#input-icon--iconposition--lefticon--righticon) | `icon` becomes `leftIcon`, or `rightIcon` with `iconPosition=\"right\"`; a literal `iconPosition` is removed | A non-literal `iconPosition={pos}` is reported |\n| [`Tabs.Tab` `iconLeft` / `iconRight`](#tabstab-iconleft--iconright) | Renamed to `leftIcon` / `rightIcon` | None |\n| [Input clear](#input-clear-no-longer-fires-onchange) | None | A `clearable` Input with `value` but no `onClear` is reported. Add `onClear` |\n| [ProgressBar HEX colours](#progressbar-hex-colour-props) | `color`, `bgColor` and `valueColor` are removed | Reported. Pick a `variant` if the bar wasn't primary |\n| [Checkbox `borderColor`](#checkbox-bordercolor) | Removed | None |\n| [Rate `activeColor` / `inactiveColor`](#rate-activecolor-and-inactivecolor) | Removed | None |\n| [Checkbox / Switch raw presets](#checkbox-and-switch-raw-colour-presets) | None | `color=\"blue\"`, `\"indigo\"` or `\"purple\"` is reported. Pick a token |\n\nThe codemod only sees literal JSX attributes. It doesn't follow props passed through a spread (`<Input {...props} />`), components re-exported from your own wrapper files, or `React.createElement` calls. The TypeScript types and the [removed props table](#removed-props-at-a-glance) cover those cases.\n\n## Renamed props\n\n### Input `icon` / `iconPosition` → `leftIcon` / `rightIcon`\n\nInput used one `icon` slot plus `iconPosition`. It now uses the same two slots as `Button`, so you can also set both at once.\n\n```jsx\n// 1.x\n<Input icon={<Search />} />\n<Input icon={<Mail />} iconPosition=\"right\" />\n\n// 2.0\n<Input leftIcon={<Search />} />\n<Input rightIcon={<Mail />} />\n<Input leftIcon={<IndianRupee />} rightIcon={<Info />} /> // new: both together\n```\n\nThe behaviour is the same as before. `rightIcon` hides while the password toggle or the clear button shows, and `type=\"search\"` still draws its own search icon on the left.\n\n**Codemod:** automated when `iconPosition` is missing, `\"left\"` or `\"right\"`. If `iconPosition` is an expression, the codemod reports it and you choose the slot yourself:\n\n```jsx\n// 1.x\n<Input icon={icon} iconPosition={end ? \"right\" : \"left\"} />\n// 2.0\n<Input leftIcon={end ? undefined : icon} rightIcon={end ? icon : undefined} />\n```\n\n### `Tabs.Tab` `iconLeft` / `iconRight`\n\n```jsx\n// 1.x\n<Tabs.Tab iconLeft={<Home />}>Home</Tabs.Tab>\n// 2.0\n<Tabs.Tab leftIcon={<Home />}>Home</Tabs.Tab>\n```\n\n**Codemod:** fully automated, including `UI.Tabs.Tab` when you use a namespace import.\n\n## Behaviour change\n\n### Input clear no longer fires `onChange`\n\nIn 1.x the clear button of a controlled `Input` called `onChange({ target: { value: \"\" } })`. That fake event had no `type`, no `currentTarget` and no `preventDefault`, so handlers that read anything beyond `target.value` broke. In 2.0 the clear button only calls `onClear`, and a controlled field clears its own `value` there.\n\n```jsx\n// 1.x: worked because clear sent a synthetic onChange\n<Input clearable value={q} onChange={(e) => setQ(e.target.value)} />\n\n// 2.0\n<Input\n clearable\n value={q}\n onChange={(e) => setQ(e.target.value)}\n onClear={() => setQ(\"\")}\n/>\n```\n\nUncontrolled fields (`defaultValue`, or no value) still clear themselves, and focus still returns to the input. `Form` text fields keep working: `Form` now passes `onClear` itself and resets the react-hook-form value, then calls the field's own `onClear` if you set one. `SearchField` has its own clear and isn't affected.\n\n**Codemod:** reports every `clearable` Input that has a `value` but no `onClear`. Add the handler yourself, because only you know which state to reset.\n\n## Removed props\n\n### ProgressBar HEX colour props\n\n`color`, `bgColor` and `valueColor` took HEX strings and painted the bar inline, which ignored the brand tokens and dark mode. They are gone. Use the new `variant` prop, a token tone for both the fill and the track.\n\n```jsx\n// 1.x\n<ProgressBar value={80} color=\"#16a34a\" bgColor=\"#dcfce7\" />\n// 2.0\n<ProgressBar value={80} variant=\"success\" />\n```\n\n| `variant` | Fill | Track |\n|---|---|---|\n| `primary` (default) | `bg-primary-600` | `bg-primary-100` / `dark:bg-primary-900/40` |\n| `secondary` | `bg-secondary-600` | `bg-secondary-100` / `dark:bg-secondary-900/40` |\n| `success` | `bg-success-600` | `bg-success-100` / `dark:bg-success-900/40` |\n| `warning` | `bg-warning-600` | `bg-warning-100` / `dark:bg-warning-900/40` |\n| `destructive` | `bg-error-600` | `bg-error-100` / `dark:bg-error-900/40` |\n\nThe value text inside the fill is always `text-white`. For a colour that isn't one of these tones, set it on the brand (`tokens/brands/<name>.json`) rather than on one bar.\n\n**Codemod:** removes the three props and reports each ProgressBar it changed, so you can add a `variant`.\n\n### Checkbox `borderColor`\n\nThe box border is always the `border-gray-300` / `dark:border-gray-600` token.\n\n```jsx\n// 1.x\n<Checkbox label=\"Accept\" borderColor=\"#9ca3af\" />\n// 2.0\n<Checkbox label=\"Accept\" />\n```\n\n**Codemod:** removes the prop.\n\n### Rate `activeColor` and `inactiveColor`\n\nStars always use `text-warning-400` (active) and `text-gray-200` (inactive), so a brand file recolours them.\n\n```jsx\n// 1.x\n<Rate value={4} activeColor=\"#ef4444\" inactiveColor=\"#fee2e2\" />\n// 2.0\n<Rate value={4} />\n```\n\n**Codemod:** removes both props.\n\n### Checkbox and Switch raw colour presets\n\nThe `blue`, `indigo` and `purple` presets used the raw Tailwind palette (`bg-blue-600`, …) instead of tokens. They are removed. The token presets remain: `primary`, `secondary`, `success`, `error` and `warning`, plus the aliases `green` (success) and `danger` / `red` (error).\n\n```jsx\n// 1.x\n<Checkbox color=\"indigo\" label=\"Remember me\" />\n<Switch color=\"blue\" />\n// 2.0\n<Checkbox color=\"primary\" label=\"Remember me\" />\n<Switch color=\"primary\" />\n```\n\n**Heads-up:** `color` still accepts any CSS colour string as an inline override, and `blue`, `indigo` and `purple` are CSS colour names. So the old values don't throw an error: they now paint the CSS colour inline (pure `#0000ff` and so on) with no dark-mode tone. Replace them.\n\n**Codemod:** reports every literal `color=\"blue\" | \"indigo\" | \"purple\"` on Checkbox and Switch. It doesn't pick a token for you, because the right tone depends on your brand.\n\n### Internal: `computePosition`\n\nThe internal `computePosition` helper in `src/utils/position.js` was never exported from the package. It was unused after every overlay moved to `useFloatingPosition` (floating-ui). It is deleted. Consumers are only affected if they deep-imported library source files, which isn't supported.\n\n## Removed props at a glance\n\n| Component | 1.x prop | 2.0 |\n|---|---|---|\n| `Input` | `icon` | `leftIcon` / `rightIcon` |\n| `Input` | `iconPosition` | Choose `leftIcon` or `rightIcon` |\n| `Input` | clear → synthetic `onChange` | `onClear` (controlled: clear `value` yourself) |\n| `Tabs.Tab` | `iconLeft` / `iconRight` | `leftIcon` / `rightIcon` |\n| `ProgressBar` | `color`, `bgColor`, `valueColor` (HEX) | `variant` |\n| `Checkbox` | `borderColor` | Removed (token border) |\n| `Checkbox` | `color=\"blue\" \\| \"indigo\" \\| \"purple\"` | A token preset |\n| `Switch` | `color=\"blue\" \\| \"indigo\" \\| \"purple\"` | A token preset |\n| `Rate` | `activeColor`, `inactiveColor` | Removed (token colours) |\n\nA removed prop that is still passed falls through `...rest` to the DOM element, where React logs an \"unknown prop\" warning in development. Treat those warnings as a list of the props left to clean up.\n\n## What did not change\n\n- **No default flips.** No 1.x ticket or changelog entry promised a 2.0 default change, so every default stays as it was. Examples are `presentation=\"auto\"`, `Sidebar swipeable`, `Slider showBubble` and the opt-in `haptics`.\n- **Sizes** already used the shared `xs | sm | md | lg | xl` scale. No size was renamed.\n- **Exports:** no component, hook or util was added, renamed or removed.\n\n[← Component overview](README.md)\n",
|
|
7220
7220
|
"MobileNativeFeel": "# Mobile native feel and PWA guide\n\nHow to make an app built with `@dreamtree-org/twreact-ui` feel like a native app on phones and when installed as a PWA. The components already handle safe areas, the dynamic viewport, sheets, touch targets and reduced motion. Your app still has to add the page-level parts: the viewport meta, `theme-color`, the web manifest and a few layout rules. This guide covers both, then ends with a checklist for testing on real devices.\n\n## 1. Viewport meta\n\nPut this in your HTML `<head>` (in Next.js, export a `viewport` object from the root layout with the same values):\n\n```html\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n```\n\n- `viewport-fit=cover` lets the page draw under the notch and the home indicator, and makes `env(safe-area-inset-*)` return real values. Without it, `pt-safe`, `pb-safe` and the padding inside `AppShell`, `FootNav`, `Sheet` and `BottomActionBar` all resolve to `0`.\n- Don't add `maximum-scale=1` or `user-scalable=no` to stop iOS zooming into focused inputs. That blocks pinch-zoom, which people with low vision need. The library's stylesheet prevents the focus zoom another way (see [Global mobile baseline](#9-global-mobile-baseline)).\n\n## 2. Theme colour per theme\n\n`theme-color` tints the browser toolbar on Android Chrome and the status bar in an installed PWA. Give each colour scheme its own meta tag:\n\n```html\n<meta name=\"theme-color\" content=\"#ffffff\" media=\"(prefers-color-scheme: light)\">\n<meta name=\"theme-color\" content=\"#111827\" media=\"(prefers-color-scheme: dark)\">\n```\n\n`#ffffff` / `#111827` are the default `surface` token (light / dark), which is the colour of the `AppShell` top bar (`bg-surface`). If your brand file changes `surface`, or your top bar uses a different colour, use that colour here.\n\n### Keep it in sync with `ThemeProvider`\n\n`ThemeProvider` stores an explicit `'light'` or `'dark'` choice and writes `data-theme`, the `.dark` class and `color-scheme` on `<html>`. It does not follow the OS setting. So a user can pick dark mode on a phone set to light, and the media-query tags above would then show the wrong toolbar colour. Rewrite the tags whenever the theme changes. Read the colour from the `--dt-surface` token variable (stored as space-separated RGB channels), so the tag also follows a brand file:\n\n```jsx\nimport { useEffect } from 'react';\nimport { ThemeProvider, useTheme } from '@dreamtree-org/twreact-ui';\n\nfunction ThemeColorSync() {\n const { theme } = useTheme();\n useEffect(() => {\n const rgb = getComputedStyle(document.documentElement).getPropertyValue('--dt-surface').trim();\n if (!rgb) return;\n document\n .querySelectorAll('meta[name=\"theme-color\"]')\n .forEach((meta) => meta.setAttribute('content', `rgb(${rgb})`));\n }, [theme]);\n return null;\n}\n\nexport function Root({ children }) {\n return (\n <ThemeProvider defaultTheme=\"light\">\n <ThemeColorSync />\n {children}\n </ThemeProvider>\n );\n}\n```\n\n`useTheme()` throws outside a `ThemeProvider`, so render `ThemeColorSync` inside it. Keep the static tags in the HTML anyway: they give the right colour before JavaScript runs.\n\n## 3. Web manifest\n\nA manifest makes the app installable (\"Add to Home Screen\" / \"Install app\") and opens it without browser UI:\n\n```json\n{\n \"name\": \"PracticePro\",\n \"short_name\": \"PracticePro\",\n \"start_url\": \"/\",\n \"scope\": \"/\",\n \"display\": \"standalone\",\n \"background_color\": \"#f8fafc\",\n \"theme_color\": \"#ffffff\",\n \"icons\": [\n { \"src\": \"/icons/icon-192.png\", \"sizes\": \"192x192\", \"type\": \"image/png\" },\n { \"src\": \"/icons/icon-512.png\", \"sizes\": \"512x512\", \"type\": \"image/png\" },\n { \"src\": \"/icons/icon-maskable-512.png\", \"sizes\": \"512x512\", \"type\": \"image/png\", \"purpose\": \"maskable\" }\n ]\n}\n```\n\n```html\n<link rel=\"manifest\" href=\"/manifest.webmanifest\">\n<link rel=\"apple-touch-icon\" href=\"/icons/apple-touch-icon.png\">\n```\n\n- `display: \"standalone\"` removes the address bar and browser toolbars.\n- `background_color` is the splash screen colour; `#f8fafc` is the default `background` token (the page background), so the splash matches the first frame.\n- The manifest's `theme_color` has no light/dark variants. The `<meta name=\"theme-color\">` tags win once the page loads.\n- iOS uses `apple-touch-icon` for the home-screen icon. To draw the page under the iOS status bar in standalone mode, add `<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">`. Your top bar then needs `pt-safe`, which `AppShell` already has.\n- Offline support (a service worker) is up to you; the library ships none.\n\n## 4. Styling for standalone mode\n\nInstalled apps have no browser back button or address bar. Use `@media (display-mode: standalone)` for anything that should change:\n\n```css\n/* Show an in-app back button only when there is no browser UI. */\n.app-back-button { display: none; }\n\n@media (display-mode: standalone) {\n .app-back-button { display: inline-flex; }\n /* Native apps don't let you select UI chrome by accident. */\n .app-chrome { user-select: none; -webkit-user-select: none; }\n}\n```\n\nIn Tailwind 3.4 you can write it inline as an arbitrary variant: `hidden [@media(display-mode:standalone)]:inline-flex`. The library has no `standalone:` variant of its own; add one to your config if you use it a lot. In JavaScript, check `window.matchMedia('(display-mode: standalone)').matches` (and `navigator.standalone` on older iOS).\n\n## 5. Use `dvh`, not `100vh`\n\nOn phones, `100vh` is the height with the browser toolbars hidden, so a `h-screen` layout is taller than the visible area and its bottom (often the main action) ends up under the toolbar. Use the dynamic viewport units, which Tailwind 3.4 ships:\n\n| Instead of | Use |\n|------------|-----|\n| `h-screen`, `height: 100vh` | `h-dvh` (`height: 100dvh`) |\n| `min-h-screen` | `min-h-dvh` |\n| `max-h-[80vh]` | `max-h-[80dvh]` |\n\nThe library already uses them: `AppShell` fills `min-h-dvh` (its sidebar is `h-dvh`), side `Sheet`s are `h-dvh`, and `Dialog`, `Drawer`, `Tour` and the toast stack cap their height in `dvh`. If you only need a fixed full-height page and the content must never resize while the toolbar moves, `h-svh` (the smallest viewport) is the steadier choice.\n\n## 6. The app shell recipe: `AppShell` + `FootNav` + `Sheet`\n\n[`AppShell`](AppShell.md) gives you a sidebar on desktop, a top bar with a navigation drawer below `lg`, and a phone tab bar via `bottomNav`. Pass a [`FootNav`](FootNav.md) there: it renders below `md` only, and `AppShell` adds the `FootNav.Spacer` after `<main>` for you. Open quick tasks in a bottom [`Sheet`](Sheet.md) from a FAB item:\n\n```jsx\nimport { useState } from 'react';\nimport { Link } from 'react-router-dom';\nimport { AppShell, FootNav, Sheet, Button, Input } from '@dreamtree-org/twreact-ui';\nimport { Home, CheckSquare, Plus, Inbox } from 'lucide-react';\n\nexport function Layout({ path, children }) {\n const [adding, setAdding] = useState(false);\n\n return (\n <AppShell\n brand={<span className=\"text-lg font-bold\">PracticePro</span>}\n nav={[\n { id: 'home', label: 'Home', href: '/', active: path === '/' },\n { id: 'tasks', label: 'Tasks', href: '/tasks', active: path === '/tasks' },\n { id: 'inbox', label: 'Inbox', href: '/inbox', active: path === '/inbox' },\n ]}\n linkAs={Link}\n pageTitle=\"Tasks\"\n data-sheet-background\n bottomNav={\n <FootNav\n linkAs={Link}\n hideOnScroll\n items={[\n { id: 'home', label: 'Home', icon: <Home />, to: '/', active: path === '/' },\n { id: 'tasks', label: 'Tasks', icon: <CheckSquare />, to: '/tasks', active: path === '/tasks', badge: 3 },\n {\n id: 'add',\n label: 'Add',\n icon: <Plus />,\n variant: 'action',\n 'aria-haspopup': 'dialog',\n 'aria-expanded': adding,\n onClick: () => setAdding(true),\n },\n { id: 'inbox', label: 'Inbox', icon: <Inbox />, to: '/inbox', active: path === '/inbox' },\n ]}\n />\n }\n >\n {children}\n\n <Sheet open={adding} onClose={() => setAdding(false)} title=\"New task\" snapPoints={[0.5, 1]} scaleBackground>\n <Sheet.Body>\n <Input label=\"Title\" />\n </Sheet.Body>\n <Sheet.Footer>\n <Button onClick={() => setAdding(false)}>Save</Button>\n </Sheet.Footer>\n </Sheet>\n </AppShell>\n );\n}\n```\n\n- Keep `bottomNav` to the 3–5 most used destinations; the full `nav` stays in the drawer and the sidebar.\n- `data-sheet-background` goes to the `AppShell` root (it forwards `...rest`), so `scaleBackground` can scale the page behind the sheet, as iOS does. It does nothing under reduced motion.\n- Only render a `FootNav.Spacer` yourself when you use `FootNav` without `AppShell`.\n- For a form's primary action, use [`BottomActionBar`](BottomActionBar.md) (with `<BottomActionBar.Spacer />`) rather than a hand-built `fixed bottom-0` bar: it pads the safe area and rides above the on-screen keyboard.\n- Other native patterns ship as components: [`PullToRefresh`](PullToRefresh.md), [`SwipeableListItem`](SwipeableListItem.md), [`SearchField`](SearchField.md), [`SegmentedControl`](SegmentedControl.md) and [`ActionSheet`](ActionSheet.md).\n\n## 7. Overlays: `presentation` overrides\n\nOn small touch screens (`(pointer: coarse) and (max-width: 639px)`), these components switch to a phone presentation by default (`presentation=\"auto\"`):\n\n| Component | Phone presentation |\n|-----------|--------------------|\n| [`Select`](Select.md), [`DatePicker`](DatePicker.md), [`DateRangePicker`](DateRangePicker.md), [`Popover`](Popover.md), [`SearchField`](SearchField.md) results | Bottom `Sheet` |\n| [`Dialog`](Dialog.md), [`ConfirmDialog`](ConfirmDialog.md) | Bottom `Sheet` instead of the centred modal |\n| [`ThreeDotPopover`](ThreeDotPopover.md), the [`Table`](Table.md) row menu | Action sheet |\n| [`Tour`](Tour.md) | Step card docked at the bottom |\n\nEverywhere else, and on the server and while hydrating, they render the desktop popover or modal. Each takes `presentation=\"auto\" | \"popover\" | \"sheet\"`. Override it only for a reason:\n\n- **Force `popover`** when the content needs the context around it: a small `Popover` next to the control it adjusts, a sort menu that should not cover the list, a `Tour` step that must point at its target, or a `Dialog` that a tablet-sized coarse-pointer screen should keep centred.\n- **Force `sheet`** when the content is long or full of touch controls on every screen size: a filter panel, a long `Select` list, a multi-field quick form.\n- Leave `auto` alone otherwise. The breakpoint matches the one the rest of the library uses, so all overlays switch together.\n\n`Tooltip` has no sheet presentation (it opens on long-press on touch screens); use `Popover` for interactive content.\n\n## 8. Safe areas\n\nWith `viewport-fit=cover` set, the stylesheet's utilities pad for the notch, rounded corners and home indicator:\n\n| Class | Pads |\n|-------|------|\n| `pt-safe`, `pb-safe`, `pl-safe`, `pr-safe` | One edge (`env(safe-area-inset-*)`) |\n| `px-safe` | Left and right |\n| `py-safe` | Top and bottom |\n| `inset-safe` | Sets `top` / `right` / `bottom` / `left` to the insets (for positioned elements) |\n\nAlready padded, so don't add them again: the `AppShell` top bar (`pt-safe px-safe`), sidebar (`pt-safe pb-safe pl-safe`) and content column (`px-safe`); `FootNav` (`pb-safe`); `BottomActionBar` (`pb-safe px-safe`); `Sheet` (bottom: `pb-safe px-safe`, top: `pt-safe px-safe`, sides: `py-safe` plus the outer edge).\n\nAdd them yourself to any edge bar you build: a custom sticky header gets `pt-safe`, a full-bleed bottom element `pb-safe`, and in landscape any full-width row `px-safe`.\n\n## 9. Global mobile baseline\n\nImporting the library's CSS gives every page this baseline (from `src/styles/index.css`):\n\n- **No tap highlight.** `-webkit-tap-highlight-color: transparent` on `:root`; components show their own pressed and `focus-visible` states instead. `text-size-adjust: 100%` stops iOS from enlarging text in landscape.\n- **No zoom on input focus.** On coarse pointers, text `input`s, `select`s and `textarea`s are at least 16px (`font-size: max(16px, 1em) !important`, so it beats `text-sm`). iOS Safari zooms into fields with smaller text. Checkboxes, radios, ranges, colour and file inputs are not affected. Add `dt-keep-font-size` to a field to opt it out.\n- **No rubber-band overscroll.** `body` has `overscroll-behavior-y: none`, so the page doesn't bounce or chain scroll, and the browser's own pull-to-refresh doesn't fight [`PullToRefresh`](PullToRefresh.md). To get the browser's pull-to-refresh back, add `dt-overscroll-auto` to `<html>` or `<body>`.\n- **`color-scheme`** follows the theme (`light` by default, `dark` under `.dark` / `[data-theme=\"dark\"]`), so native scrollbars and form controls match.\n- **Reduced motion.** Under `prefers-reduced-motion: reduce`, animations and transitions are cut to near zero, and components such as `Sheet`, `FootNav` and `Dialog` skip their slides.\n- Touch targets in the components grow to 44px on coarse pointers.\n\n## 10. Haptics are opt-in\n\nNothing vibrates unless you ask. [`useHaptics`](useHaptics.md) wraps `navigator.vibrate` with named patterns (`selection`, `success`, `warning`, `error`), and `Switch`, `Checkbox`, `ConfirmDialog` and `Sheet` take a `haptics` prop that defaults to `false`:\n\n```jsx\n{/* Each alongside its usual props (open, onClose, onConfirm, …) */}\n<Switch haptics rightLabel=\"Notifications\" />\n<ConfirmDialog haptics destructive />\n<Sheet haptics snapPoints={[0.5, 1]} />\n```\n\n- iOS Safari has no vibration API, so haptics never fire on iPhones. Treat them as a bonus on Android, never as the only feedback.\n- Calls are no-ops on the server, while hydrating, under reduced motion and before the user's first interaction.\n- To let users turn haptics off, pass your setting as `useHaptics({ enabled })` or as the components' `haptics` prop.\n\n## 11. Real-device testing checklist\n\nEmulators and desktop responsive mode miss most of these. Test on at least one iPhone (iOS Safari) and one Android phone (Chrome), in the browser and installed to the home screen.\n\n**Safe area**\n- [ ] On a notched iPhone, nothing sits under the notch or the Dynamic Island in portrait, and nothing hides under the rounded corners in landscape.\n- [ ] The `FootNav` labels and `BottomActionBar` buttons sit above the home indicator.\n- [ ] A bottom `Sheet`'s last row isn't covered by the home indicator.\n\n**No zoom on input focus**\n- [ ] Tapping an `Input`, `Select`, `Textarea` or `SearchField` does not zoom the page on iOS Safari.\n- [ ] Pinch-zoom still works (the viewport meta doesn't block it).\n- [ ] With the keyboard open, the `BottomActionBar` and a bottom `Sheet` stay above it.\n\n**Sheet swipe**\n- [ ] Dragging a bottom `Sheet` (handle, header or body scrolled to the top) follows the finger and dismisses past the threshold; a short drag snaps back.\n- [ ] With `snapPoints`, the sheet settles on each snap point.\n- [ ] Scrolling inside `Sheet.Body` scrolls the content, not the page behind it.\n- [ ] The left navigation drawer of `AppShell` closes when dragged left.\n\n**No horizontal scroll at 375px**\n- [ ] At 375px wide (iPhone SE / mini), no page scrolls sideways. Wide tables, long words and fixed-width elements are the usual cause.\n- [ ] Five `FootNav` tabs fit, and long labels truncate.\n\n**Standalone mode**\n- [ ] The app installs from Android Chrome (\"Install app\") and iOS Safari (Share → Add to Home Screen) with the right icon and name.\n- [ ] It opens without browser UI, and the status bar / toolbar colour matches the theme in light and dark (including after switching with `ThemeProvider`).\n- [ ] The splash screen colour matches the first frame.\n- [ ] Every screen has a way back without a browser back button, and links within `scope` stay in the app.\n\n**General**\n- [ ] Pressing a button gives visual feedback with no grey tap flash.\n- [ ] Overlays open as sheets on the phone and as popovers / modals on a desktop browser.\n- [ ] With Reduce Motion on (iOS Settings → Accessibility → Motion; Android Remove animations), sheets and the tab bar appear without sliding.\n\n[← Component overview](README.md)\n",
|
|
7221
|
-
"Navbar": "# Navbar\n\nTop navigation bar with logo, nav items (links or buttons), optional search, notifications dropdown, and login/logout. Uses an **emitter** for notification actions (view, markAsRead, markAsUnread, clearAll, delete). Responsive: left icon and hamburger on mobile; desktop nav and search. You can replace the right area with a custom component via `rightMenuContent`. The bar, search box and dropdown panels use the semantic `bg-surface` / `border-line` / `text-foreground` tokens, so they follow dark mode.\n\n## Import\n\n```jsx\nimport { Navbar } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `emitter` | `object` | **required** | Event emitter for notifications (e.g. `notification:view`, `notification:markAsRead`, etc.). |\n| `logo` | `ReactNode` | — | Logo (e.g. image or link). Truncates on narrow screens. |\n| `center` | `ReactNode` | — | Centre slot (e.g. a search field or primary action) between the logo and the right-hand icons. Rendered in a `min-w-0 flex-1` column so it shrinks first on narrow screens; give its content a fluid width such as `w-full max-w-md`. |\n| `items` | `Array` | `[]` | Nav items: `{ id, label, href?, active?, onClick?(item) }`. If `href` is set, renders `<a>`; else `<button>` with `onClick`. |\n| `user` | `object` | — | `{ name, email, avatar?, menuItems? }`. If set, shows the user menu and a logout button; if omitted, a login button. Without `avatar`, the menu trigger shows `name` initials (via `Avatar`). |\n| `notifications` | `Array` | `[]` | List of notifications for the dropdown. Each: `{ id, title, message?, read? }`. |\n| `searchable` | `boolean` | `false` | Show the search input from `sm` up. Below `sm` a **Search** icon button opens a top `Sheet` holding a [`SearchField`](SearchField.md) instead (same term, same `onSearch`). |\n| `onSearch` | `(term: string) => void` | — | Called with the current term on search form submit (desktop) or Enter in the phone search sheet (the sheet then closes). |\n| `sticky` | `boolean` | `false` | Sticks the bar to the top (`sticky top-0 z-sticky`), pads it with the top safe area (`pt-safe`) and makes it translucent (`bg-surface/80 backdrop-blur` where `backdrop-filter` is supported; opaque `bg-surface` otherwise). |\n| `largeTitle` | `ReactNode` | — | iOS large title: an `<h1>` under the bar that collapses into a compact centred title in the bar once the window scrolls past 44px (and expands again under 8px). Passive scroll listener, SSR-safe (expanded on the server), instant under `prefers-reduced-motion`. |\n| `leftIcon` | `ReactNode` \\| `false` | `<ChevronLeft />` | Icon shown on mobile (e.g. back). Set `false` to hide. |\n| `onLeftIconClick` | `(event) => void` | — | Called when left icon (mobile) is clicked. |\n| `leftIconLabel` | `string` | `\"Go back\"` | Accessible name of the left button. Set it when the button is not \"back\", e.g. `\"Open navigation\"` when it opens a `Sidebar` drawer. |\n| `leftIconProps` | `ButtonHTMLAttributes` | — | Extra props spread onto the left `<button>` — e.g. `{ \"aria-expanded\": open, \"aria-controls\": \"app-sidebar\" }` when it toggles a drawer. `className` merges last; `onClick` runs after `onLeftIconClick`. |\n| `rightMenuContent` | `(props) => ReactNode` | — | Custom right menu.
|
|
7221
|
+
"Navbar": "# Navbar\n\nTop navigation bar with logo, nav items (links or buttons), optional search, notifications dropdown, and login/logout. Uses an **emitter** for notification actions (view, markAsRead, markAsUnread, clearAll, delete). Responsive: left icon and hamburger on mobile; desktop nav and search. You can replace the right area with a custom component via `rightMenuContent`. The bar, search box and dropdown panels use the semantic `bg-surface` / `border-line` / `text-foreground` tokens, so they follow dark mode.\n\n## Import\n\n```jsx\nimport { Navbar } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `emitter` | `object` | **required** | Event emitter for notifications (e.g. `notification:view`, `notification:markAsRead`, etc.). |\n| `logo` | `ReactNode` | — | Logo (e.g. image or link). Truncates on narrow screens. |\n| `center` | `ReactNode` | — | Centre slot (e.g. a search field or primary action) between the logo and the right-hand icons. Rendered in a `min-w-0 flex-1` column so it shrinks first on narrow screens; give its content a fluid width such as `w-full max-w-md`. |\n| `items` | `Array` | `[]` | Nav items: `{ id, label, href?, active?, onClick?(item) }`. If `href` is set, renders `<a>`; else `<button>` with `onClick`. |\n| `user` | `object` | — | `{ name, email, avatar?, menuItems? }`. If set, shows the user menu and a logout button; if omitted, a login button. Without `avatar`, the menu trigger shows `name` initials (via `Avatar`). |\n| `notifications` | `Array` | `[]` | List of notifications for the dropdown. Each: `{ id, title, message?, read? }`. |\n| `searchable` | `boolean` | `false` | Show the search input from `sm` up. Below `sm` a **Search** icon button opens a top `Sheet` holding a [`SearchField`](SearchField.md) instead (same term, same `onSearch`). |\n| `onSearch` | `(term: string) => void` | — | Called with the current term on search form submit (desktop) or Enter in the phone search sheet (the sheet then closes). |\n| `sticky` | `boolean` | `false` | Sticks the bar to the top (`sticky top-0 z-sticky`), pads it with the top safe area (`pt-safe`) and makes it translucent (`bg-surface/80 backdrop-blur` where `backdrop-filter` is supported; opaque `bg-surface` otherwise). |\n| `largeTitle` | `ReactNode` | — | iOS large title: an `<h1>` under the bar that collapses into a compact centred title in the bar once the window scrolls past 44px (and expands again under 8px). Passive scroll listener, SSR-safe (expanded on the server), instant under `prefers-reduced-motion`. |\n| `leftIcon` | `ReactNode` \\| `false` | `<ChevronLeft />` | Icon shown on mobile (e.g. back). Set `false` to hide. |\n| `onLeftIconClick` | `(event) => void` | — | Called when left icon (mobile) is clicked. |\n| `leftIconLabel` | `string` | `\"Go back\"` | Accessible name of the left button. Set it when the button is not \"back\", e.g. `\"Open navigation\"` when it opens a `Sidebar` drawer. |\n| `leftIconProps` | `ButtonHTMLAttributes` | — | Extra props spread onto the left `<button>` — e.g. `{ \"aria-expanded\": open, \"aria-controls\": \"app-sidebar\" }` when it toggles a drawer. `className` merges last; `onClick` runs after `onLeftIconClick`. |\n| `rightMenuContent` | `ReactNode` \\| `(props) => ReactNode` | — | Custom right menu. An element is rendered as is; a function receives `{ notifications }` and any Navbar `...props` (e.g. `emitter`, `onFetch`). If omitted or `null`, the default notifications dropdown renders. |\n| `onLoginClick` | `() => void` | — | Called when the login icon button (accessible name \"Sign in\") is clicked. |\n| `onLogoutClick` | `() => void` | — | Called when the logout icon button (accessible name \"Sign out\") or the user-menu **Sign out** item is clicked. |\n| `onFetch` | `({ setNotifications }) => void` | — | Refresh handler for the default notifications dropdown. Called on mount and when `notifications` changes. Consumed by the right menu — it is **not** forwarded onto `<nav>`. |\n| `className` | `string` | — | Wrapper CSS classes. |\n| ...rest | — | — | Only DOM-valid attributes (e.g. `id`, `aria-*`, `data-*`, event handlers) are forwarded to the root `<nav>`. The component's own control props (`isMobileOpen`, `setIsMobileOpen`, `onFetch`, …) are consumed and never leak to the DOM. |\n\n`Navbar` forwards `ref` to its root `<nav>` element.\n\n## Touch targets and labels\n\nIcon buttons (back, search, notifications, sign in/out, menu, user menu) are 36px on desktop and grow to the 44×44px touch target on coarse pointers, with a `focus-visible` ring. Accessible names: **Go back** (the left button; rename it with `leftIconLabel`), **Search**, **Notifications** (or **Notifications, 3 unread**), **Sign in**, **Sign out**, **Open menu** / **Close menu** (with `aria-expanded`) and **User menu**.\n\nThe unread badge grows with its count (`min-w-4` + padding) and caps at **99+**. It is `aria-hidden`; the count is in the bell's name.\n\n## Sticky bar and large title\n\n```jsx\n<Navbar emitter={emitter} logo={<Logo />} sticky largeTitle=\"Invoices\" searchable onSearch={search} />\n```\n\nThe large title collapses with the window scroll, so use it on pages that scroll the document (not an inner scroll container). Add `viewport-fit=cover` to your viewport meta for the safe-area inset.\n\n## Small screens (≤ 390px)\n\nThe header fits a 390px phone: columns use `gap` + `min-w-0` so the logo\ntruncates and the `center` slot shrinks instead of overflowing. Below the `sm`\nbreakpoint the standalone logout icon is hidden and a **Sign out** item appears\nat the bottom of the user menu instead (it calls `onLogoutClick`). With\n`searchable`, the inline search box is replaced by a **Search** button that\nopens the search sheet. From `sm` up the layout is unchanged.\n\n## Centre slot and initials avatar\n\n```jsx\n<Navbar\n emitter={emitter}\n logo={<Logo />}\n center={<Input type=\"search\" placeholder=\"Search tasks...\" className=\"w-full max-w-md\" />}\n user={{ name: 'Ada Lovelace', email: 'ada@example.com' }} // no avatar → \"AL\"\n/>\n```\n\n## Item shape (items)\n\n- `id`: string or number (key).\n- `label`: string.\n- `href`: optional; if set, item is an `<a href={href}>`.\n- `active`: optional boolean; applies primary styling.\n- `onClick(item)`: optional; used when no `href` (button click).\n\n## Notification shape (notifications)\n\n- `id`: string or number.\n- `title`: string.\n- `message`: optional string.\n- `read`: optional boolean; unread count uses `!n.read`.\n\n## Default notifications dropdown\n\nIf `rightMenuContent` is not provided, the right menu shows a bell icon and dropdown with:\n\n- Unread count badge.\n- \"Clear all\" that clears the list and calls emitter `notification:clearAll`.\n- Per notification: title, message, \"Mark as Read\" / \"Mark as Unread\", \"Delete\" (emitter events).\n- Clicking a notification calls `emitter.emit('notification:view', [n])`.\n\nTo refresh the list from the server, pass `onFetch({ setNotifications })`; the default dropdown uses it on mount and when `notifications` prop changes.\n\n## Example\n\n```jsx\nimport { Navbar } from '@dreamtree-org/twreact-ui';\nimport MyEmitter from './emitter';\n\n<Navbar\n emitter={MyEmitter}\n logo={<a href=\"/\"><img src=\"/logo.svg\" alt=\"Home\" /></a>}\n items={[\n { id: '1', label: 'Home', href: '/', active: pathname === '/' },\n { id: '2', label: 'About', href: '/about' },\n { id: '3', label: 'Settings', onClick: () => openSettings() },\n ]}\n user={user}\n notifications={notifications}\n searchable\n onSearch={(term) => doSearch(term)}\n onLoginClick={() => navigate('/login')}\n onLogoutClick={logout}\n onFetch={async ({ setNotifications }) => {\n const res = await fetch('/api/notifications');\n const data = await res.json();\n setNotifications(data.items);\n }}\n/>\n```\n\n### Left button as the Sidebar menu button\n\n```jsx\nimport { Menu } from 'lucide-react';\n\n<Navbar\n emitter={MyEmitter}\n leftIcon={<Menu />}\n leftIconLabel=\"Open navigation\"\n leftIconProps={{ 'aria-expanded': drawerOpen, 'aria-controls': 'app-sidebar' }}\n onLeftIconClick={() => setDrawerOpen(true)}\n/>\n<Sidebar id=\"app-sidebar\" items={items} isMobileOpen={drawerOpen} setIsMobileOpen={setDrawerOpen} />\n```\n\n[← Component overview](README.md)\n",
|
|
7222
7222
|
"OptionCard": "# OptionCard\n\nSelectable choice tile for **rich choices**: page templates (with a small wireframe), \"add a block\" pickers, starter kits. Each tile has a title, a description, an optional media slot, an optional badge and a selected state (primary border and ring **plus** a check indicator, so it never relies on colour alone). Wrap tiles in `OptionCardGroup` for a responsive grid with `role=\"radiogroup\"` semantics, or `multiple` for checkbox-group semantics. On its own an `OptionCard` is a toggle button (`aria-pressed`). For short text-only choices use `SegmentedControl` or `Radio` instead.\n\n## Import\n\n```jsx\nimport { OptionCard, OptionCardGroup } from '@dreamtree-org/twreact-ui';\n// OptionCard.Group is the same component as OptionCardGroup.\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `string` | — | The value this tile stands for (a string or number). Required inside an `OptionCardGroup`, where it is compared with the group's `value`; passed to `onSelect` everywhere. |\n| `title` | `ReactNode` | — | Tile heading. Also the tile's accessible name (`aria-labelledby`). |\n| `description` | `ReactNode` | — | Secondary text under the title (`text-muted`). Also the tile's accessible description (`aria-describedby`). |\n| `media` | `ReactNode` | — | Optional visual above the text, e.g. a small wireframe SVG or an image. Clipped to rounded corners. |\n| `badge` | `ReactNode` | — | Optional small pill next to the title, e.g. `\"New\"` or `\"Pro\"`. |\n| `selected` | `boolean` | `false` | Standalone only: whether the tile is pressed (controlled). Ignored inside a group, which derives it from its `value`. |\n| `onSelect` | `(value, event) => void` | — | Called when the tile is activated (click, `Enter` or `Space`), standalone or inside a group. Not called while disabled. |\n| `disabled` | `boolean` | `false` | Dims the tile, sets `aria-disabled`, removes it from the tab order and keyboard navigation, and ignores activation. |\n| `children` | `ReactNode` | — | Extra content rendered below the description. |\n| `className` | `string` | — | Merged onto the tile with `cn`, so your classes win on conflict. |\n\nOther props (`id`, `data-*`, event handlers, …) go to the tile's root `<div>` (`role=\"radio\"`, `\"checkbox\"` or `\"button\"`), which also receives the forwarded `ref`. Your `onClick` / `onKeyDown` run first; call `event.preventDefault()` to stop the tile from selecting.\n\n## OptionCardGroup props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `value` | `string \\| string[]` | — | Controlled selection: a single value, or an array of values with `multiple`. |\n| `defaultValue` | `string \\| string[]` | — | Uncontrolled initial selection. Without `value` or `defaultValue` nothing is selected and the first enabled tile is the tab stop. |\n| `onChange` | `(value) => void` | — | Receives the new value (single) or the new array (`multiple`), not a DOM event. In single mode it is not called when the selected tile is activated again. |\n| `multiple` | `boolean` | `false` | Multi-select: the root becomes `role=\"group\"`, tiles become `role=\"checkbox\"`, and `value` is an array. |\n| `columns` | `number` | `3` | `1`–`4`. Always one column on phones; two from `sm`, then `columns` from `lg`. |\n| `label` | `ReactNode` | — | Visible label above the grid, wired up as the group's `aria-labelledby`. |\n| `aria-label` | `string` | — | Accessible name when there is no visible `label`. Pass `label`, `aria-label` or `aria-labelledby`. |\n| `name` | `string` | — | Submits the selection with a form through hidden inputs (one per selected value). |\n| `disabled` | `boolean` | `false` | Disable every tile (dimmed, `aria-disabled` on the root and on each tile). |\n| `className` | `string` | — | Merged onto the root with `cn`. |\n\nOther props go to the root `<div role=\"radiogroup\">` (or `role=\"group\"`), which receives the forwarded `ref`. Tiles connect to their group through React context, so they can sit at any depth inside it.\n\n## Behavior\n\n- **Keyboard (group):** one tab stop (roving tabindex): the last focused tile, else the selected tile, else the first enabled tile. `ArrowRight` / `ArrowDown` and `ArrowLeft` / `ArrowUp` move to the next / previous tile in DOM order, skip disabled tiles and wrap around; `Home` / `End` jump to the first / last enabled tile. Under `dir=\"rtl\"` the left and right arrows swap. In a single-select group the arrows also **select** (standard radio behaviour); in a `multiple` group they only move focus. `Space` / `Enter` select the focused tile (toggle it with `multiple`).\n- **Keyboard (standalone):** the tile is a focusable `role=\"button\"` with `aria-pressed`; `Enter` / `Space` activate it.\n- **Styling:** `bg-surface` tile with a `border-line` border; selected tiles get a primary border, a `ring-1` primary ring, a light primary tint (dark-mode aware) and a filled check indicator in the top-right corner (round for radios, square for checkboxes). Tiles are at least 44px tall, lift on hover, show a `focus-visible` ring and shrink slightly while pressed. Transitions and the press effect are off under `prefers-reduced-motion`.\n- **Media:** decorative wireframes should carry `aria-hidden=\"true\"`; the tile's name comes from `title`.\n\n## Example\n\n```jsx\nconst [template, setTemplate] = useState('classic');\n\n<OptionCardGroup value={template} onChange={setTemplate} columns={3} aria-label=\"Template\">\n <OptionCard value=\"classic\" title=\"Classic\" description=\"Filters beside the grid\" media={<Sketch />} />\n <OptionCard value=\"stacked\" title=\"Stacked\" description=\"Filters above the grid\" badge=\"New\" />\n <OptionCard value=\"minimal\" title=\"Minimal\" description=\"No filters\" disabled />\n</OptionCardGroup>\n\n<OptionCardGroup multiple label=\"Blocks to add\" defaultValue={['hero']} onChange={setBlocks} columns={2}>\n <OptionCard value=\"hero\" title=\"Hero\" description=\"Large heading with a call to action\" />\n <OptionCard value=\"faq\" title=\"FAQ\" description=\"Collapsible questions\" />\n</OptionCardGroup>\n\n<OptionCard value=\"starter\" title=\"Starter kit\" description=\"Pages and blocks to begin with\" selected={on} onSelect={() => setOn(!on)} />\n```\n\n[← Component overview](README.md)\n",
|
|
7223
7223
|
"PageHeader": "# PageHeader\n\nPage-level block: the page title (`h1`), a muted subtitle, the page's actions (one primary action) and an optional back link. Extracted from taskpro `features/shared/PageHeader.jsx`.\n\n## Import\n\n```jsx\nimport { PageHeader } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `title` | `ReactNode` | — | Page title, rendered as the page's only `h1` |\n| `subtitle` | `ReactNode` | — | One line under the title |\n| `actions` | `ReactNode` | — | Buttons on the right; keep one primary action |\n| `actionsPlacement` | `'inline' \\| 'bottom'` | `\"inline\"` | `inline`: actions next to the title. `bottom`: on phones (coarse pointer and ≤639px) the actions are pinned in a [`BottomActionBar`](BottomActionBar.md) (`fullWidth`) at the bottom of the screen; on desktop, and on the server until hydration, they stay inline |\n| `back` | `{ href: string; label: string }` | — | Back link for detail pages. Grows to a 44px touch target on coarse pointers; `focus-visible` ring |\n| `linkAs` | `ElementType` | `\"a\"` | Link element for the back link |\n| `className` | `string` | — | Extra CSS classes (merged with `cn`) |\n| ...rest | `HTMLAttributes<HTMLDivElement>` | — | Forwarded to the root `<div>`; the ref is forwarded to it |\n\n## Example\n\n```jsx\n<PageHeader\n title=\"Compliance calendar\"\n subtitle=\"Track deadlines, ownership and filing readiness across clients\"\n actions={<Button leftIcon={<Plus className=\"h-4 w-4\" />}>Add obligation</Button>}\n/>\n```\n\n### Actions at the bottom on phones\n\n```jsx\n<PageHeader\n title=\"New invoice\"\n back={{ href: '/invoices', label: 'Invoices' }}\n actionsPlacement=\"bottom\"\n actions={\n <>\n <Button variant=\"outline\">Save draft</Button>\n <Button>Send invoice</Button>\n </>\n }\n/>\n{form}\n<BottomActionBar.Spacer className=\"sm:hidden\" />\n```\n\nWith `actionsPlacement=\"bottom\"`, put a `BottomActionBar.Spacer` at the end of the page so the bar doesn't cover the last fields. The actions render once (never duplicated), so ids and test queries stay unique.\n\n[← Component overview](README.md)\n",
|
|
7224
7224
|
"Pagination": "# Pagination\n\nPage navigation: first, previous, page numbers, next, last. Renders only when `totalPages > 1`. Optional `theme` for styling (e.g. from Table). Page buttons use the semantic `bg-surface` / `border-line` / `text-foreground` tokens, so they follow dark mode.\n\n## Import\n\n```jsx\nimport { Pagination } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `currentPage` | `number` | `1` | Current page (1-based). |\n| `totalPages` | `number` | `1` | Total pages. If ≤ 1, component returns null. |\n| `onPageChange` | `(page: number) => void` | — | Called when user selects a page. |\n| `showFirstLast` | `boolean` | `true` | Show first/last page buttons (and the `...` ellipses). The jump-to-first / jump-to-last button only renders when page 1 / the last page is outside the visible page window, so no page number ever appears twice. |\n| `showPrevNext` | `boolean` | `true` | Show previous/next buttons. |\n| `maxVisiblePages` | `number` | `5` | Max page number buttons to show. |\n| `className` | `string` | — | Nav wrapper CSS classes. |\n| ...rest | — | — | Passed to `<nav>`. |\n\n**Responsive:** below the `sm` breakpoint (640 px) the bar collapses to a compact `‹ Page 3 of 10 ›` layout. Only previous/next and the status text show; the page numbers, jump buttons and ellipses are hidden with CSS (`hidden sm:inline-flex`). From `sm` up the full layout is unchanged. When `showPrevNext={false}`, nothing would drive the compact view, so the full layout shows at every width. Buttons wrap instead of overflowing, grow to a 44 px touch target on coarse pointers, show a `focus-visible` ring, and have dark-mode colours.\n\n## Example\n\n```jsx\n<Pagination\n currentPage={page}\n totalPages={Math.ceil(total / pageSize)}\n onPageChange={setPage}\n maxVisiblePages={5}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
@@ -7249,7 +7249,7 @@
|
|
|
7249
7249
|
"Stepper": "# Stepper\n\nHorizontal or vertical step indicator. Each step has circle (with check for completed), optional line, and label. Optional `onStepClick` for clickable steps.\n\n## Import\n\n```jsx\nimport { Stepper } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `steps` | `Array` | `[]` | Each step: `{ id?, title?, label?, description? }`. `title` is the step's text; `label` is accepted as an alias (when both are set, `title` wins). |\n| `currentStep` | `number` | `0` | Zero-based index of current step. |\n| `orientation` | `\"horizontal\"` \\| `\"vertical\"` \\| `\"auto\"` | `\"horizontal\"` | Layout. `\"auto\"` stacks vertically below the `sm` breakpoint (640 px) and lays out horizontally from `sm` up, using CSS only. |\n| `variant` | `\"default\"` \\| `\"dots\"` | `\"default\"` | `\"dots\"` renders compact progress dots: one labelled dot per step, the current step as a wider pill, and no titles or descriptions. Clicks work as with `onStepClick`. Unknown values render the default stepper. |\n| `onStepClick` | `(step, stepIndex) => void` | — | Called when a step is clicked (e.g. for completed/current). |\n| `className` | `string` | — | Nav wrapper CSS classes. |\n| ...rest | — | — | Passed to `<nav>`. The nav defaults to `aria-label=\"Progress\"`; a caller's `aria-label` replaces it. |\n\n## Step status\n\n- **completed**: `stepIndex < currentStep` (check icon, primary color).\n- **current**: `stepIndex === currentStep`.\n- **upcoming**: `stepIndex > currentStep` (gray circle; title and description use the `text-muted` role, 7.5:1 on white).\n\nConnectors flex to fill the space between circles (horizontal) or between steps (vertical), so any number of steps fits its container without overflowing. The connector after a completed step is primary. Step buttons show a `focus-visible` ring, have dark-mode colours, and grow to a 44 px touch target on coarse pointers when they are clickable.\n\nEach step button (and each dot) is named `Step <n>: <title or label>`, with ` (completed)` for completed steps (their circle shows only a check icon).\n\n## Example\n\n```jsx\n<Stepper\n steps={[\n { id: '1', label: 'Details' },\n { id: '2', label: 'Payment' },\n { id: '3', label: 'Confirm' },\n ]}\n currentStep={1}\n orientation=\"horizontal\"\n onStepClick={(step, idx) => setStep(idx)}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
7250
7250
|
"SwipeableListItem": "# SwipeableListItem\n\nA list row with swipe actions, the iOS Mail / Gmail pattern. Swipe right to reveal the **leading** actions, left to reveal the **trailing** ones; release to snap open or closed, or swipe all the way to commit the first action on that side. Past the actions the row rubber-bands. Every action is also reachable without a gesture through a **More actions** button that opens them as a menu (an action sheet on phones).\n\n## Import\n\n```jsx\nimport { SwipeableListItem } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `leadingActions` | `SwipeAction[]` | `[]` | Revealed by swiping right. The first one sits at the left edge and is the full-swipe action |\n| `trailingActions` | `SwipeAction[]` | `[]` | Revealed by swiping left. The first one sits at the right edge and is the full-swipe action |\n| `fullSwipe` | `boolean` \\| `{ leading?, trailing? }` | `true` | A swipe past `fullSwipeThreshold` commits the first action of that side. Pass an object to allow it on one side only |\n| `fullSwipeThreshold` | `number` | `0.6` | Fraction of the row width a full swipe must travel (and at least 24px past the revealed actions) |\n| `actionWidth` | `number` | `80` | Width in px of each revealed action |\n| `open` | `'leading'` \\| `'trailing'` \\| `null` | — | Controlled open side |\n| `defaultOpen` | `'leading'` \\| `'trailing'` \\| `null` | `null` | Uncontrolled initial open side |\n| `onOpenChange` | `(side) => void` | — | Called with `'leading'`, `'trailing'` or `null` when the row opens or closes |\n| `disabled` | `boolean` | `false` | Turns off the gesture and the More actions button |\n| `moreActions` | `'focus'` \\| `'always'` \\| `false` | `'focus'` | The gesture-free button. `focus`: shown on keyboard focus and mouse hover. `always`: always visible. `false`: removed (only when you provide another way to reach the actions) |\n| `moreActionsLabel` | `string` | `\"More actions\"` | Accessible name of the button and its menu |\n| `menuPresentation` | `'auto'` \\| `'popover'` \\| `'sheet'` | `'auto'` | How the More actions menu opens. `auto` uses an action sheet on small touch screens |\n| `as` | `ElementType` | `\"div\"` | Root element, e.g. `\"li\"` inside a `<ul>` |\n| `contentClassName` | `string` | — | Classes for the sliding content layer (default background `bg-surface`) |\n| `className` | `string` | — | Root classes (merged with `cn`) |\n| `children` | `ReactNode` | — | The row content |\n| `...rest` | `HTMLAttributes` | — | Forwarded to the root; `ref` points at the root |\n\n### `SwipeAction`\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `key` | `string` | Unique key |\n| `label` | `ReactNode` | Visible text (also the menu item text) |\n| `icon` | `ReactNode` \\| `Component` | Shown above the label (`<Trash2 />` or `Trash2`) |\n| `tone` | `string` | `default`, `primary`, `secondary`, `success`, `warning`, `info`, `error` or `destructive`. Sets the action's background; `error` / `destructive` also mark it destructive in the menu |\n| `disabled` | `boolean` | Not selectable |\n| `onSelect` | `({ action, source }) => void` | Runs the action. `source` is `'button'` (a revealed action was tapped), `'full-swipe'` or `'menu'`. The row closes afterwards |\n\n## Behaviour\n\n- **Pointer events**, with `touch-action: pan-y` on the content: a swipe only starts once horizontal movement dominates, so vertical scrolling is never blocked.\n- **Snap**: on release the row opens when dragged past half of its actions (or flicked toward open) and closes otherwise.\n- **Full swipe**: while past the threshold the first action grows to fill the revealed area; releasing there commits it.\n- **Closing**: a tap outside the row, a tap on the open row (which does not activate the row), Escape, or selecting an action.\n- **No ghost clicks**: the click that ends a swipe is swallowed.\n- **Reduced motion**: snapping animates with the motion tokens and is instant under `prefers-reduced-motion`.\n\n## Accessibility\n\n- Actions that are not revealed are `aria-hidden` and out of the tab order; revealed actions are regular buttons.\n- The **More actions** button (`aria-haspopup=\"menu\"`) is in the tab order even when visually hidden (`moreActions=\"focus\"`) and appears on focus. It opens every action as a `role=\"menu\"` (arrow keys, Home/End, typeahead, Escape returns focus) or, on phones, as an action sheet.\n- Escape closes an open row.\n\n## Examples\n\n### Inbox row\n\n```jsx\nimport { Archive, Trash2, Flag, MailOpen } from 'lucide-react';\n\n<ul className=\"divide-y divide-line\">\n {messages.map((m) => (\n <SwipeableListItem\n key={m.id}\n as=\"li\"\n leadingActions={[{ key: 'read', label: 'Read', icon: MailOpen, tone: 'primary', onSelect: () => markRead(m) }]}\n trailingActions={[\n { key: 'delete', label: 'Delete', icon: Trash2, tone: 'destructive', onSelect: () => remove(m) },\n { key: 'flag', label: 'Flag', icon: Flag, tone: 'warning', onSelect: () => flag(m) },\n { key: 'archive', label: 'Archive', icon: Archive, tone: 'success', onSelect: () => archive(m) },\n ]}\n >\n <div className=\"px-4 py-3\">{m.subject}</div>\n </SwipeableListItem>\n ))}\n</ul>\n```\n\n### One row open at a time\n\n```jsx\nconst [open, setOpen] = useState(null); // { id, side }\n\n<SwipeableListItem\n open={open?.id === m.id ? open.side : null}\n onOpenChange={(side) => setOpen(side ? { id: m.id, side } : null)}\n trailingActions={actions}\n>\n …\n</SwipeableListItem>\n```\n\n### In a Table\n\n`Table` builds its mobile cards on this component through the `swipeActions` prop; see [Table](Table.md#mobile-cards-filters-and-swipe-actions).\n\n[← Component overview](README.md)\n",
|
|
7251
7251
|
"Switch": "# Switch\n\nToggle switch (on/off). Controlled or uncontrolled. Without an `id` prop, the id comes from `React.useId()`, so it is stable across server rendering and hydration.\n\n## Import\n\n```jsx\nimport { Switch } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `checked` | `boolean` | — | Controlled checked |\n| `defaultChecked` | `boolean` | `false` | Uncontrolled default |\n| `onChange` | `(checked: boolean) => void` | — | Receives the new boolean state (not a DOM event) |\n| `disabled` | `boolean` | `false` | Disable |\n| `size` | `string` | `\"md\"` | `sm`, `md`, `lg`. `md` takes the iOS 51×31 size on coarse pointers (touch screens). |\n| `color` | `string` | `\"primary\"` | Token preset (`primary`, `secondary`, `success`, `error`, `warning`; aliases `green`, `danger`, `red`) or a custom CSS colour string. The raw-palette presets `blue`, `indigo`, `purple` were **removed in 2.0.0** ([migration](MIGRATION-2.0.md#checkbox-and-switch-raw-colour-presets)). |\n| `name` | `string` | — | Form name |\n| `topLabel` | `string` | — | Label above |\n| `bottomLabel` | `string` | — | Label below |\n| `leftLabel` | `string` | — | Label left |\n| `rightLabel` | `string` | — | Label right |\n| `haptics` | `boolean` | `false` | Opt-in: a `selection` vibration (10ms) on every toggle (tap, drag or keyboard). No-op without `navigator.vibrate` (iOS Safari) and under reduced motion. See [useHaptics](useHaptics.md) |\n\n## Behavior\n\n- **Tap or drag:** a tap/click toggles; dragging the knob left or right (pointer events) sets off / on, iOS style. A drag toward the current state is a no-op, and a cancelled drag reverts. `Space` / `Enter` toggle from the keyboard.\n- The off track is a visible grey in light and dark mode; colour presets (`primary`, `success`, …) paint the on track.\n- Transitions are `transition-colors` / `transition-transform` and turn off under `prefers-reduced-motion`. Focus shows a `focus-visible` ring.\n\n## Example\n\n```jsx\n<Switch checked={enabled} onChange={checked => setEnabled(checked)} />\n<Switch defaultChecked rightLabel=\"Enable notifications\" />\n```\n\n[← Component overview](README.md)\n",
|
|
7252
|
-
"Table": "# Table\n\nData table with sorting, filtering, pagination, row selection, expandable details, and responsive mobile card view. The toolbar **Reload** button is tinted `bg-primary-100 text-primary-700` with `dark:bg-primary-900/40 dark:text-primary-300` in dark mode.\n\n## Import\n\n```jsx\nimport { Table } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `data` | `Array` | `[]` | Row data (array of objects) |\n| `columns` | `Array` | `[]` | Column definitions `[{ key, label, sortable?, isVisible?, editable?, type?, options?, required?, validate?, onRender?, render?, searchable?, wrap?, hideOnMobile? }]`. `searchable: false` excludes the column from the global search, `wrap: true` lets its cells wrap, `hideOnMobile: true` drops it from the mobile card view. `label` may be text or a node (an aligned or icon header); filter placeholders and validation messages use the label when it is text and the column `key` otherwise. |\n| `interactive` | `boolean` | `false` | Enable interactive mode with inline cell editing (double-click cell), keyboard shortcuts (`Tab`/`Shift+Tab` cell navigation, `Enter` to save modified changes, `Arrows`, `Escape`), primary Save button, outline Clear Changes button, constraint validation before save, and instant new row addition. |\n| `onSave` | `function` | — | Save callback in interactive mode `({ updatedRows, newRows, allData, tableData }) => Promise<allData | { data: allData } | void> | void`. Returning a dataset array or `{ data }`/`{ allData }` updates internal state with returned records. |\n| `onEdit` | `function` | — | Live edit callback fired when a cell value changes or row edit action is clicked `({ rowKey, colKey, newValue, row, action, isNew }) => void` |\n| `sortable` | `boolean` | `true` | Enable column sort. Each sortable header is a real `<button>` inside its `<th>` (Tab to it, Enter / Space to sort) and the `<th>` carries `aria-sort` (`none`, `ascending` or `descending`). |\n| `filterable` | `boolean` | `false` | Show a filter input per column. Client-side, each non-empty filter keeps rows whose value in that column contains the text (case-insensitive); filters combine with each other and with the global search. In `serverSide` mode nothing is filtered locally. |\n| `selectable` | `boolean` | `false` | Row selection checkboxes |\n| `pagination` | `boolean` | `false` | Enable pagination |\n| `pageSize` | `number` | `25` | Rows per page |\n| `onSort` | `function` | — | Sort callback |\n| `onFilter` | `function` | — | Filter callback |\n| `onFetch` | `function` | — | Server-side fetch (setData, setLoading, filters, page, limit, sort) |\n| `onFilterChange` | `function` | — | Filter change callback |\n| `onSelectionChange` | `function` | — | Selected rows callback |\n| `onRowClick` | `function` | — | Row click callback |\n| `hasDetails` | `boolean` or `(row, index) => boolean` | `false` | Expandable row details. Pass a predicate to give only matching rows an expand toggle; non-matching rows render an empty placeholder cell so columns stay aligned. |\n| `DetailsComponent` | `Component` | — | Component for expanded content |\n| `withAction` | `boolean` | `true` | Show actions column |\n| `onAction` | `function` | — | Action menu callback |\n| `actions` | `Array` or `(row, index) => Array` | — | Row actions. Entries are `{ name, label, icon, render?, onClick?, hidden?, disabled?, inline?, tone? }`; `tone: 'destructive'` (or `destructive: true`) styles a menu action in the error tone; `label`, `icon`, `hidden` and `disabled` each accept a value or `(row, index) => value`. `inline: true` renders the action as an icon button in the actions cell instead of inside the kebab menu; the kebab is hidden when no menu actions remain. Entries are merged onto the built-in Edit/Delete/View set by `name` (so `{ name: 'edit', onClick }` keeps the default label and icon, and `{ name: 'delete', hidden: true }` removes a default); unmatched entries are appended. Set `replaceDefaultActions` to opt out. |\n| `actionMenuPresentation` | `string` | `\"auto\"` | One of `auto`, `popover`, `sheet`. How the row kebab menu opens. `popover`: a floating menu portaled to `document.body` that flips/shifts to stay on screen (never clipped by the table's overflow). `sheet`: an iOS-style action sheet from the bottom with a separate Cancel button. `auto` uses the sheet on small touch screens (`(pointer: coarse) and (max-width: 639px)`) and the popover elsewhere. |\n| `replaceDefaultActions` | `boolean` | `false` | Replace the built-in Edit/Delete/View actions entirely instead of patching them by `name`. |\n| `showSerial` | `boolean` | `true` | Show row number column |\n| `cellClass` | `string` \\| `function` | — | Cell className |\n| `rowClass` | `string` \\| `function` | — | Row className |\n| `globalSearch` | `boolean` | `false` | Global search box. Works with or without `filterable`. Client-side, keeps rows where any value outside a `searchable: false` column contains the term (case-insensitive). In `serverSide` mode it only reports `filters.global`. |\n| `limitOptions` | `number[]` | `[10,25,50,100]` | Page size options |\n| `showLimitSelector` | `boolean` | `true` | Show page size selector |\n| `showReloadButton` | `boolean` | `true` | Show reload button. It is named \"Reload\" (`aria-label`) because its text label is hidden below the `sm` breakpoint |\n| `onReload` | `function` | — | Reload click handler |\n| `stripedRows` | `boolean` | `true` | Striped row background |\n| `theme` | `object` | see below | `stripedColors`, `rowHover`, `accentColor`, `menuBg`, `menuItemHover`. Defaults are dark-mode aware: `{ stripedColors: ['bg-surface', 'bg-background'], rowHover: 'bg-background', accentColor: 'bg-background', menuBg: 'bg-surface', menuItemHover: 'hover:bg-background' }`. A `stripedColors` entry may be a Tailwind `bg-*` class (applied as a class) or a CSS colour such as `'#f7fafc'` (applied as an inline background). `rowHover` is a `bg-*` token or a full hover class string. In the pagination controls, the shipped tokens (`bg-background`, `bg-surface`, `bg-gray-50`, `bg-gray-100`, `bg-primary-50`, `bg-primary-100`) map to static `hover:` literals; for any other colour pass the full class (e.g. `'hover:bg-gray-200'`) so your Tailwind build generates it — a bare unknown token is still prefixed with `hover:` at runtime for backward compatibility, but Tailwind can purge such a class. Pagination buttons grow to a 44×44 px touch target on coarse pointers. |\n| `serverSide` | `boolean` | `false` | Server-side pagination/sort/filter |\n| `totalRecords` | `number` | `0` | Total count (serverSide). Drives the page count and the \"start - end of total\" counter; when omitted the counter falls back to the rows received. |\n| `pageNumber` | `number` | — | Controlled page. Client-side, a page past the last one is clamped to the last page once rows exist. |\n| `onPageChange` | `(page: number) => void` | — | Called with the new page number when the user changes page, and when the table clamps a page that no longer exists (client-side). Not called for the reset to page 1 on a filter or page-size change. Consumed by the table — it is **not** forwarded onto the `<table>`. |\n| `responsiveBreakpoint` | `number` | `768` | px breakpoint for mobile cards: card mode while the viewport is narrower than this. Read with an SSR-safe media query (`not all and (min-width: <n>px)`), so client renders pick the right layout on the first render and follow resizes live. |\n| `defaultMobileView` | `boolean` | `false` | Layout used on the server and while hydrating, before the media query can be read. Set `true` on phone-first pages so server-rendered HTML starts as cards. |\n| `mobileFiltersPresentation` | `string` | `\"auto\"` | Card-mode filters (with `filterable` / `globalSearch`). `sheet`: a \"Filters\" button (with an active-filter count) opening a bottom `Sheet`; edits are a draft until **Apply**, **Clear** empties the draft. `inline`: the collapsible inline panel (the previous behaviour, live filtering). `auto` uses the sheet on small touch screens (`(pointer: coarse) and (max-width: 639px)`) and the inline panel elsewhere. |\n| `swipeActions` | `(row, index) => { leading?, trailing? } \\| null` | — | Swipe actions on the mobile cards, built on [`SwipeableListItem`](SwipeableListItem.md). Each side is an array of `{ key, label, icon?, tone?, disabled?, onSelect }`; swipe right for `leading`, left for `trailing`, a full swipe commits the first action of that side. Only one card is open at a time. Every action is also reachable without a gesture through the card's \"Swipe actions\" button. Return `null` (or empty arrays) for rows without swipe actions. |\n| `stickyHeader` | `boolean` \\| `string` \\| `number` | `false` | Keep the desktop header visible while the body scrolls. `true` sticks within the table's own scroll container; a CSS length (`\"24rem\"`) or a number (px) also caps that container's height so the rows scroll under the header. |\n| `paginationPosition` | `'top'` \\| `'bottom'` \\| `'both'` | `'both'` | Which pager renders when `pagination` is on. `'both'` keeps the compact pager in the toolbar and the \"Page N of M\" footer. `'bottom'` drops the toolbar pager but keeps the rest of the toolbar (reload, page size, result count). `'top'` keeps only the toolbar pager (so with `showToolbar={false}` there is no pager). Unknown values fall back to `'both'`. |\n| `showToolbar` | `boolean` | `true` | Show the toolbar above the table (reload, page size, result count, pager header) and the column picker. `false` hides all of it; the pager footer stays, and in `interactive` mode the add-row / Save / Clear controls still render. `globalSearch` is its own opt-in and is unaffected. |\n| `showColumnPicker` | `boolean` | `true` | Show the column-picker button in the header (accessible name \"Choose columns\", with `aria-expanded`). `false` also removes its (otherwise empty) column from every row. |\n| `wrapCells` | `boolean` | `false` | Let cell text wrap instead of staying on one line. A column's own `wrap` overrides it. |\n| `mobileMaxFields` | `number` | — | Cap the fields shown on each mobile card. Applied in column order, after `hideOnMobile` columns are removed. |\n\nOnly DOM-valid attributes in `...rest` (e.g. `id`, `aria-*`, `data-*`) are\nforwarded to the root `<table>`; the table's own control props (`onPageChange`,\n`serverSide`, `pagination`, the `on*` callbacks, …) are consumed and never leak\nto the DOM as unknown attributes.\n\n## Examples\n\n### Basic table\n\n```jsx\nconst columns = [\n { key: 'name', label: 'Name' },\n { key: 'email', label: 'Email' },\n];\nconst data = [\n { id: 1, name: 'Alice', email: 'alice@example.com' },\n { id: 2, name: 'Bob', email: 'bob@example.com' },\n];\n<Table data={data} columns={columns} />\n```\n\n### With pagination and selection\n\n```jsx\n<Table\n data={data}\n columns={columns}\n pagination\n pageSize={10}\n selectable\n onSelectionChange={(selected) => console.log(selected)}\n/>\n```\n\n### Server-side data\n\n```jsx\n<Table\n data={data}\n columns={columns}\n serverSide\n pagination\n totalRecords={total}\n onFetch={async ({ setData, setLoading, page, limit, sort }) => {\n setLoading(true);\n const res = await fetch(`/api/users?page=${page}&limit=${limit}`);\n const json = await res.json();\n setData(json.rows);\n setLoading(false);\n }}\n/>\n```\n\n### Filtering and page clamping (client-side)\n\nWithout `serverSide`, the table filters the rows itself:\n\n- **Global search** (`globalSearch`) applies on Enter or the search button,\n with or without `filterable`. Columns with `searchable: false` are skipped.\n- **Column filters** (`filterable`) apply as you type. Each one matches only its\n own column's raw value (not a `render` output).\n- All matches are case-insensitive \"contains\" tests, and every active filter\n must match (AND).\n- `onFilter` / `onFilterChange` still receive the whole `filters` object\n (`{ global?, [columnKey]: text }`) on every change.\n\nWhen the rows shrink (filtering, deleted rows, a new `data` prop) and the\ncurrent page no longer exists, the table shows the last page that does and\ncalls `onPageChange` with it. A parent that drives `pageNumber` can simply\nstore that value, and no `clampPage` helper is needed. The clamp waits while\nthere are no rows, so a page restored from the URL survives until the data\narrives.\n\nWith `serverSide`, none of this runs locally: filters go to `onFetch` /\n`onFilter` / `onFilterChange`, the rows shown are exactly the rows you pass,\nand keeping `pageNumber` within `totalRecords` is up to you.\n\n### Compact, read-only table\n\nHide the chrome for a dashboard card, keep id columns out of the search, let\nlong text wrap, and keep phone cards short.\n\n```jsx\nconst columns = [\n { key: 'id', label: 'ID', searchable: false, hideOnMobile: true },\n { key: 'client', label: 'Client' },\n { key: 'status', label: 'Status' },\n { key: 'notes', label: 'Notes', wrap: true },\n { key: 'owner', label: 'Owner' },\n];\n\n<Table\n data={rows}\n columns={columns}\n showToolbar={false}\n showSerial={false}\n withAction={false}\n mobileMaxFields={3}\n/>\n```\n\nOn a phone each card shows Client, Status and Notes. With no serial, selection,\nexpand toggle or actions, the card has no header strip either.\n\n`showToolbar={false}` does not change pagination: with `pagination` on, the pager\nfooter still renders. Hide only the picker with `showColumnPicker={false}`.\n\n### One pager instead of two\n\nWith `pagination` and the toolbar both on, the table shows a compact pager in the\ntoolbar and another under the rows. Keep the toolbar (reload, page size, result\ncount) and drop the top pager with `paginationPosition=\"bottom\"`:\n\n```jsx\n<Table data={rows} columns={columns} pagination paginationPosition=\"bottom\" />\n```\n\n`paginationPosition=\"top\"` does the opposite and keeps only the toolbar pager.\n\n### Conditional row details\n\n`hasDetails` accepts a predicate so only rows that actually have something to\nshow get an expand toggle. Rows that don't qualify still render the (empty)\ntoggle cell, so the columns stay aligned.\n\n```jsx\nconst ErrorDetails = ({ row }) => <pre>{row.stackTrace}</pre>;\n\n<Table\n data={jobs}\n columns={columns}\n hasDetails={(row) => row.status === 'failed'}\n DetailsComponent={ErrorDetails}\n/>\n```\n\n### Mobile cards, filters and swipe actions\n\nBelow `responsiveBreakpoint` rows render as cards. Card controls (expand\ntoggle, selection checkbox, inline actions, kebab) have 44×44 px hit areas.\nWith `filterable` on a phone, the filters open in a bottom sheet with an\nApply / Clear footer; pass `mobileFiltersPresentation=\"inline\"` to keep the\ninline panel.\n\n```jsx\n<Table\n data={messages}\n columns={columns}\n filterable\n defaultMobileView // server-rendered HTML starts as cards\n swipeActions={(row) => ({\n leading: [{ key: 'read', label: 'Read', icon: MailOpen, tone: 'primary', onSelect: () => markRead(row) }],\n trailing: [\n { key: 'delete', label: 'Delete', icon: Trash2, tone: 'destructive', onSelect: () => remove(row) },\n { key: 'flag', label: 'Flag', icon: Flag, tone: 'warning', onSelect: () => flag(row) },\n ],\n })}\n/>\n```\n\n### Sticky header\n\n```jsx\n<Table data={rows} columns={columns} stickyHeader=\"28rem\" />\n```\n\n### Row action menu\n\nThe kebab button (`aria-haspopup=\"menu\"`) opens the row's menu actions. On\ndesktop it is a `role=\"menu\"` popover: focus moves to the first action,\nArrowUp/ArrowDown (wrapping), Home/End and first-letter typeahead move between\nactions, Escape or Tab closes it and returns focus to the kebab, and a press\noutside or a page scroll closes it (pressing the kebab again closes it too). On phones (`actionMenuPresentation=\"auto\"`,\nthe default) the same actions open as an action sheet: 48px rows in a rounded\ncard, destructive actions in red, and a Cancel button below. Pass\n`actionMenuPresentation=\"popover\"` to keep the desktop menu everywhere.\n\n```jsx\n<Table\n data={users}\n columns={columns}\n actions={[{ name: 'delete', tone: 'destructive' }]}\n actionMenuPresentation=\"auto\"\n/>\n```\n\n### Conditional and inline actions\n\n`hidden` and `disabled` accept a `(row, index)` predicate, `label` and `icon`\naccept a `(row, index)` producer, and `inline: true` promotes an action out of\nthe kebab menu into the row itself. When no menu actions remain for a row, the\nkebab button is not rendered at all.\n\n```jsx\n<Table\n data={jobs}\n columns={columns}\n replaceDefaultActions\n actions={[\n { name: 'view', label: 'View', icon: <Eye size={16} />, inline: true },\n { name: 'edit', label: 'Edit', icon: <Edit size={16} />,\n disabled: (row) => row.locked },\n { name: 'retry', label: (row) => `Retry (${row.tries})`,\n hidden: (row) => row.status !== 'failed' },\n ]}\n onAction={({ action, row }) => handle(action.name, row)}\n/>\n```\n\nFor a large table, wrap a function-valued `actions` in `useCallback` — a fresh\narrow on every render defeats the per-row memoization.\n\n```jsx\nconst actions = useCallback(\n (row) => (row.role === 'admin' ? adminActions : userActions),\n []\n);\n```\n\nNote that because `icon` may be a `(row, index) => node` producer, passing a\nbare component reference (`icon: Edit`) calls it rather than rendering it. Pass\nan element instead: `icon: <Edit size={16} />`.\n\n### Patching the default actions\n\nEntries you supply are merged onto the built-in Edit/Delete/View set by `name`,\nso you can swap one handler without restating the rest. Unmatched names are\nappended after the defaults, and `hidden: true` removes a default outright.\n\n```jsx\n<Table\n data={users}\n columns={columns}\n actions={[\n { name: 'edit', onClick: ({ row }) => edit(row) }, // keeps default label + icon\n { name: 'delete', hidden: (row) => !row.canDelete },\n { name: 'invite', label: 'Invite', icon: <Mail size={16} /> }, // appended\n ]}\n/>\n```\n\nPass `replaceDefaultActions` to skip merging and use only your own set:\n\n```jsx\n<Table\n data={users}\n columns={columns}\n replaceDefaultActions\n actions={[{ name: 'approve', label: 'Approve' }]}\n/>\n```\n\n### Interactive Table Mode\n\nPass `interactive={true}` to enable inline cell editing (double-click a cell or press standard `'e'` key on a focused active cell to enter edit mode), instant record addition (at top or bottom), keyboard navigation (`Tab`/`Shift+Tab` to move between cells, `Escape` to exit edit mode, `Enter` to save modified changes), and constraint validation before saving.\n\n#### Interactive Props & Options\n\n- `interactive`: `boolean` (default: `false`) — Enables interactive mode.\n- `editKeyBinding`: `string | ((e) => boolean)` (default: `\"e\"`) — Standard key binding to enter cell edit mode on active focused cell.\n- `newRowPosition`: `\"bottom\" | \"top\"` (default: `\"bottom\"`) — Position where empty new rows are inserted (at the bottom or top of table dataset).\n- `addRowRef`: `React.MutableRefObject` — External ref populated with `(position?: \"top\" | \"bottom\") => void` to trigger adding a new row from external buttons.\n- `onInitAddRow`: `(addRowFn: (position?: \"top\" | \"bottom\") => void) => void` — Callback receiving the add-row function on component initialization.\n- `renderAddRowButton`: `({ addRow, newRowPosition }) => ReactNode` — Render prop for rendering a custom external add-row button in the table toolbar.\n\n#### Interactive Usage Example\n\n```jsx\nconst columns = [\n { key: 'name', label: 'Name', type: 'text', required: true },\n { key: 'role', label: 'Role', type: 'select', options: ['Admin', 'User'], required: true },\n { key: 'notes', label: 'Notes', type: 'textarea' },\n];\n\nconst addRowRef = useRef();\n\nreturn (\n <div>\n <button onClick={() => addRowRef.current?.(\"top\")}>\n Add Row at Top\n </button>\n\n <Table\n interactive\n editKeyBinding=\"e\"\n newRowPosition=\"top\"\n addRowRef={addRowRef}\n columns={columns}\n data={users}\n onSave={async ({ updatedRows, newRows, allData }) => {\n await api.saveUsers({ updatedRows, newRows });\n }}\n />\n </div>\n);\n```\n\n[← Component overview](README.md)\n",
|
|
7252
|
+
"Table": "# Table\n\nData table with sorting, filtering, pagination, row selection, expandable details, and responsive mobile card view. The toolbar **Reload** button is tinted `bg-primary-100 text-primary-700` with `dark:bg-primary-900/40 dark:text-primary-300` in dark mode. Column headers, mobile-card labels and filter labels use `text-primary-700` / `dark:text-primary-300`; mobile-card values use `text-primary-900` / `dark:text-foreground`.\n\n## Import\n\n```jsx\nimport { Table } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `data` | `Array` | `[]` | Row data (array of objects) |\n| `columns` | `Array` | `[]` | Column definitions `[{ key, label, sortable?, isVisible?, editable?, type?, options?, required?, validate?, onRender?, render?, searchable?, wrap?, hideOnMobile? }]`. `searchable: false` excludes the column from the global search, `wrap: true` lets its cells wrap, `hideOnMobile: true` drops it from the mobile card view. `label` may be text or a node (an aligned or icon header); filter placeholders and validation messages use the label when it is text and the column `key` otherwise. |\n| `interactive` | `boolean` | `false` | Enable interactive mode with inline cell editing (double-click cell), keyboard shortcuts (`Tab`/`Shift+Tab` cell navigation, `Enter` to save modified changes, `Arrows`, `Escape`), primary Save button, outline Clear Changes button, constraint validation before save, and instant new row addition. |\n| `onSave` | `function` | — | Save callback in interactive mode `({ updatedRows, newRows, allData, tableData }) => Promise<allData | { data: allData } | void> | void`. Returning a dataset array or `{ data }`/`{ allData }` updates internal state with returned records. |\n| `onEdit` | `function` | — | Live edit callback fired when a cell value changes or row edit action is clicked `({ rowKey, colKey, newValue, row, action, isNew }) => void` |\n| `sortable` | `boolean` | `true` | Enable column sort. Each sortable header is a real `<button>` inside its `<th>` (Tab to it, Enter / Space to sort) and the `<th>` carries `aria-sort` (`none`, `ascending` or `descending`). |\n| `filterable` | `boolean` | `false` | Show a filter input per column. Client-side, each non-empty filter keeps rows whose value in that column contains the text (case-insensitive); filters combine with each other and with the global search. In `serverSide` mode nothing is filtered locally. |\n| `selectable` | `boolean` | `false` | Row selection checkboxes |\n| `pagination` | `boolean` | `false` | Enable pagination |\n| `pageSize` | `number` | `25` | Rows per page |\n| `onSort` | `function` | — | Sort callback `(key, direction)`. Called once per sort, after the state update (not during render), so it may set parent state. |\n| `onFilter` | `function` | — | Filter callback `(filters)`. Called once per change, outside render, like `onSort`. |\n| `onFetch` | `function` | — | Server-side fetch (setData, setLoading, filters, page, limit, sort) |\n| `onFilterChange` | `function` | — | Filter change callback |\n| `onSelectionChange` | `function` | — | Selected rows callback |\n| `onRowClick` | `function` | — | Row click callback |\n| `hasDetails` | `boolean` or `(row, index) => boolean` | `false` | Expandable row details. Pass a predicate to give only matching rows an expand toggle; non-matching rows render an empty placeholder cell so columns stay aligned. |\n| `DetailsComponent` | `Component` | — | Component for expanded content |\n| `withAction` | `boolean` | `true` | Show actions column |\n| `onAction` | `function` | — | Action menu callback |\n| `actions` | `Array` or `(row, index) => Array` | — | Row actions. Entries are `{ name, label, icon, render?, onClick?, hidden?, disabled?, inline?, tone? }`; `tone: 'destructive'` (or `destructive: true`) styles a menu action in the error tone; `label`, `icon`, `hidden` and `disabled` each accept a value or `(row, index) => value`. `inline: true` renders the action as an icon button in the actions cell instead of inside the kebab menu; the kebab is hidden when no menu actions remain. Entries are merged onto the built-in Edit/Delete/View set by `name` (so `{ name: 'edit', onClick }` keeps the default label and icon, and `{ name: 'delete', hidden: true }` removes a default); unmatched entries are appended. Set `replaceDefaultActions` to opt out. |\n| `actionMenuPresentation` | `string` | `\"auto\"` | One of `auto`, `popover`, `sheet`. How the row kebab menu opens. `popover`: a floating menu portaled to `document.body` that flips/shifts to stay on screen (never clipped by the table's overflow). `sheet`: an iOS-style action sheet from the bottom with a separate Cancel button. `auto` uses the sheet on small touch screens (`(pointer: coarse) and (max-width: 639px)`) and the popover elsewhere. |\n| `replaceDefaultActions` | `boolean` | `false` | Replace the built-in Edit/Delete/View actions entirely instead of patching them by `name`. |\n| `showSerial` | `boolean` | `true` | Show row number column |\n| `cellClass` | `string` \\| `function` | — | Cell className |\n| `rowClass` | `string` \\| `function` | — | Row className |\n| `globalSearch` | `boolean` | `false` | Global search box. Works with or without `filterable`. Client-side, keeps rows where any value outside a `searchable: false` column contains the term (case-insensitive). In `serverSide` mode it only reports `filters.global`. |\n| `limitOptions` | `number[]` | `[10,25,50,100]` | Page size options |\n| `showLimitSelector` | `boolean` | `true` | Show page size selector |\n| `showReloadButton` | `boolean` | `true` | Show reload button. It is named \"Reload\" (`aria-label`) because its text label is hidden below the `sm` breakpoint |\n| `onReload` | `function` | — | Reload click handler |\n| `stripedRows` | `boolean` | `true` | Striped row background |\n| `theme` | `object` | see below | `stripedColors`, `rowHover`, `accentColor`, `menuBg`, `menuItemHover`. Defaults are dark-mode aware: `{ stripedColors: ['bg-surface', 'bg-background'], rowHover: 'bg-background', accentColor: 'bg-background', menuBg: 'bg-surface', menuItemHover: 'hover:bg-background' }`. A `stripedColors` entry may be a Tailwind `bg-*` class (applied as a class) or a CSS colour such as `'#f7fafc'` (applied as an inline background). `rowHover` is a `bg-*` token or a full hover class string. In the pagination controls, the shipped tokens (`bg-background`, `bg-surface`, `bg-gray-50`, `bg-gray-100`, `bg-primary-50`, `bg-primary-100`) map to static `hover:` literals; for any other colour pass the full class (e.g. `'hover:bg-gray-200'`) so your Tailwind build generates it — a bare unknown token is still prefixed with `hover:` at runtime for backward compatibility, but Tailwind can purge such a class. Pagination buttons grow to a 44×44 px touch target on coarse pointers. |\n| `serverSide` | `boolean` | `false` | Server-side pagination/sort/filter |\n| `totalRecords` | `number` | `0` | Total count (serverSide). Drives the page count and the \"start - end of total\" counter; when omitted the counter falls back to the rows received. |\n| `pageNumber` | `number` | — | Controlled page. Client-side, a page past the last one is clamped to the last page once rows exist. |\n| `onPageChange` | `(page: number) => void` | — | Called with the new page number when the user changes page, and when the table clamps a page that no longer exists (client-side). Not called for the reset to page 1 on a filter or page-size change. Consumed by the table — it is **not** forwarded onto the `<table>`. |\n| `responsiveBreakpoint` | `number` | `768` | px breakpoint for mobile cards: card mode while the viewport is narrower than this. Read with an SSR-safe media query (`not all and (min-width: <n>px)`), so client renders pick the right layout on the first render and follow resizes live. |\n| `defaultMobileView` | `boolean` | `false` | Layout used on the server and while hydrating, before the media query can be read. Set `true` on phone-first pages so server-rendered HTML starts as cards. |\n| `mobileFiltersPresentation` | `string` | `\"auto\"` | Card-mode filters (with `filterable` / `globalSearch`). `sheet`: a \"Filters\" button (with an active-filter count) opening a bottom `Sheet`; edits are a draft until **Apply**, **Clear** empties the draft. `inline`: the collapsible inline panel (the previous behaviour, live filtering). `auto` uses the sheet on small touch screens (`(pointer: coarse) and (max-width: 639px)`) and the inline panel elsewhere. |\n| `swipeActions` | `(row, index) => { leading?, trailing? } \\| null` | — | Swipe actions on the mobile cards, built on [`SwipeableListItem`](SwipeableListItem.md). Each side is an array of `{ key, label, icon?, tone?, disabled?, onSelect }`; swipe right for `leading`, left for `trailing`, a full swipe commits the first action of that side. Only one card is open at a time. Every action is also reachable without a gesture through the card's \"Swipe actions\" button. Return `null` (or empty arrays) for rows without swipe actions. |\n| `stickyHeader` | `boolean` \\| `string` \\| `number` | `false` | Keep the desktop header visible while the body scrolls. `true` sticks within the table's own scroll container; a CSS length (`\"24rem\"`) or a number (px) also caps that container's height so the rows scroll under the header. |\n| `paginationPosition` | `'top'` \\| `'bottom'` \\| `'both'` | `'both'` | Which pager renders when `pagination` is on. `'both'` keeps the compact pager in the toolbar and the \"Page N of M\" footer. `'bottom'` drops the toolbar pager but keeps the rest of the toolbar (reload, page size, result count). `'top'` keeps only the toolbar pager (so with `showToolbar={false}` there is no pager). Unknown values fall back to `'both'`. |\n| `showToolbar` | `boolean` | `true` | Show the toolbar above the table (reload, page size, result count, pager header) and the column picker. `false` hides all of it; the pager footer stays, and in `interactive` mode the add-row / Save / Clear controls still render. `globalSearch` is its own opt-in and is unaffected. |\n| `showColumnPicker` | `boolean` | `true` | Show the column-picker button in the header (accessible name \"Choose columns\", with `aria-expanded`). `false` also removes its (otherwise empty) column from every row. |\n| `wrapCells` | `boolean` | `false` | Let cell text wrap instead of staying on one line. A column's own `wrap` overrides it. |\n| `mobileMaxFields` | `number` | — | Cap the fields shown on each mobile card. Applied in column order, after `hideOnMobile` columns are removed. |\n\nOnly DOM-valid attributes in `...rest` (e.g. `id`, `aria-*`, `data-*`) are\nforwarded to the root `<table>`; the table's own control props (`onPageChange`,\n`serverSide`, `pagination`, the `on*` callbacks, …) are consumed and never leak\nto the DOM as unknown attributes.\n\n## Examples\n\n### Basic table\n\n```jsx\nconst columns = [\n { key: 'name', label: 'Name' },\n { key: 'email', label: 'Email' },\n];\nconst data = [\n { id: 1, name: 'Alice', email: 'alice@example.com' },\n { id: 2, name: 'Bob', email: 'bob@example.com' },\n];\n<Table data={data} columns={columns} />\n```\n\n### With pagination and selection\n\n```jsx\n<Table\n data={data}\n columns={columns}\n pagination\n pageSize={10}\n selectable\n onSelectionChange={(selected) => console.log(selected)}\n/>\n```\n\n### Server-side data\n\n```jsx\n<Table\n data={data}\n columns={columns}\n serverSide\n pagination\n totalRecords={total}\n onFetch={async ({ setData, setLoading, page, limit, sort }) => {\n setLoading(true);\n const res = await fetch(`/api/users?page=${page}&limit=${limit}`);\n const json = await res.json();\n setData(json.rows);\n setLoading(false);\n }}\n/>\n```\n\n### Filtering and page clamping (client-side)\n\nWithout `serverSide`, the table filters the rows itself:\n\n- **Global search** (`globalSearch`) applies on Enter or the search button,\n with or without `filterable`. Columns with `searchable: false` are skipped.\n- **Column filters** (`filterable`) apply as you type. Each one matches only its\n own column's raw value (not a `render` output).\n- All matches are case-insensitive \"contains\" tests, and every active filter\n must match (AND).\n- `onFilter` / `onFilterChange` still receive the whole `filters` object\n (`{ global?, [columnKey]: text }`) on every change.\n\nWhen the rows shrink (filtering, deleted rows, a new `data` prop) and the\ncurrent page no longer exists, the table shows the last page that does and\ncalls `onPageChange` with it. A parent that drives `pageNumber` can simply\nstore that value, and no `clampPage` helper is needed. The clamp waits while\nthere are no rows, so a page restored from the URL survives until the data\narrives.\n\nWith `serverSide`, none of this runs locally: filters go to `onFetch` /\n`onFilter` / `onFilterChange`, the rows shown are exactly the rows you pass,\nand keeping `pageNumber` within `totalRecords` is up to you.\n\n### Compact, read-only table\n\nHide the chrome for a dashboard card, keep id columns out of the search, let\nlong text wrap, and keep phone cards short.\n\n```jsx\nconst columns = [\n { key: 'id', label: 'ID', searchable: false, hideOnMobile: true },\n { key: 'client', label: 'Client' },\n { key: 'status', label: 'Status' },\n { key: 'notes', label: 'Notes', wrap: true },\n { key: 'owner', label: 'Owner' },\n];\n\n<Table\n data={rows}\n columns={columns}\n showToolbar={false}\n showSerial={false}\n withAction={false}\n mobileMaxFields={3}\n/>\n```\n\nOn a phone each card shows Client, Status and Notes. With no serial, selection,\nexpand toggle or actions, the card has no header strip either.\n\n`showToolbar={false}` does not change pagination: with `pagination` on, the pager\nfooter still renders. Hide only the picker with `showColumnPicker={false}`.\n\n### One pager instead of two\n\nWith `pagination` and the toolbar both on, the table shows a compact pager in the\ntoolbar and another under the rows. Keep the toolbar (reload, page size, result\ncount) and drop the top pager with `paginationPosition=\"bottom\"`:\n\n```jsx\n<Table data={rows} columns={columns} pagination paginationPosition=\"bottom\" />\n```\n\n`paginationPosition=\"top\"` does the opposite and keeps only the toolbar pager.\n\n### Conditional row details\n\n`hasDetails` accepts a predicate so only rows that actually have something to\nshow get an expand toggle. Rows that don't qualify still render the (empty)\ntoggle cell, so the columns stay aligned.\n\n```jsx\nconst ErrorDetails = ({ row }) => <pre>{row.stackTrace}</pre>;\n\n<Table\n data={jobs}\n columns={columns}\n hasDetails={(row) => row.status === 'failed'}\n DetailsComponent={ErrorDetails}\n/>\n```\n\n### Mobile cards, filters and swipe actions\n\nBelow `responsiveBreakpoint` rows render as cards. Card controls (expand\ntoggle, selection checkbox, inline actions, kebab) have 44×44 px hit areas.\nWith `filterable` on a phone, the filters open in a bottom sheet with an\nApply / Clear footer; pass `mobileFiltersPresentation=\"inline\"` to keep the\ninline panel.\n\n```jsx\n<Table\n data={messages}\n columns={columns}\n filterable\n defaultMobileView // server-rendered HTML starts as cards\n swipeActions={(row) => ({\n leading: [{ key: 'read', label: 'Read', icon: MailOpen, tone: 'primary', onSelect: () => markRead(row) }],\n trailing: [\n { key: 'delete', label: 'Delete', icon: Trash2, tone: 'destructive', onSelect: () => remove(row) },\n { key: 'flag', label: 'Flag', icon: Flag, tone: 'warning', onSelect: () => flag(row) },\n ],\n })}\n/>\n```\n\n### Sticky header\n\n```jsx\n<Table data={rows} columns={columns} stickyHeader=\"28rem\" />\n```\n\n### Row action menu\n\nThe kebab button (`aria-haspopup=\"menu\"`) opens the row's menu actions. On\ndesktop it is a `role=\"menu\"` popover: focus moves to the first action,\nArrowUp/ArrowDown (wrapping), Home/End and first-letter typeahead move between\nactions, Escape or Tab closes it and returns focus to the kebab, and a press\noutside or a page scroll closes it (pressing the kebab again closes it too). On phones (`actionMenuPresentation=\"auto\"`,\nthe default) the same actions open as an action sheet: 48px rows in a rounded\ncard, destructive actions in red, and a Cancel button below. Pass\n`actionMenuPresentation=\"popover\"` to keep the desktop menu everywhere.\n\n```jsx\n<Table\n data={users}\n columns={columns}\n actions={[{ name: 'delete', tone: 'destructive' }]}\n actionMenuPresentation=\"auto\"\n/>\n```\n\n### Conditional and inline actions\n\n`hidden` and `disabled` accept a `(row, index)` predicate, `label` and `icon`\naccept a `(row, index)` producer, and `inline: true` promotes an action out of\nthe kebab menu into the row itself. When no menu actions remain for a row, the\nkebab button is not rendered at all.\n\n```jsx\n<Table\n data={jobs}\n columns={columns}\n replaceDefaultActions\n actions={[\n { name: 'view', label: 'View', icon: <Eye size={16} />, inline: true },\n { name: 'edit', label: 'Edit', icon: <Edit size={16} />,\n disabled: (row) => row.locked },\n { name: 'retry', label: (row) => `Retry (${row.tries})`,\n hidden: (row) => row.status !== 'failed' },\n ]}\n onAction={({ action, row }) => handle(action.name, row)}\n/>\n```\n\nFor a large table, wrap a function-valued `actions` in `useCallback` — a fresh\narrow on every render defeats the per-row memoization.\n\n```jsx\nconst actions = useCallback(\n (row) => (row.role === 'admin' ? adminActions : userActions),\n []\n);\n```\n\nNote that because `icon` may be a `(row, index) => node` producer, passing a\nbare component reference (`icon: Edit`) calls it rather than rendering it. Pass\nan element instead: `icon: <Edit size={16} />`.\n\n### Patching the default actions\n\nEntries you supply are merged onto the built-in Edit/Delete/View set by `name`,\nso you can swap one handler without restating the rest. Unmatched names are\nappended after the defaults, and `hidden: true` removes a default outright.\n\n```jsx\n<Table\n data={users}\n columns={columns}\n actions={[\n { name: 'edit', onClick: ({ row }) => edit(row) }, // keeps default label + icon\n { name: 'delete', hidden: (row) => !row.canDelete },\n { name: 'invite', label: 'Invite', icon: <Mail size={16} /> }, // appended\n ]}\n/>\n```\n\nPass `replaceDefaultActions` to skip merging and use only your own set:\n\n```jsx\n<Table\n data={users}\n columns={columns}\n replaceDefaultActions\n actions={[{ name: 'approve', label: 'Approve' }]}\n/>\n```\n\n### Interactive Table Mode\n\nPass `interactive={true}` to enable inline cell editing (double-click a cell or press standard `'e'` key on a focused active cell to enter edit mode), instant record addition (at top or bottom), keyboard navigation (`Tab`/`Shift+Tab` to move between cells, `Escape` to exit edit mode, `Enter` to save modified changes), and constraint validation before saving.\n\n#### Interactive Props & Options\n\n- `interactive`: `boolean` (default: `false`) — Enables interactive mode.\n- `editKeyBinding`: `string | ((e) => boolean)` (default: `\"e\"`) — Standard key binding to enter cell edit mode on active focused cell.\n- `newRowPosition`: `\"bottom\" | \"top\"` (default: `\"bottom\"`) — Position where empty new rows are inserted (at the bottom or top of table dataset).\n- `addRowRef`: `React.MutableRefObject` — External ref populated with `(position?: \"top\" | \"bottom\") => void` to trigger adding a new row from external buttons.\n- `onInitAddRow`: `(addRowFn: (position?: \"top\" | \"bottom\") => void) => void` — Callback receiving the add-row function on component initialization.\n- `renderAddRowButton`: `({ addRow, newRowPosition }) => ReactNode` — Render prop for rendering a custom external add-row button in the table toolbar.\n\n#### Interactive Usage Example\n\n```jsx\nconst columns = [\n { key: 'name', label: 'Name', type: 'text', required: true },\n { key: 'role', label: 'Role', type: 'select', options: ['Admin', 'User'], required: true },\n { key: 'notes', label: 'Notes', type: 'textarea' },\n];\n\nconst addRowRef = useRef();\n\nreturn (\n <div>\n <button onClick={() => addRowRef.current?.(\"top\")}>\n Add Row at Top\n </button>\n\n <Table\n interactive\n editKeyBinding=\"e\"\n newRowPosition=\"top\"\n addRowRef={addRowRef}\n columns={columns}\n data={users}\n onSave={async ({ updatedRows, newRows, allData }) => {\n await api.saveUsers({ updatedRows, newRows });\n }}\n />\n </div>\n);\n```\n\n[← Component overview](README.md)\n",
|
|
7253
7253
|
"Tabs": "# Tabs\n\nTabbed content with list and panels. Use subcomponents: `Tabs.List`, `Tabs.Tab`, `Tabs.Panels`, `Tabs.Panel`. Supports controlled (`index` + `onChange`) or uncontrolled (`defaultIndex`), keyboard navigation, and variants.\n\n**Accessibility and SSR:** tab and panel ids come from `React.useId()`, so server-rendered markup hydrates without mismatches and two Tabs on one page never collide. Each tab's `aria-controls` and each panel's `aria-labelledby` point at elements that exist. Panels mount lazily, so only the selected tab carries `aria-controls`.\n\n## Import\n\n```jsx\nimport { Tabs } from '@dreamtree-org/twreact-ui';\n```\n\n## Props (Tabs root)\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Must include `Tabs.List` and `Tabs.Panels`. |\n| `defaultIndex` | `number` | `0` | Uncontrolled initial active tab index. |\n| `index` | `number` | — | Controlled active index. |\n| `onChange` | `(index: number) => void` | — | Called when tab changes. |\n| `orientation` | `\"horizontal\"` \\| `\"vertical\"` | `\"horizontal\"` | Layout direction. |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Tab size. Other `Size` values (`xs`, `xl`, `2xl`) fall back to `md`. |\n| `variant` | `\"line\"` \\| `\"pills\"` \\| `\"segmented\"` \\| `\"unstyled\"` | `\"line\"` | Tab style. `segmented` is an iOS-style segmented control: equal-width segments on a tinted `bg-line` track (same as `SegmentedControl`, follows dark mode) with a sliding surface thumb. |\n| `animated` | `boolean` | `true` | Fade-in (`animate-fade-in`) on the newly active panel; skipped under `prefers-reduced-motion`. |\n| `swipeable` | `boolean` | `false` | Swipe the panels left/right (touch or pen, at least 50 px and mostly horizontal) to move to the next/previous enabled tab. Vertical scrolling is never blocked (`touch-action: pan-y`); mouse drags are ignored. |\n| `className` | `string` | `\"\"` | Wrapper CSS classes. |\n\n**Scrolling and indicator:** a horizontal `Tabs.List` scrolls sideways with scroll-snap and a hidden scrollbar, and keeps the active tab scrolled into view. For `line` and `segmented`, the active underline / thumb is a single indicator that slides between tabs (`duration-base`, no transition under `prefers-reduced-motion`). It is measured after mount, so server-rendered markup shows the static active style until then. Horizontal tabs draw their focus ring inset so the scroll container doesn't clip it.\n\n`Tabs.List` forwards a `ref` and spreads extra props (`aria-label`, `data-*`, …) onto the `role=\"tablist\"` element.\n\n## Tabs.Tab props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Tab label |\n| `leftIcon` | `ReactNode` | — | Icon before the label. Renamed from `iconLeft` in 2.0.0 ([migration](MIGRATION-2.0.md#tabstab-iconleft--iconright)) |\n| `rightIcon` | `ReactNode` | — | Icon after the label. Renamed from `iconRight` in 2.0.0 |\n| `disabled` | `boolean` | `false` | Skip the tab in keyboard navigation and ignore clicks |\n| `tabClassName` | `string` | `\"\"` | Extra classes for the tab button |\n\n## Usage\n\n```jsx\n<Tabs defaultIndex={0} variant=\"pills\" size=\"md\">\n <Tabs.List>\n <Tabs.Tab>Home</Tabs.Tab>\n <Tabs.Tab>Profile</Tabs.Tab>\n <Tabs.Tab leftIcon={<Settings className=\"h-4 w-4\" />}>Settings</Tabs.Tab>\n </Tabs.List>\n <Tabs.Panels>\n <Tabs.Panel>Home content</Tabs.Panel>\n <Tabs.Panel>Profile content</Tabs.Panel>\n <Tabs.Panel>Settings content</Tabs.Panel>\n </Tabs.Panels>\n</Tabs>\n```\n\nControlled:\n\n```jsx\nconst [index, setIndex] = useState(0);\n<Tabs index={index} onChange={setIndex}>\n ...\n</Tabs>\n```\n\n[← Component overview](README.md)\n",
|
|
7254
7254
|
"TextToSpeech": "# TextToSpeech\n\nText-to-speech using the browser’s Speech Synthesis API. Renders a styled \"Speak\" / \"Stop\" button (matching `SpeechToText`) or a custom button via `renderButton`. Toggling speak while playing stops playback.\n\n## Import\n\n```jsx\nimport { TextToSpeech } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `text` | `string` | `\"\"` | Text to speak. The **current** value is spoken on each click, so later prop changes are honoured. |\n| `rate` | `number` | `1` | Speech rate (0.1–10). |\n| `pitch` | `number` | `0.5` | Pitch (0–2). |\n| `onSpeak` | `(text: string) => void` | `() => {}` | Called when speech starts (with the text spoken). |\n| `onError` | `(error: Error) => void` | `() => {}` | Called when synthesis fails. Intentional stops (`interrupted` / `canceled`) are not reported. |\n| `disabled` | `boolean` | `false` | Disables the button. |\n| `renderButton` | `(props: { onClick, isSpeaking, isSupported, disabled, speak, stop }) => ReactNode` | — | Custom button. `onClick` toggles; `isSupported` is `false` when the browser has no `speechSynthesis`. |\n| `className` | `string` | — | Merged (via `cn`) onto the default button. |\n| `...rest` | `ButtonHTMLAttributes` | — | Forwarded to the default `<button>` (`aria-*`, `data-*`, `id`, …). |\n\n`ref` is forwarded to the default `<button>` element.\n\n## Behavior\n\n- Clicking the button toggles: if not speaking, speaks the current `text` with a new `SpeechSynthesisUtterance`; if speaking, calls `speechSynthesis.cancel()`.\n- `onSpeak` is called when speech starts; the button returns to \"Speak\" when the utterance ends **or errors**.\n- **Unsupported browsers:** when `window.speechSynthesis` is missing the default button renders disabled with a `title` (and screen-reader text) explaining that text-to-speech is not supported. `renderButton` receives `isSupported: false` / `disabled: true`.\n- **Unmount:** if this instance is speaking when it unmounts, playback is cancelled.\n- The default button is rendered directly (no wrapper `<div>`), with `aria-pressed` reflecting the speaking state and a `focus-visible` ring.\n\n## Example\n\n```jsx\n<TextToSpeech text=\"Hello, world!\" rate={1} onSpeak={(t) => console.log('Speaking:', t)} />\n\n<TextToSpeech\n text={content}\n renderButton={({ onClick, isSpeaking }) => (\n <Button onClick={onClick}>{isSpeaking ? 'Stop' : 'Play'} audio</Button>\n )}\n/>\n```\n\n[← Component overview](README.md)\n",
|
|
7255
7255
|
"Textarea": "# Textarea\n\nMulti-line text input that mirrors `Input`'s API: label, error, helper text, required marker, character count, plus optional auto-resize.\n\n## Import\n\n```jsx\nimport { Textarea } from '@dreamtree-org/twreact-ui';\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `label` | `ReactNode` | — | Label rendered above the field (linked with `htmlFor`) |\n| `error` | `ReactNode` | — | Error message; sets `aria-invalid` and is shown with an icon |\n| `helperText` | `ReactNode` | — | Hint below the field; wired to `aria-describedby` |\n| `required` | `boolean` | `false` | Marks the field required (native `required` + visual `*`) |\n| `disabled` | `boolean` | `false` | Disables the field |\n| `showCount` | `boolean` | `false` | Show `current/maxLength` (needs `maxLength`) |\n| `maxLength` | `number` | — | Native max length |\n| `autoResize` | `boolean` | `false` | Grow with the content; disables the manual resize handle |\n| `rows` | `number` | `3` | Initial (minimum) rows |\n| `maxRows` | `number` | — | With `autoResize`, cap the height and scroll beyond it |\n| `size` | `\"sm\"` \\| `\"md\"` \\| `\"lg\"` | `\"md\"` | Padding and text size (`md` is the original look); `rows` still sets the height |\n| `value` | `string` | — | Controlled value |\n| `defaultValue` | `string` | — | Uncontrolled initial value |\n| `onChange` | `function` | — | Native change handler |\n| `id` | `string` | — | Field id (auto-generated otherwise) |\n| `className` | `string` | — | Extra classes for the `<textarea>` (merged with `cn`) |\n| `...rest` | `TextareaHTMLAttributes` | — | Forwarded to the `<textarea>`; `ref` points at it |\n\nWorks controlled (`value` + `onChange`) or uncontrolled (`defaultValue`); the character count tracks both.\n\nThe focus ring is `focus-visible` only (no ring left after a mouse click), and colours use role tokens (`bg-surface`, `text-foreground`) with `dark:` counterparts.\n\n## Examples\n\n### Basic\n\n```jsx\n<Textarea label=\"Description\" placeholder=\"What needs to happen?\" helperText=\"Plain text only.\" />\n```\n\n### Count + validation\n\n```jsx\n<Textarea label=\"Reason\" required showCount maxLength={280} error={errors.reason} value={reason} onChange={(e) => setReason(e.target.value)} />\n```\n\n### Auto-resize\n\n```jsx\n<Textarea label=\"Notes\" autoResize maxRows={8} value={notes} onChange={(e) => setNotes(e.target.value)} />\n```\n\n## Accessibility\n\n| Behavior | Detail |\n|----------|--------|\n| Labelling | `label` is a real `<label htmlFor>` |\n| Description | `error` and `helperText` ids (plus any `aria-describedby` you pass) are joined into `aria-describedby` |\n| Error | `aria-invalid=\"true\"`; the message is `aria-live=\"polite\"` and paired with an icon (not color alone) |\n| Focus ring | `focus:ring-2 focus:ring-primary-500` (error ring when invalid) |\n| Dark mode | Field, border, label and helper text use `dark:` variants |\n\n[← Component overview](README.md)\n",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreamtree-org/twreact-ui",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "A comprehensive React + Tailwind components library for building modern web apps",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Partha Preetham Krishna",
|
|
@@ -166,8 +166,9 @@
|
|
|
166
166
|
"@storybook/blocks": "8.6.15",
|
|
167
167
|
"@storybook/react": "8.6.15",
|
|
168
168
|
"@storybook/react-vite": "8.6.15",
|
|
169
|
+
"@testing-library/dom": "^10.4.2",
|
|
169
170
|
"@testing-library/jest-dom": "^6.9.1",
|
|
170
|
-
"@testing-library/react": "^
|
|
171
|
+
"@testing-library/react": "^16.3.3",
|
|
171
172
|
"@tiptap/core": "^3.32.0",
|
|
172
173
|
"@tiptap/pm": "^3.32.0",
|
|
173
174
|
"@tiptap/react": "^3.32.0",
|