From 058a4fdf48fb43e11187e702f5914ea1fdfa466d Mon Sep 17 00:00:00 2001 From: Karn Date: Wed, 26 Aug 2026 21:58:50 +0530 Subject: [PATCH] feat: add Linear-style collection views --- .changeset/linear-collection-views.md | 6 + .gitignore | 1 + apps/docs/package.json | 3 + apps/docs/public/components/accordion.md | 9 + apps/docs/public/components/alert-dialog.md | 9 + apps/docs/public/components/avatar.md | 9 + apps/docs/public/components/badge.md | 9 + apps/docs/public/components/breadcrumbs.md | 9 + apps/docs/public/components/button.md | 9 + apps/docs/public/components/calendar.md | 9 + apps/docs/public/components/callout.md | 9 + apps/docs/public/components/checkbox.md | 9 + apps/docs/public/components/collapsible.md | 9 + .../public/components/collection-controls.md | 9 + apps/docs/public/components/combobox.md | 9 + apps/docs/public/components/command-menu.md | 9 + apps/docs/public/components/composer.md | 9 + apps/docs/public/components/context-menu.md | 9 + apps/docs/public/components/data-table.md | 9 + apps/docs/public/components/dialog.md | 9 + apps/docs/public/components/drawer.md | 9 + apps/docs/public/components/empty-state.md | 9 + apps/docs/public/components/file-upload.md | 9 + apps/docs/public/components/form.md | 9 + apps/docs/public/components/icon-button.md | 9 + apps/docs/public/components/index.md | 50 ++ apps/docs/public/components/input.md | 9 + apps/docs/public/components/kbd.md | 9 + apps/docs/public/components/list.md | 9 + apps/docs/public/components/menu.md | 9 + apps/docs/public/components/native-select.md | 9 + apps/docs/public/components/pagination.md | 9 + apps/docs/public/components/popover.md | 9 + apps/docs/public/components/progress.md | 9 + .../docs/public/components/property-picker.md | 9 + apps/docs/public/components/radio-group.md | 9 + apps/docs/public/components/search-field.md | 9 + apps/docs/public/components/select.md | 9 + apps/docs/public/components/separator.md | 9 + apps/docs/public/components/sidebar.md | 9 + apps/docs/public/components/skeleton.md | 9 + apps/docs/public/components/slider.md | 9 + apps/docs/public/components/spinner.md | 9 + apps/docs/public/components/status.md | 9 + apps/docs/public/components/switch.md | 9 + apps/docs/public/components/tabs.md | 9 + apps/docs/public/components/toast.md | 9 + apps/docs/public/components/toggle-group.md | 9 + apps/docs/public/components/tooltip.md | 9 + apps/docs/public/components/tree-view.md | 9 + apps/docs/public/dependencies.md | 18 + apps/docs/public/index.md | 8 + apps/docs/public/llms-full.txt | 570 ++++++++++++++++++ apps/docs/public/llms.txt | 61 ++ apps/docs/scripts/generate-agent-docs.mjs | 189 ++++++ apps/docs/src/docs.css | 89 ++- apps/docs/src/lib/nav.ts | 16 +- apps/docs/src/routeTree.gen.ts | 43 ++ apps/docs/src/routes/__root.tsx | 9 +- .../routes/components/collection-controls.tsx | 194 ++++++ .../docs/src/routes/components/data-table.tsx | 316 ++++++++-- apps/docs/src/routes/components/form.tsx | 122 ++++ apps/docs/src/routes/dependencies.tsx | 7 +- apps/docs/worker.mjs | 46 ++ apps/docs/wrangler.jsonc | 8 +- packages/dowel/package.json | 1 + .../src/components/badge/badge.stylex.ts | 2 +- .../data-table/data-table.stylex.ts | 110 +++- .../components/data-table/data-table.test.tsx | 78 +++ .../dowel/src/components/data-table/index.tsx | 549 +++++++++++++++-- .../filter-bar/filter-bar.stylex.ts | 113 ++++ .../components/filter-bar/filter-bar.test.tsx | 36 ++ .../dowel/src/components/filter-bar/index.tsx | 97 +++ .../filter-picker/filter-picker.stylex.ts | 125 ++++ .../filter-picker/filter-picker.test.tsx | 48 ++ .../src/components/filter-picker/index.tsx | 222 +++++++ .../dowel/src/components/form/form.stylex.ts | 22 + .../dowel/src/components/form/form.test.tsx | 60 ++ packages/dowel/src/components/form/index.tsx | 169 ++++++ .../hover-actions/hover-actions.stylex.ts | 22 + .../hover-actions/hover-actions.test.tsx | 26 + .../src/components/hover-actions/index.tsx | 42 ++ packages/dowel/src/components/menu/index.tsx | 3 +- .../dowel/src/components/menu/menu.stylex.ts | 3 + .../components/multi-select-toolbar/index.tsx | 52 ++ .../multi-select-toolbar.stylex.ts | 62 ++ .../multi-select-toolbar.test.tsx | 28 + .../dowel/src/components/popover/index.tsx | 3 +- .../src/components/popover/popover.stylex.ts | 3 + .../search-field/search-field.stylex.ts | 4 + .../src/components/view-options/index.tsx | 267 ++++++++ .../view-options/view-options.stylex.ts | 174 ++++++ .../view-options/view-options.test.tsx | 51 ++ packages/dowel/src/index.ts | 38 ++ packages/dowel/src/theme/themes.stylex.ts | 4 + packages/dowel/src/theme/tokens.stylex.ts | 2 + pnpm-lock.yaml | 42 ++ 97 files changed, 4506 insertions(+), 122 deletions(-) create mode 100644 .changeset/linear-collection-views.md create mode 100644 apps/docs/public/components/accordion.md create mode 100644 apps/docs/public/components/alert-dialog.md create mode 100644 apps/docs/public/components/avatar.md create mode 100644 apps/docs/public/components/badge.md create mode 100644 apps/docs/public/components/breadcrumbs.md create mode 100644 apps/docs/public/components/button.md create mode 100644 apps/docs/public/components/calendar.md create mode 100644 apps/docs/public/components/callout.md create mode 100644 apps/docs/public/components/checkbox.md create mode 100644 apps/docs/public/components/collapsible.md create mode 100644 apps/docs/public/components/collection-controls.md create mode 100644 apps/docs/public/components/combobox.md create mode 100644 apps/docs/public/components/command-menu.md create mode 100644 apps/docs/public/components/composer.md create mode 100644 apps/docs/public/components/context-menu.md create mode 100644 apps/docs/public/components/data-table.md create mode 100644 apps/docs/public/components/dialog.md create mode 100644 apps/docs/public/components/drawer.md create mode 100644 apps/docs/public/components/empty-state.md create mode 100644 apps/docs/public/components/file-upload.md create mode 100644 apps/docs/public/components/form.md create mode 100644 apps/docs/public/components/icon-button.md create mode 100644 apps/docs/public/components/index.md create mode 100644 apps/docs/public/components/input.md create mode 100644 apps/docs/public/components/kbd.md create mode 100644 apps/docs/public/components/list.md create mode 100644 apps/docs/public/components/menu.md create mode 100644 apps/docs/public/components/native-select.md create mode 100644 apps/docs/public/components/pagination.md create mode 100644 apps/docs/public/components/popover.md create mode 100644 apps/docs/public/components/progress.md create mode 100644 apps/docs/public/components/property-picker.md create mode 100644 apps/docs/public/components/radio-group.md create mode 100644 apps/docs/public/components/search-field.md create mode 100644 apps/docs/public/components/select.md create mode 100644 apps/docs/public/components/separator.md create mode 100644 apps/docs/public/components/sidebar.md create mode 100644 apps/docs/public/components/skeleton.md create mode 100644 apps/docs/public/components/slider.md create mode 100644 apps/docs/public/components/spinner.md create mode 100644 apps/docs/public/components/status.md create mode 100644 apps/docs/public/components/switch.md create mode 100644 apps/docs/public/components/tabs.md create mode 100644 apps/docs/public/components/toast.md create mode 100644 apps/docs/public/components/toggle-group.md create mode 100644 apps/docs/public/components/tooltip.md create mode 100644 apps/docs/public/components/tree-view.md create mode 100644 apps/docs/public/dependencies.md create mode 100644 apps/docs/public/index.md create mode 100644 apps/docs/public/llms-full.txt create mode 100644 apps/docs/public/llms.txt create mode 100644 apps/docs/scripts/generate-agent-docs.mjs create mode 100644 apps/docs/src/routes/components/collection-controls.tsx create mode 100644 apps/docs/src/routes/components/form.tsx create mode 100644 apps/docs/worker.mjs create mode 100644 packages/dowel/src/components/filter-bar/filter-bar.stylex.ts create mode 100644 packages/dowel/src/components/filter-bar/filter-bar.test.tsx create mode 100644 packages/dowel/src/components/filter-bar/index.tsx create mode 100644 packages/dowel/src/components/filter-picker/filter-picker.stylex.ts create mode 100644 packages/dowel/src/components/filter-picker/filter-picker.test.tsx create mode 100644 packages/dowel/src/components/filter-picker/index.tsx create mode 100644 packages/dowel/src/components/form/form.stylex.ts create mode 100644 packages/dowel/src/components/form/form.test.tsx create mode 100644 packages/dowel/src/components/form/index.tsx create mode 100644 packages/dowel/src/components/hover-actions/hover-actions.stylex.ts create mode 100644 packages/dowel/src/components/hover-actions/hover-actions.test.tsx create mode 100644 packages/dowel/src/components/hover-actions/index.tsx create mode 100644 packages/dowel/src/components/multi-select-toolbar/index.tsx create mode 100644 packages/dowel/src/components/multi-select-toolbar/multi-select-toolbar.stylex.ts create mode 100644 packages/dowel/src/components/multi-select-toolbar/multi-select-toolbar.test.tsx create mode 100644 packages/dowel/src/components/view-options/index.tsx create mode 100644 packages/dowel/src/components/view-options/view-options.stylex.ts create mode 100644 packages/dowel/src/components/view-options/view-options.test.tsx diff --git a/.changeset/linear-collection-views.md b/.changeset/linear-collection-views.md new file mode 100644 index 0000000..4124c04 --- /dev/null +++ b/.changeset/linear-collection-views.md @@ -0,0 +1,6 @@ +--- +"@karnstack/dowel": minor +--- + +Add collection view controls, advanced DataTable state and interactions, a +TanStack Form adapter, and a darker dark-mode badge surface. diff --git a/.gitignore b/.gitignore index cf06758..8719d87 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ dist/ # TanStack Start's scratch directory: the SSR bundle it builds only to render # the prerendered HTML, plus the route generator's temp files. .tanstack/ +.wrangler/ test-results/ playwright-report/ blob-report/ diff --git a/apps/docs/package.json b/apps/docs/package.json index 15e1cea..ca99eae 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -4,7 +4,10 @@ "private": true, "type": "module", "scripts": { + "agent-docs": "node scripts/generate-agent-docs.mjs", + "predev": "pnpm agent-docs", "dev": "vite dev", + "prebuild": "pnpm agent-docs", "build": "vite build", "typecheck": "tsc --noEmit", "test": "echo \"browser accessibility tests run with pnpm test:a11y\" && exit 0", diff --git a/apps/docs/public/components/accordion.md b/apps/docs/public/components/accordion.md new file mode 100644 index 0000000..0d406de --- /dev/null +++ b/apps/docs/public/components/accordion.md @@ -0,0 +1,9 @@ +# Accordion + +> A keyboard-accessible group of animated disclosure panels. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/accordion) +- [Markdown documentation](https://dowel.sh/components/accordion.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/alert-dialog.md b/apps/docs/public/components/alert-dialog.md new file mode 100644 index 0000000..ff286bc --- /dev/null +++ b/apps/docs/public/components/alert-dialog.md @@ -0,0 +1,9 @@ +# Alert Dialog + +> A focused confirmation modal for consequential actions. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/alert-dialog) +- [Markdown documentation](https://dowel.sh/components/alert-dialog.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/avatar.md b/apps/docs/public/components/avatar.md new file mode 100644 index 0000000..eb6df24 --- /dev/null +++ b/apps/docs/public/components/avatar.md @@ -0,0 +1,9 @@ +# Avatar + +> An image or initials fallback with shape, size, and presence. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/avatar) +- [Markdown documentation](https://dowel.sh/components/avatar.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/badge.md b/apps/docs/public/components/badge.md new file mode 100644 index 0000000..b3828ee --- /dev/null +++ b/apps/docs/public/components/badge.md @@ -0,0 +1,9 @@ +# Badge + +> A compact status or metadata pill in five tones. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/badge) +- [Markdown documentation](https://dowel.sh/components/badge.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/breadcrumbs.md b/apps/docs/public/components/breadcrumbs.md new file mode 100644 index 0000000..22c42e9 --- /dev/null +++ b/apps/docs/public/components/breadcrumbs.md @@ -0,0 +1,9 @@ +# Breadcrumbs + +> A compact location trail with automatic middle-item elision. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/breadcrumbs) +- [Markdown documentation](https://dowel.sh/components/breadcrumbs.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/button.md b/apps/docs/public/components/button.md new file mode 100644 index 0000000..f1ef950 --- /dev/null +++ b/apps/docs/public/components/button.md @@ -0,0 +1,9 @@ +# Button + +> The default action control. Five hierarchy levels, two sizes. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/button) +- [Markdown documentation](https://dowel.sh/components/button.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/calendar.md b/apps/docs/public/components/calendar.md new file mode 100644 index 0000000..d5fc11a --- /dev/null +++ b/apps/docs/public/components/calendar.md @@ -0,0 +1,9 @@ +# Calendar & Date Picker + +> An internationalized month grid and segmented date field. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/calendar) +- [Markdown documentation](https://dowel.sh/components/calendar.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/callout.md b/apps/docs/public/components/callout.md new file mode 100644 index 0000000..bb9df69 --- /dev/null +++ b/apps/docs/public/components/callout.md @@ -0,0 +1,9 @@ +# Callout + +> Persistent contextual guidance with optional actions and tone. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/callout) +- [Markdown documentation](https://dowel.sh/components/callout.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/checkbox.md b/apps/docs/public/components/checkbox.md new file mode 100644 index 0000000..a8484a2 --- /dev/null +++ b/apps/docs/public/components/checkbox.md @@ -0,0 +1,9 @@ +# Checkbox + +> Checked, unchecked, and mixed selection with native form values. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/checkbox) +- [Markdown documentation](https://dowel.sh/components/checkbox.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/collapsible.md b/apps/docs/public/components/collapsible.md new file mode 100644 index 0000000..eb28932 --- /dev/null +++ b/apps/docs/public/components/collapsible.md @@ -0,0 +1,9 @@ +# Collapsible + +> An animated disclosure for one optional content region. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/collapsible) +- [Markdown documentation](https://dowel.sh/components/collapsible.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/collection-controls.md b/apps/docs/public/components/collection-controls.md new file mode 100644 index 0000000..b75cca1 --- /dev/null +++ b/apps/docs/public/components/collection-controls.md @@ -0,0 +1,9 @@ +# Collection Controls + +> Filters, view options, row actions, and bulk selection controls. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/collection-controls) +- [Markdown documentation](https://dowel.sh/components/collection-controls.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/combobox.md b/apps/docs/public/components/combobox.md new file mode 100644 index 0000000..41af427 --- /dev/null +++ b/apps/docs/public/components/combobox.md @@ -0,0 +1,9 @@ +# Combobox + +> A filterable single-choice field for larger collections. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/combobox) +- [Markdown documentation](https://dowel.sh/components/combobox.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/command-menu.md b/apps/docs/public/components/command-menu.md new file mode 100644 index 0000000..086b1b1 --- /dev/null +++ b/apps/docs/public/components/command-menu.md @@ -0,0 +1,9 @@ +# Command Menu + +> A modal search surface for grouped application commands. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/command-menu) +- [Markdown documentation](https://dowel.sh/components/command-menu.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/composer.md b/apps/docs/public/components/composer.md new file mode 100644 index 0000000..d7e657d --- /dev/null +++ b/apps/docs/public/components/composer.md @@ -0,0 +1,9 @@ +# Composer + +> A borderless creation shell with metadata and action regions. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/composer) +- [Markdown documentation](https://dowel.sh/components/composer.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/context-menu.md b/apps/docs/public/components/context-menu.md new file mode 100644 index 0000000..8c70357 --- /dev/null +++ b/apps/docs/public/components/context-menu.md @@ -0,0 +1,9 @@ +# Context Menu + +> A pointer-positioned menu for actions on a target surface. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/context-menu) +- [Markdown documentation](https://dowel.sh/components/context-menu.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/data-table.md b/apps/docs/public/components/data-table.md new file mode 100644 index 0000000..2f23590 --- /dev/null +++ b/apps/docs/public/components/data-table.md @@ -0,0 +1,9 @@ +# Data Table + +> A borderless, sortable, selectable, and resizable data table. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/data-table) +- [Markdown documentation](https://dowel.sh/components/data-table.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/dialog.md b/apps/docs/public/components/dialog.md new file mode 100644 index 0000000..850f3dd --- /dev/null +++ b/apps/docs/public/components/dialog.md @@ -0,0 +1,9 @@ +# Dialog + +> A modal on the modal elevation tier, labelled by its title. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/dialog) +- [Markdown documentation](https://dowel.sh/components/dialog.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/drawer.md b/apps/docs/public/components/drawer.md new file mode 100644 index 0000000..28aa8d2 --- /dev/null +++ b/apps/docs/public/components/drawer.md @@ -0,0 +1,9 @@ +# Drawer + +> A swipe-dismissable edge panel for navigation and detail. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/drawer) +- [Markdown documentation](https://dowel.sh/components/drawer.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/empty-state.md b/apps/docs/public/components/empty-state.md new file mode 100644 index 0000000..a041176 --- /dev/null +++ b/apps/docs/public/components/empty-state.md @@ -0,0 +1,9 @@ +# Empty State + +> Structured guidance and actions for a surface without content. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/empty-state) +- [Markdown documentation](https://dowel.sh/components/empty-state.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/file-upload.md b/apps/docs/public/components/file-upload.md new file mode 100644 index 0000000..b873239 --- /dev/null +++ b/apps/docs/public/components/file-upload.md @@ -0,0 +1,9 @@ +# File Upload + +> A native file picker and dropzone with built-in validation. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/file-upload) +- [Markdown documentation](https://dowel.sh/components/file-upload.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/form.md b/apps/docs/public/components/form.md new file mode 100644 index 0000000..8d75b05 --- /dev/null +++ b/apps/docs/public/components/form.md @@ -0,0 +1,9 @@ +# Form + +> TanStack Form with Dowel fields and actions pre-bound. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/form) +- [Markdown documentation](https://dowel.sh/components/form.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/icon-button.md b/apps/docs/public/components/icon-button.md new file mode 100644 index 0000000..dd4a333 --- /dev/null +++ b/apps/docs/public/components/icon-button.md @@ -0,0 +1,9 @@ +# Icon Button + +> A square control for a single icon, with a required label. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/icon-button) +- [Markdown documentation](https://dowel.sh/components/icon-button.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/index.md b/apps/docs/public/components/index.md new file mode 100644 index 0000000..d68bbaa --- /dev/null +++ b/apps/docs/public/components/index.md @@ -0,0 +1,50 @@ +# Dowel components + +> The complete Dowel component catalog. + +- [Accordion](https://dowel.sh/components/accordion.md): A keyboard-accessible group of animated disclosure panels. +- [Alert Dialog](https://dowel.sh/components/alert-dialog.md): A focused confirmation modal for consequential actions. +- [Avatar](https://dowel.sh/components/avatar.md): An image or initials fallback with shape, size, and presence. +- [Badge](https://dowel.sh/components/badge.md): A compact status or metadata pill in five tones. +- [Breadcrumbs](https://dowel.sh/components/breadcrumbs.md): A compact location trail with automatic middle-item elision. +- [Button](https://dowel.sh/components/button.md): The default action control. Five hierarchy levels, two sizes. +- [Calendar & Date Picker](https://dowel.sh/components/calendar.md): An internationalized month grid and segmented date field. +- [Callout](https://dowel.sh/components/callout.md): Persistent contextual guidance with optional actions and tone. +- [Checkbox](https://dowel.sh/components/checkbox.md): Checked, unchecked, and mixed selection with native form values. +- [Collapsible](https://dowel.sh/components/collapsible.md): An animated disclosure for one optional content region. +- [Combobox](https://dowel.sh/components/combobox.md): A filterable single-choice field for larger collections. +- [Command Menu](https://dowel.sh/components/command-menu.md): A modal search surface for grouped application commands. +- [Collection Controls](https://dowel.sh/components/collection-controls.md): Filters, view options, row actions, and bulk selection controls. +- [Composer](https://dowel.sh/components/composer.md): A borderless creation shell with metadata and action regions. +- [Context Menu](https://dowel.sh/components/context-menu.md): A pointer-positioned menu for actions on a target surface. +- [Data Table](https://dowel.sh/components/data-table.md): A borderless, sortable, selectable, and resizable data table. +- [Dialog](https://dowel.sh/components/dialog.md): A modal on the modal elevation tier, labelled by its title. +- [Drawer](https://dowel.sh/components/drawer.md): A swipe-dismissable edge panel for navigation and detail. +- [Empty State](https://dowel.sh/components/empty-state.md): Structured guidance and actions for a surface without content. +- [File Upload](https://dowel.sh/components/file-upload.md): A native file picker and dropzone with built-in validation. +- [Form](https://dowel.sh/components/form.md): TanStack Form with Dowel fields and actions pre-bound. +- [Icon Button](https://dowel.sh/components/icon-button.md): A square control for a single icon, with a required label. +- [Input](https://dowel.sh/components/input.md): Surface and borderless fields for settings and rich composers. +- [Kbd](https://dowel.sh/components/kbd.md): A keyboard shortcut rendered one key per cap. +- [List](https://dowel.sh/components/list.md): Semantic grouped rows with cells, density, and selection. +- [Menu](https://dowel.sh/components/menu.md): A dropdown with keyboard navigation and typeahead. +- [Native Select](https://dowel.sh/components/native-select.md): A resilient native choice control with Dowel field styling. +- [Pagination](https://dowel.sh/components/pagination.md): Controlled page navigation with compact number elision. +- [Popover](https://dowel.sh/components/popover.md): An anchored surface for interactive controls and rich details. +- [Property Picker](https://dowel.sh/components/property-picker.md): Searchable grouped metadata selection with a compact pill trigger. +- [Progress](https://dowel.sh/components/progress.md): Determinate and indeterminate progress for long tasks. +- [Radio Group](https://dowel.sh/components/radio-group.md): A single-choice group with native forms and arrow-key selection. +- [Search Field](https://dowel.sh/components/search-field.md): A search input with icon, clear action, and shortcut hint. +- [Separator](https://dowel.sh/components/separator.md): A quiet horizontal or vertical content divider. +- [Select](https://dowel.sh/components/select.md): A grouped single-choice popup with native form submission. +- [Sidebar](https://dowel.sh/components/sidebar.md): A responsive application rail with pointer and keyboard resize. +- [Slider](https://dowel.sh/components/slider.md): A pointer and keyboard control for a numeric range. +- [Skeleton](https://dowel.sh/components/skeleton.md): Reduced-motion-aware placeholders for pending content. +- [Spinner](https://dowel.sh/components/spinner.md): Accessible indeterminate progress in three compact sizes. +- [Status](https://dowel.sh/components/status.md): A lightweight state icon and label in semantic tones. +- [Switch](https://dowel.sh/components/switch.md): A compact immediate setting for binary on and off state. +- [Tabs](https://dowel.sh/components/tabs.md): Compact pill and line view switching with arrow-key activation. +- [Toast](https://dowel.sh/components/toast.md): A global notification queue with actions and promises. +- [Toggle Group](https://dowel.sh/components/toggle-group.md): A connected set of independently pressed view controls. +- [Tooltip](https://dowel.sh/components/tooltip.md): A hover and focus label on the popover elevation tier. +- [Tree View](https://dowel.sh/components/tree-view.md): A nested hierarchy with selection and keyboard navigation. diff --git a/apps/docs/public/components/input.md b/apps/docs/public/components/input.md new file mode 100644 index 0000000..cb4c836 --- /dev/null +++ b/apps/docs/public/components/input.md @@ -0,0 +1,9 @@ +# Input + +> Surface and borderless fields for settings and rich composers. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/input) +- [Markdown documentation](https://dowel.sh/components/input.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/kbd.md b/apps/docs/public/components/kbd.md new file mode 100644 index 0000000..2930898 --- /dev/null +++ b/apps/docs/public/components/kbd.md @@ -0,0 +1,9 @@ +# Kbd + +> A keyboard shortcut rendered one key per cap. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/kbd) +- [Markdown documentation](https://dowel.sh/components/kbd.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/list.md b/apps/docs/public/components/list.md new file mode 100644 index 0000000..f163972 --- /dev/null +++ b/apps/docs/public/components/list.md @@ -0,0 +1,9 @@ +# List + +> Semantic grouped rows with cells, density, and selection. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/list) +- [Markdown documentation](https://dowel.sh/components/list.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/menu.md b/apps/docs/public/components/menu.md new file mode 100644 index 0000000..a163b87 --- /dev/null +++ b/apps/docs/public/components/menu.md @@ -0,0 +1,9 @@ +# Menu + +> A dropdown with keyboard navigation and typeahead. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/menu) +- [Markdown documentation](https://dowel.sh/components/menu.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/native-select.md b/apps/docs/public/components/native-select.md new file mode 100644 index 0000000..e6f1824 --- /dev/null +++ b/apps/docs/public/components/native-select.md @@ -0,0 +1,9 @@ +# Native Select + +> A resilient native choice control with Dowel field styling. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/native-select) +- [Markdown documentation](https://dowel.sh/components/native-select.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/pagination.md b/apps/docs/public/components/pagination.md new file mode 100644 index 0000000..723e01b --- /dev/null +++ b/apps/docs/public/components/pagination.md @@ -0,0 +1,9 @@ +# Pagination + +> Controlled page navigation with compact number elision. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/pagination) +- [Markdown documentation](https://dowel.sh/components/pagination.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/popover.md b/apps/docs/public/components/popover.md new file mode 100644 index 0000000..0a7d2fc --- /dev/null +++ b/apps/docs/public/components/popover.md @@ -0,0 +1,9 @@ +# Popover + +> An anchored surface for interactive controls and rich details. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/popover) +- [Markdown documentation](https://dowel.sh/components/popover.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/progress.md b/apps/docs/public/components/progress.md new file mode 100644 index 0000000..7a6c490 --- /dev/null +++ b/apps/docs/public/components/progress.md @@ -0,0 +1,9 @@ +# Progress + +> Determinate and indeterminate progress for long tasks. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/progress) +- [Markdown documentation](https://dowel.sh/components/progress.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/property-picker.md b/apps/docs/public/components/property-picker.md new file mode 100644 index 0000000..d8e8d6c --- /dev/null +++ b/apps/docs/public/components/property-picker.md @@ -0,0 +1,9 @@ +# Property Picker + +> Searchable grouped metadata selection with a compact pill trigger. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/property-picker) +- [Markdown documentation](https://dowel.sh/components/property-picker.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/radio-group.md b/apps/docs/public/components/radio-group.md new file mode 100644 index 0000000..f203891 --- /dev/null +++ b/apps/docs/public/components/radio-group.md @@ -0,0 +1,9 @@ +# Radio Group + +> A single-choice group with native forms and arrow-key selection. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/radio-group) +- [Markdown documentation](https://dowel.sh/components/radio-group.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/search-field.md b/apps/docs/public/components/search-field.md new file mode 100644 index 0000000..a380ea4 --- /dev/null +++ b/apps/docs/public/components/search-field.md @@ -0,0 +1,9 @@ +# Search Field + +> A search input with icon, clear action, and shortcut hint. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/search-field) +- [Markdown documentation](https://dowel.sh/components/search-field.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/select.md b/apps/docs/public/components/select.md new file mode 100644 index 0000000..f8132fd --- /dev/null +++ b/apps/docs/public/components/select.md @@ -0,0 +1,9 @@ +# Select + +> A grouped single-choice popup with native form submission. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/select) +- [Markdown documentation](https://dowel.sh/components/select.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/separator.md b/apps/docs/public/components/separator.md new file mode 100644 index 0000000..487fe98 --- /dev/null +++ b/apps/docs/public/components/separator.md @@ -0,0 +1,9 @@ +# Separator + +> A quiet horizontal or vertical content divider. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/separator) +- [Markdown documentation](https://dowel.sh/components/separator.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/sidebar.md b/apps/docs/public/components/sidebar.md new file mode 100644 index 0000000..a9a563b --- /dev/null +++ b/apps/docs/public/components/sidebar.md @@ -0,0 +1,9 @@ +# Sidebar + +> A responsive application rail with pointer and keyboard resize. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/sidebar) +- [Markdown documentation](https://dowel.sh/components/sidebar.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/skeleton.md b/apps/docs/public/components/skeleton.md new file mode 100644 index 0000000..f5b72c2 --- /dev/null +++ b/apps/docs/public/components/skeleton.md @@ -0,0 +1,9 @@ +# Skeleton + +> Reduced-motion-aware placeholders for pending content. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/skeleton) +- [Markdown documentation](https://dowel.sh/components/skeleton.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/slider.md b/apps/docs/public/components/slider.md new file mode 100644 index 0000000..8f32458 --- /dev/null +++ b/apps/docs/public/components/slider.md @@ -0,0 +1,9 @@ +# Slider + +> A pointer and keyboard control for a numeric range. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/slider) +- [Markdown documentation](https://dowel.sh/components/slider.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/spinner.md b/apps/docs/public/components/spinner.md new file mode 100644 index 0000000..78a0846 --- /dev/null +++ b/apps/docs/public/components/spinner.md @@ -0,0 +1,9 @@ +# Spinner + +> Accessible indeterminate progress in three compact sizes. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/spinner) +- [Markdown documentation](https://dowel.sh/components/spinner.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/status.md b/apps/docs/public/components/status.md new file mode 100644 index 0000000..c2f6832 --- /dev/null +++ b/apps/docs/public/components/status.md @@ -0,0 +1,9 @@ +# Status + +> A lightweight state icon and label in semantic tones. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/status) +- [Markdown documentation](https://dowel.sh/components/status.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/switch.md b/apps/docs/public/components/switch.md new file mode 100644 index 0000000..9f9ccf6 --- /dev/null +++ b/apps/docs/public/components/switch.md @@ -0,0 +1,9 @@ +# Switch + +> A compact immediate setting for binary on and off state. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/switch) +- [Markdown documentation](https://dowel.sh/components/switch.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/tabs.md b/apps/docs/public/components/tabs.md new file mode 100644 index 0000000..8ef0140 --- /dev/null +++ b/apps/docs/public/components/tabs.md @@ -0,0 +1,9 @@ +# Tabs + +> Compact pill and line view switching with arrow-key activation. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/tabs) +- [Markdown documentation](https://dowel.sh/components/tabs.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/toast.md b/apps/docs/public/components/toast.md new file mode 100644 index 0000000..7afe492 --- /dev/null +++ b/apps/docs/public/components/toast.md @@ -0,0 +1,9 @@ +# Toast + +> A global notification queue with actions and promises. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/toast) +- [Markdown documentation](https://dowel.sh/components/toast.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/toggle-group.md b/apps/docs/public/components/toggle-group.md new file mode 100644 index 0000000..626262b --- /dev/null +++ b/apps/docs/public/components/toggle-group.md @@ -0,0 +1,9 @@ +# Toggle Group + +> A connected set of independently pressed view controls. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/toggle-group) +- [Markdown documentation](https://dowel.sh/components/toggle-group.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/tooltip.md b/apps/docs/public/components/tooltip.md new file mode 100644 index 0000000..036272f --- /dev/null +++ b/apps/docs/public/components/tooltip.md @@ -0,0 +1,9 @@ +# Tooltip + +> A hover and focus label on the popover elevation tier. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/tooltip) +- [Markdown documentation](https://dowel.sh/components/tooltip.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/components/tree-view.md b/apps/docs/public/components/tree-view.md new file mode 100644 index 0000000..6af75a2 --- /dev/null +++ b/apps/docs/public/components/tree-view.md @@ -0,0 +1,9 @@ +# Tree View + +> A nested hierarchy with selection and keyboard navigation. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/tree-view) +- [Markdown documentation](https://dowel.sh/components/tree-view.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/dependencies.md b/apps/docs/public/dependencies.md new file mode 100644 index 0000000..e68a1fa --- /dev/null +++ b/apps/docs/public/dependencies.md @@ -0,0 +1,18 @@ +# Dowel dependencies + +> Runtime packages installed with Dowel and peer runtimes owned by the consuming application. + +## Runtime dependencies + +- `@base-ui/react`: `^1.7.0` +- `@heroicons/react`: `2.2.0` +- `@internationalized/date`: `^3.12.3` +- `@stylexjs/stylex`: `0.19.0` +- `@tanstack/react-form`: `^1.33.5` +- `@tanstack/react-table`: `^9.1.2` +- `react-aria-components`: `^1.20.0` + +## Peer dependencies + +- `react`: `^19.0.0` +- `react-dom`: `^19.0.0` diff --git a/apps/docs/public/index.md b/apps/docs/public/index.md new file mode 100644 index 0000000..a92514b --- /dev/null +++ b/apps/docs/public/index.md @@ -0,0 +1,8 @@ +# Dowel + +> Compact, accessible React components for product interfaces. + +- [Agent documentation index](https://dowel.sh/llms.txt) +- [Complete agent documentation](https://dowel.sh/llms-full.txt) +- [Component catalog](https://dowel.sh/components/index.md) +- [HTML documentation](https://dowel.sh/components) diff --git a/apps/docs/public/llms-full.txt b/apps/docs/public/llms-full.txt new file mode 100644 index 0000000..0181d31 --- /dev/null +++ b/apps/docs/public/llms-full.txt @@ -0,0 +1,570 @@ +# Dowel complete documentation + +> Dowel 0.2.0 is an opinionated React component library for compact, accessible product interfaces. + +## Installation + +```sh +pnpm add @karnstack/dowel +``` + +```tsx +import "@karnstack/dowel/dowel.css"; +``` + +## Conventions + +- Import public components and types from `@karnstack/dowel`. +- Wrap the application in `ThemeProvider` when selecting light, dark, or system themes explicitly. +- Dowel controls own their appearance. Prefer documented props over consumer class or style overrides. +- Icon-only controls require an accessible `label`. + +## Accordion + +> A keyboard-accessible group of animated disclosure panels. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/accordion) +- [Markdown documentation](https://dowel.sh/components/accordion.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Alert Dialog + +> A focused confirmation modal for consequential actions. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/alert-dialog) +- [Markdown documentation](https://dowel.sh/components/alert-dialog.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Avatar + +> An image or initials fallback with shape, size, and presence. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/avatar) +- [Markdown documentation](https://dowel.sh/components/avatar.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Badge + +> A compact status or metadata pill in five tones. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/badge) +- [Markdown documentation](https://dowel.sh/components/badge.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Breadcrumbs + +> A compact location trail with automatic middle-item elision. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/breadcrumbs) +- [Markdown documentation](https://dowel.sh/components/breadcrumbs.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Button + +> The default action control. Five hierarchy levels, two sizes. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/button) +- [Markdown documentation](https://dowel.sh/components/button.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Calendar & Date Picker + +> An internationalized month grid and segmented date field. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/calendar) +- [Markdown documentation](https://dowel.sh/components/calendar.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Callout + +> Persistent contextual guidance with optional actions and tone. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/callout) +- [Markdown documentation](https://dowel.sh/components/callout.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Checkbox + +> Checked, unchecked, and mixed selection with native form values. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/checkbox) +- [Markdown documentation](https://dowel.sh/components/checkbox.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Collapsible + +> An animated disclosure for one optional content region. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/collapsible) +- [Markdown documentation](https://dowel.sh/components/collapsible.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Combobox + +> A filterable single-choice field for larger collections. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/combobox) +- [Markdown documentation](https://dowel.sh/components/combobox.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Command Menu + +> A modal search surface for grouped application commands. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/command-menu) +- [Markdown documentation](https://dowel.sh/components/command-menu.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Collection Controls + +> Filters, view options, row actions, and bulk selection controls. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/collection-controls) +- [Markdown documentation](https://dowel.sh/components/collection-controls.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Composer + +> A borderless creation shell with metadata and action regions. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/composer) +- [Markdown documentation](https://dowel.sh/components/composer.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Context Menu + +> A pointer-positioned menu for actions on a target surface. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/context-menu) +- [Markdown documentation](https://dowel.sh/components/context-menu.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Data Table + +> A borderless, sortable, selectable, and resizable data table. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/data-table) +- [Markdown documentation](https://dowel.sh/components/data-table.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Dialog + +> A modal on the modal elevation tier, labelled by its title. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/dialog) +- [Markdown documentation](https://dowel.sh/components/dialog.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Drawer + +> A swipe-dismissable edge panel for navigation and detail. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/drawer) +- [Markdown documentation](https://dowel.sh/components/drawer.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Empty State + +> Structured guidance and actions for a surface without content. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/empty-state) +- [Markdown documentation](https://dowel.sh/components/empty-state.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## File Upload + +> A native file picker and dropzone with built-in validation. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/file-upload) +- [Markdown documentation](https://dowel.sh/components/file-upload.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Form + +> TanStack Form with Dowel fields and actions pre-bound. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/form) +- [Markdown documentation](https://dowel.sh/components/form.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Icon Button + +> A square control for a single icon, with a required label. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/icon-button) +- [Markdown documentation](https://dowel.sh/components/icon-button.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Input + +> Surface and borderless fields for settings and rich composers. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/input) +- [Markdown documentation](https://dowel.sh/components/input.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Kbd + +> A keyboard shortcut rendered one key per cap. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/kbd) +- [Markdown documentation](https://dowel.sh/components/kbd.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## List + +> Semantic grouped rows with cells, density, and selection. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/list) +- [Markdown documentation](https://dowel.sh/components/list.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Menu + +> A dropdown with keyboard navigation and typeahead. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/menu) +- [Markdown documentation](https://dowel.sh/components/menu.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Native Select + +> A resilient native choice control with Dowel field styling. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/native-select) +- [Markdown documentation](https://dowel.sh/components/native-select.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Pagination + +> Controlled page navigation with compact number elision. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/pagination) +- [Markdown documentation](https://dowel.sh/components/pagination.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Popover + +> An anchored surface for interactive controls and rich details. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/popover) +- [Markdown documentation](https://dowel.sh/components/popover.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Property Picker + +> Searchable grouped metadata selection with a compact pill trigger. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/property-picker) +- [Markdown documentation](https://dowel.sh/components/property-picker.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Progress + +> Determinate and indeterminate progress for long tasks. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/progress) +- [Markdown documentation](https://dowel.sh/components/progress.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Radio Group + +> A single-choice group with native forms and arrow-key selection. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/radio-group) +- [Markdown documentation](https://dowel.sh/components/radio-group.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Search Field + +> A search input with icon, clear action, and shortcut hint. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/search-field) +- [Markdown documentation](https://dowel.sh/components/search-field.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Separator + +> A quiet horizontal or vertical content divider. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/separator) +- [Markdown documentation](https://dowel.sh/components/separator.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Select + +> A grouped single-choice popup with native form submission. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/select) +- [Markdown documentation](https://dowel.sh/components/select.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Sidebar + +> A responsive application rail with pointer and keyboard resize. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/sidebar) +- [Markdown documentation](https://dowel.sh/components/sidebar.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Slider + +> A pointer and keyboard control for a numeric range. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/slider) +- [Markdown documentation](https://dowel.sh/components/slider.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Skeleton + +> Reduced-motion-aware placeholders for pending content. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/skeleton) +- [Markdown documentation](https://dowel.sh/components/skeleton.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Spinner + +> Accessible indeterminate progress in three compact sizes. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/spinner) +- [Markdown documentation](https://dowel.sh/components/spinner.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Status + +> A lightweight state icon and label in semantic tones. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/status) +- [Markdown documentation](https://dowel.sh/components/status.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Switch + +> A compact immediate setting for binary on and off state. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/switch) +- [Markdown documentation](https://dowel.sh/components/switch.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Tabs + +> Compact pill and line view switching with arrow-key activation. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/tabs) +- [Markdown documentation](https://dowel.sh/components/tabs.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Toast + +> A global notification queue with actions and promises. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/toast) +- [Markdown documentation](https://dowel.sh/components/toast.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Toggle Group + +> A connected set of independently pressed view controls. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/toggle-group) +- [Markdown documentation](https://dowel.sh/components/toggle-group.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Tooltip + +> A hover and focus label on the popover elevation tier. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/tooltip) +- [Markdown documentation](https://dowel.sh/components/tooltip.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. + +--- + +## Tree View + +> A nested hierarchy with selection and keyboard navigation. + +- Package: `@karnstack/dowel` +- [HTML documentation](https://dowel.sh/components/tree-view) +- [Markdown documentation](https://dowel.sh/components/tree-view.md) + +Import public components and types from the package root. Import `@karnstack/dowel/dowel.css` once at the application root. diff --git a/apps/docs/public/llms.txt b/apps/docs/public/llms.txt new file mode 100644 index 0000000..4a76e4c --- /dev/null +++ b/apps/docs/public/llms.txt @@ -0,0 +1,61 @@ +# Dowel + +> Dowel is an opinionated React component library for compact, accessible product interfaces. + +Install `@karnstack/dowel`, import `@karnstack/dowel/dowel.css` once, and import components from the package root. Dowel targets React 19. + +## Components + +- [Accordion](https://dowel.sh/components/accordion.md): A keyboard-accessible group of animated disclosure panels. +- [Alert Dialog](https://dowel.sh/components/alert-dialog.md): A focused confirmation modal for consequential actions. +- [Avatar](https://dowel.sh/components/avatar.md): An image or initials fallback with shape, size, and presence. +- [Badge](https://dowel.sh/components/badge.md): A compact status or metadata pill in five tones. +- [Breadcrumbs](https://dowel.sh/components/breadcrumbs.md): A compact location trail with automatic middle-item elision. +- [Button](https://dowel.sh/components/button.md): The default action control. Five hierarchy levels, two sizes. +- [Calendar & Date Picker](https://dowel.sh/components/calendar.md): An internationalized month grid and segmented date field. +- [Callout](https://dowel.sh/components/callout.md): Persistent contextual guidance with optional actions and tone. +- [Checkbox](https://dowel.sh/components/checkbox.md): Checked, unchecked, and mixed selection with native form values. +- [Collapsible](https://dowel.sh/components/collapsible.md): An animated disclosure for one optional content region. +- [Combobox](https://dowel.sh/components/combobox.md): A filterable single-choice field for larger collections. +- [Command Menu](https://dowel.sh/components/command-menu.md): A modal search surface for grouped application commands. +- [Collection Controls](https://dowel.sh/components/collection-controls.md): Filters, view options, row actions, and bulk selection controls. +- [Composer](https://dowel.sh/components/composer.md): A borderless creation shell with metadata and action regions. +- [Context Menu](https://dowel.sh/components/context-menu.md): A pointer-positioned menu for actions on a target surface. +- [Data Table](https://dowel.sh/components/data-table.md): A borderless, sortable, selectable, and resizable data table. +- [Dialog](https://dowel.sh/components/dialog.md): A modal on the modal elevation tier, labelled by its title. +- [Drawer](https://dowel.sh/components/drawer.md): A swipe-dismissable edge panel for navigation and detail. +- [Empty State](https://dowel.sh/components/empty-state.md): Structured guidance and actions for a surface without content. +- [File Upload](https://dowel.sh/components/file-upload.md): A native file picker and dropzone with built-in validation. +- [Form](https://dowel.sh/components/form.md): TanStack Form with Dowel fields and actions pre-bound. +- [Icon Button](https://dowel.sh/components/icon-button.md): A square control for a single icon, with a required label. +- [Input](https://dowel.sh/components/input.md): Surface and borderless fields for settings and rich composers. +- [Kbd](https://dowel.sh/components/kbd.md): A keyboard shortcut rendered one key per cap. +- [List](https://dowel.sh/components/list.md): Semantic grouped rows with cells, density, and selection. +- [Menu](https://dowel.sh/components/menu.md): A dropdown with keyboard navigation and typeahead. +- [Native Select](https://dowel.sh/components/native-select.md): A resilient native choice control with Dowel field styling. +- [Pagination](https://dowel.sh/components/pagination.md): Controlled page navigation with compact number elision. +- [Popover](https://dowel.sh/components/popover.md): An anchored surface for interactive controls and rich details. +- [Property Picker](https://dowel.sh/components/property-picker.md): Searchable grouped metadata selection with a compact pill trigger. +- [Progress](https://dowel.sh/components/progress.md): Determinate and indeterminate progress for long tasks. +- [Radio Group](https://dowel.sh/components/radio-group.md): A single-choice group with native forms and arrow-key selection. +- [Search Field](https://dowel.sh/components/search-field.md): A search input with icon, clear action, and shortcut hint. +- [Separator](https://dowel.sh/components/separator.md): A quiet horizontal or vertical content divider. +- [Select](https://dowel.sh/components/select.md): A grouped single-choice popup with native form submission. +- [Sidebar](https://dowel.sh/components/sidebar.md): A responsive application rail with pointer and keyboard resize. +- [Slider](https://dowel.sh/components/slider.md): A pointer and keyboard control for a numeric range. +- [Skeleton](https://dowel.sh/components/skeleton.md): Reduced-motion-aware placeholders for pending content. +- [Spinner](https://dowel.sh/components/spinner.md): Accessible indeterminate progress in three compact sizes. +- [Status](https://dowel.sh/components/status.md): A lightweight state icon and label in semantic tones. +- [Switch](https://dowel.sh/components/switch.md): A compact immediate setting for binary on and off state. +- [Tabs](https://dowel.sh/components/tabs.md): Compact pill and line view switching with arrow-key activation. +- [Toast](https://dowel.sh/components/toast.md): A global notification queue with actions and promises. +- [Toggle Group](https://dowel.sh/components/toggle-group.md): A connected set of independently pressed view controls. +- [Tooltip](https://dowel.sh/components/tooltip.md): A hover and focus label on the popover elevation tier. +- [Tree View](https://dowel.sh/components/tree-view.md): A nested hierarchy with selection and keyboard navigation. + +## Library + +- [Complete documentation](https://dowel.sh/llms-full.txt): All component summaries in one Markdown document. +- [Component index](https://dowel.sh/components/index.md): Compact component catalog. +- [Dependencies](https://dowel.sh/dependencies.md): Runtime and peer dependency overview. +- [GitHub](https://github.com/karnstack/dowel): Source, issues, and releases. diff --git a/apps/docs/scripts/generate-agent-docs.mjs b/apps/docs/scripts/generate-agent-docs.mjs new file mode 100644 index 0000000..0a8d204 --- /dev/null +++ b/apps/docs/scripts/generate-agent-docs.mjs @@ -0,0 +1,189 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; + +import { componentNav } from "../src/lib/nav.ts"; + +const docsOrigin = "https://dowel.sh"; +const publicRoot = new URL("../public/", import.meta.url); +const packageRoot = new URL("../../../packages/dowel/", import.meta.url); +const tick = String.fromCharCode(96); + +function absolute(path) { + return new URL(path, docsOrigin).href; +} + +function componentSlug(item) { + return String(item.to).replace("/components/", ""); +} + +function componentMarkdown(item, heading = "#") { + const docsUrl = absolute(String(item.to)); + const markdownUrl = absolute(String(item.to) + ".md"); + return [ + heading + " " + item.title, + "", + "> " + item.summary, + "", + "- Package: " + tick + "@karnstack/dowel" + tick, + "- [HTML documentation](" + docsUrl + ")", + "- [Markdown documentation](" + markdownUrl + ")", + "", + "Import public components and types from the package root. Import " + + tick + + "@karnstack/dowel/dowel.css" + + tick + + " once at the application root.", + ].join("\n"); +} + +await mkdir(new URL("components/", publicRoot), { recursive: true }); + +for (const item of componentNav) { + await writeFile( + new URL("components/" + componentSlug(item) + ".md", publicRoot), + componentMarkdown(item) + "\n", + ); +} + +const packageMetadata = JSON.parse( + await readFile(new URL("package.json", packageRoot), "utf8"), +); +const componentLinks = componentNav + .map( + (item) => + "- [" + + item.title + + "](" + + absolute(String(item.to) + ".md") + + "): " + + item.summary, + ) + .join("\n"); + +const llms = [ + "# Dowel", + "", + "> Dowel is an opinionated React component library for compact, accessible product interfaces.", + "", + "Install " + + tick + + "@karnstack/dowel" + + tick + + ", import " + + tick + + "@karnstack/dowel/dowel.css" + + tick + + " once, and import components from the package root. Dowel targets React 19.", + "", + "## Components", + "", + componentLinks, + "", + "## Library", + "", + "- [Complete documentation](" + + absolute("/llms-full.txt") + + "): All component summaries in one Markdown document.", + "- [Component index](" + + absolute("/components/index.md") + + "): Compact component catalog.", + "- [Dependencies](" + + absolute("/dependencies.md") + + "): Runtime and peer dependency overview.", + "- [GitHub](https://github.com/karnstack/dowel): Source, issues, and releases.", + "", +].join("\n"); + +const componentDocs = componentNav + .map((item) => componentMarkdown(item, "##")) + .join("\n\n---\n\n"); +const llmsFull = [ + "# Dowel complete documentation", + "", + "> Dowel " + + packageMetadata.version + + " is an opinionated React component library for compact, accessible product interfaces.", + "", + "## Installation", + "", + tick.repeat(3) + "sh", + "pnpm add @karnstack/dowel", + tick.repeat(3), + "", + tick.repeat(3) + "tsx", + 'import "@karnstack/dowel/dowel.css";', + tick.repeat(3), + "", + "## Conventions", + "", + "- Import public components and types from " + + tick + + "@karnstack/dowel" + + tick + + ".", + "- Wrap the application in " + + tick + + "ThemeProvider" + + tick + + " when selecting light, dark, or system themes explicitly.", + "- Dowel controls own their appearance. Prefer documented props over consumer class or style overrides.", + "- Icon-only controls require an accessible " + tick + "label" + tick + ".", + "", + componentDocs, + "", +].join("\n"); + +const componentIndex = [ + "# Dowel components", + "", + "> The complete Dowel component catalog.", + "", + componentLinks, + "", +].join("\n"); + +const dependencyLines = Object.entries(packageMetadata.dependencies) + .map( + ([name, version]) => + "- " + tick + name + tick + ": " + tick + version + tick, + ) + .join("\n"); +const peerLines = Object.entries(packageMetadata.peerDependencies) + .map( + ([name, version]) => + "- " + tick + name + tick + ": " + tick + version + tick, + ) + .join("\n"); +const dependencies = [ + "# Dowel dependencies", + "", + "> Runtime packages installed with Dowel and peer runtimes owned by the consuming application.", + "", + "## Runtime dependencies", + "", + dependencyLines, + "", + "## Peer dependencies", + "", + peerLines, + "", +].join("\n"); + +const index = [ + "# Dowel", + "", + "> Compact, accessible React components for product interfaces.", + "", + "- [Agent documentation index](" + absolute("/llms.txt") + ")", + "- [Complete agent documentation](" + absolute("/llms-full.txt") + ")", + "- [Component catalog](" + absolute("/components/index.md") + ")", + "- [HTML documentation](" + absolute("/components") + ")", + "", +].join("\n"); + +await Promise.all([ + writeFile(new URL("llms.txt", publicRoot), llms), + writeFile(new URL("llms-full.txt", publicRoot), llmsFull), + writeFile(new URL("components/index.md", publicRoot), componentIndex), + writeFile(new URL("dependencies.md", publicRoot), dependencies), + writeFile(new URL("index.md", publicRoot), index), +]); diff --git a/apps/docs/src/docs.css b/apps/docs/src/docs.css index 8040ac1..6664ccf 100644 --- a/apps/docs/src/docs.css +++ b/apps/docs/src/docs.css @@ -1709,24 +1709,103 @@ body { border: 0; } -.docs-data-table-header-demo { - margin-block-start: var(--dowel-space-8); +.docs-collection-view { + position: relative; + inline-size: 100%; + min-inline-size: 0; +} + +.docs-collection-controls { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--dowel-space-3); + inline-size: 100%; + min-inline-size: 0; + padding-block-end: var(--dowel-space-3); +} + +.docs-collection-controls [data-dowel-component="filter-bar"] { + flex: 1; + min-inline-size: 0; +} + +.docs-collection-actions { + display: flex; + flex: none; + align-items: center; + gap: var(--dowel-space-2); +} + +.docs-selection-demo { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--dowel-space-8); + inline-size: 100%; + min-block-size: 5rem; +} + +.docs-form-demo { + inline-size: min(100%, 30rem); +} + +.docs-form-result { + margin: var(--dowel-space-4) 0 0; + color: var(--dowel-text-2); + font-size: var(--dowel-font-size-sm); } .docs-data-table-states { display: grid; grid-template-columns: minmax(0, 1fr); - gap: var(--dowel-space-6); + gap: var(--dowel-space-4); margin-block-start: var(--dowel-space-8); } -.docs-data-table-states > [data-dowel-component="data-table"] { - padding: var(--dowel-space-3); +.docs-data-table-state, +.docs-data-table-mode { + overflow: hidden; border: 1px solid var(--dowel-border-1); border-radius: var(--dowel-radius-lg); background-color: var(--dowel-bg-1); } +.docs-data-table-state { + margin: 0; +} + +.docs-data-table-state figcaption, +.docs-data-table-mode-heading { + display: flex; + align-items: center; + min-block-size: 2.25rem; + padding-inline: var(--dowel-space-5); + border-block-end: 1px solid var(--dowel-border-1); + color: var(--dowel-text-2); + font-size: var(--dowel-fs-mini); + font-weight: 550; +} + +.docs-data-table-state > [data-dowel-component="data-table"], +.docs-data-table-mode > [data-dowel-component="data-table"] { + padding: var(--dowel-space-2); +} + +.docs-data-table-mode { + margin-block-start: var(--dowel-space-8); +} + +.docs-data-table-mode-heading { + justify-content: space-between; + gap: var(--dowel-space-4); +} + +.docs-data-table-mode-heading > :last-child { + color: var(--dowel-text-4); + font-weight: 450; +} + .docs-issue-title { display: flex; align-items: center; diff --git a/apps/docs/src/lib/nav.ts b/apps/docs/src/lib/nav.ts index 374b325..9ad698a 100644 --- a/apps/docs/src/lib/nav.ts +++ b/apps/docs/src/lib/nav.ts @@ -80,6 +80,11 @@ export const componentNav: NavItem[] = [ to: "/components/command-menu", summary: "A modal search surface for grouped application commands.", }, + { + title: "Collection Controls", + to: "/components/collection-controls", + summary: "Filters, view options, row actions, and bulk selection controls.", + }, { title: "Composer", to: "/components/composer", @@ -115,6 +120,11 @@ export const componentNav: NavItem[] = [ to: "/components/file-upload", summary: "A native file picker and dropzone with built-in validation.", }, + { + title: "Form", + to: "/components/form", + summary: "TanStack Form with Dowel fields and actions pre-bound.", + }, { title: "Icon Button", to: "/components/icon-button", @@ -267,6 +277,7 @@ export const componentSections: NavSection[] = [ "/components/calendar", "/components/combobox", "/components/file-upload", + "/components/form", "/components/input", "/components/native-select", "/components/property-picker", @@ -327,7 +338,10 @@ export const componentSections: NavSection[] = [ }, { title: "Composites", - items: components("/components/composer"), + items: components( + "/components/collection-controls", + "/components/composer", + ), }, ]; diff --git a/apps/docs/src/routeTree.gen.ts b/apps/docs/src/routeTree.gen.ts index a68130f..a357c82 100644 --- a/apps/docs/src/routeTree.gen.ts +++ b/apps/docs/src/routeTree.gen.ts @@ -23,6 +23,7 @@ import { Route as ComponentsCalendarRouteImport } from './routes/components/cale import { Route as ComponentsCalloutRouteImport } from './routes/components/callout' import { Route as ComponentsCheckboxRouteImport } from './routes/components/checkbox' import { Route as ComponentsCollapsibleRouteImport } from './routes/components/collapsible' +import { Route as ComponentsCollectionControlsRouteImport } from './routes/components/collection-controls' import { Route as ComponentsComboboxRouteImport } from './routes/components/combobox' import { Route as ComponentsCommandMenuRouteImport } from './routes/components/command-menu' import { Route as ComponentsComposerRouteImport } from './routes/components/composer' @@ -32,6 +33,7 @@ import { Route as ComponentsDialogRouteImport } from './routes/components/dialog import { Route as ComponentsDrawerRouteImport } from './routes/components/drawer' import { Route as ComponentsEmptyStateRouteImport } from './routes/components/empty-state' import { Route as ComponentsFileUploadRouteImport } from './routes/components/file-upload' +import { Route as ComponentsFormRouteImport } from './routes/components/form' import { Route as ComponentsIconButtonRouteImport } from './routes/components/icon-button' import { Route as ComponentsInputRouteImport } from './routes/components/input' import { Route as ComponentsKbdRouteImport } from './routes/components/kbd' @@ -128,6 +130,12 @@ const ComponentsCollapsibleRoute = ComponentsCollapsibleRouteImport.update({ path: '/collapsible', getParentRoute: () => ComponentsRouteRoute, } as any) +const ComponentsCollectionControlsRoute = + ComponentsCollectionControlsRouteImport.update({ + id: '/collection-controls', + path: '/collection-controls', + getParentRoute: () => ComponentsRouteRoute, + } as any) const ComponentsComboboxRoute = ComponentsComboboxRouteImport.update({ id: '/combobox', path: '/combobox', @@ -173,6 +181,11 @@ const ComponentsFileUploadRoute = ComponentsFileUploadRouteImport.update({ path: '/file-upload', getParentRoute: () => ComponentsRouteRoute, } as any) +const ComponentsFormRoute = ComponentsFormRouteImport.update({ + id: '/form', + path: '/form', + getParentRoute: () => ComponentsRouteRoute, +} as any) const ComponentsIconButtonRoute = ComponentsIconButtonRouteImport.update({ id: '/icon-button', path: '/icon-button', @@ -314,6 +327,7 @@ export interface FileRoutesByFullPath { '/components/callout': typeof ComponentsCalloutRoute '/components/checkbox': typeof ComponentsCheckboxRoute '/components/collapsible': typeof ComponentsCollapsibleRoute + '/components/collection-controls': typeof ComponentsCollectionControlsRoute '/components/combobox': typeof ComponentsComboboxRoute '/components/command-menu': typeof ComponentsCommandMenuRoute '/components/composer': typeof ComponentsComposerRoute @@ -323,6 +337,7 @@ export interface FileRoutesByFullPath { '/components/drawer': typeof ComponentsDrawerRoute '/components/empty-state': typeof ComponentsEmptyStateRoute '/components/file-upload': typeof ComponentsFileUploadRoute + '/components/form': typeof ComponentsFormRoute '/components/icon-button': typeof ComponentsIconButtonRoute '/components/input': typeof ComponentsInputRoute '/components/kbd': typeof ComponentsKbdRoute @@ -363,6 +378,7 @@ export interface FileRoutesByTo { '/components/callout': typeof ComponentsCalloutRoute '/components/checkbox': typeof ComponentsCheckboxRoute '/components/collapsible': typeof ComponentsCollapsibleRoute + '/components/collection-controls': typeof ComponentsCollectionControlsRoute '/components/combobox': typeof ComponentsComboboxRoute '/components/command-menu': typeof ComponentsCommandMenuRoute '/components/composer': typeof ComponentsComposerRoute @@ -372,6 +388,7 @@ export interface FileRoutesByTo { '/components/drawer': typeof ComponentsDrawerRoute '/components/empty-state': typeof ComponentsEmptyStateRoute '/components/file-upload': typeof ComponentsFileUploadRoute + '/components/form': typeof ComponentsFormRoute '/components/icon-button': typeof ComponentsIconButtonRoute '/components/input': typeof ComponentsInputRoute '/components/kbd': typeof ComponentsKbdRoute @@ -414,6 +431,7 @@ export interface FileRoutesById { '/components/callout': typeof ComponentsCalloutRoute '/components/checkbox': typeof ComponentsCheckboxRoute '/components/collapsible': typeof ComponentsCollapsibleRoute + '/components/collection-controls': typeof ComponentsCollectionControlsRoute '/components/combobox': typeof ComponentsComboboxRoute '/components/command-menu': typeof ComponentsCommandMenuRoute '/components/composer': typeof ComponentsComposerRoute @@ -423,6 +441,7 @@ export interface FileRoutesById { '/components/drawer': typeof ComponentsDrawerRoute '/components/empty-state': typeof ComponentsEmptyStateRoute '/components/file-upload': typeof ComponentsFileUploadRoute + '/components/form': typeof ComponentsFormRoute '/components/icon-button': typeof ComponentsIconButtonRoute '/components/input': typeof ComponentsInputRoute '/components/kbd': typeof ComponentsKbdRoute @@ -466,6 +485,7 @@ export interface FileRouteTypes { | '/components/callout' | '/components/checkbox' | '/components/collapsible' + | '/components/collection-controls' | '/components/combobox' | '/components/command-menu' | '/components/composer' @@ -475,6 +495,7 @@ export interface FileRouteTypes { | '/components/drawer' | '/components/empty-state' | '/components/file-upload' + | '/components/form' | '/components/icon-button' | '/components/input' | '/components/kbd' @@ -515,6 +536,7 @@ export interface FileRouteTypes { | '/components/callout' | '/components/checkbox' | '/components/collapsible' + | '/components/collection-controls' | '/components/combobox' | '/components/command-menu' | '/components/composer' @@ -524,6 +546,7 @@ export interface FileRouteTypes { | '/components/drawer' | '/components/empty-state' | '/components/file-upload' + | '/components/form' | '/components/icon-button' | '/components/input' | '/components/kbd' @@ -565,6 +588,7 @@ export interface FileRouteTypes { | '/components/callout' | '/components/checkbox' | '/components/collapsible' + | '/components/collection-controls' | '/components/combobox' | '/components/command-menu' | '/components/composer' @@ -574,6 +598,7 @@ export interface FileRouteTypes { | '/components/drawer' | '/components/empty-state' | '/components/file-upload' + | '/components/form' | '/components/icon-button' | '/components/input' | '/components/kbd' @@ -708,6 +733,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ComponentsCollapsibleRouteImport parentRoute: typeof ComponentsRouteRoute } + '/components/collection-controls': { + id: '/components/collection-controls' + path: '/collection-controls' + fullPath: '/components/collection-controls' + preLoaderRoute: typeof ComponentsCollectionControlsRouteImport + parentRoute: typeof ComponentsRouteRoute + } '/components/combobox': { id: '/components/combobox' path: '/combobox' @@ -771,6 +803,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ComponentsFileUploadRouteImport parentRoute: typeof ComponentsRouteRoute } + '/components/form': { + id: '/components/form' + path: '/form' + fullPath: '/components/form' + preLoaderRoute: typeof ComponentsFormRouteImport + parentRoute: typeof ComponentsRouteRoute + } '/components/icon-button': { id: '/components/icon-button' path: '/icon-button' @@ -960,6 +999,7 @@ interface ComponentsRouteRouteChildren { ComponentsCalloutRoute: typeof ComponentsCalloutRoute ComponentsCheckboxRoute: typeof ComponentsCheckboxRoute ComponentsCollapsibleRoute: typeof ComponentsCollapsibleRoute + ComponentsCollectionControlsRoute: typeof ComponentsCollectionControlsRoute ComponentsComboboxRoute: typeof ComponentsComboboxRoute ComponentsCommandMenuRoute: typeof ComponentsCommandMenuRoute ComponentsComposerRoute: typeof ComponentsComposerRoute @@ -969,6 +1009,7 @@ interface ComponentsRouteRouteChildren { ComponentsDrawerRoute: typeof ComponentsDrawerRoute ComponentsEmptyStateRoute: typeof ComponentsEmptyStateRoute ComponentsFileUploadRoute: typeof ComponentsFileUploadRoute + ComponentsFormRoute: typeof ComponentsFormRoute ComponentsIconButtonRoute: typeof ComponentsIconButtonRoute ComponentsInputRoute: typeof ComponentsInputRoute ComponentsKbdRoute: typeof ComponentsKbdRoute @@ -1008,6 +1049,7 @@ const ComponentsRouteRouteChildren: ComponentsRouteRouteChildren = { ComponentsCalloutRoute: ComponentsCalloutRoute, ComponentsCheckboxRoute: ComponentsCheckboxRoute, ComponentsCollapsibleRoute: ComponentsCollapsibleRoute, + ComponentsCollectionControlsRoute: ComponentsCollectionControlsRoute, ComponentsComboboxRoute: ComponentsComboboxRoute, ComponentsCommandMenuRoute: ComponentsCommandMenuRoute, ComponentsComposerRoute: ComponentsComposerRoute, @@ -1017,6 +1059,7 @@ const ComponentsRouteRouteChildren: ComponentsRouteRouteChildren = { ComponentsDrawerRoute: ComponentsDrawerRoute, ComponentsEmptyStateRoute: ComponentsEmptyStateRoute, ComponentsFileUploadRoute: ComponentsFileUploadRoute, + ComponentsFormRoute: ComponentsFormRoute, ComponentsIconButtonRoute: ComponentsIconButtonRoute, ComponentsInputRoute: ComponentsInputRoute, ComponentsKbdRoute: ComponentsKbdRoute, diff --git a/apps/docs/src/routes/__root.tsx b/apps/docs/src/routes/__root.tsx index f7b664e..fb6aab8 100644 --- a/apps/docs/src/routes/__root.tsx +++ b/apps/docs/src/routes/__root.tsx @@ -43,7 +43,14 @@ export const Route = createRootRoute({ content: "Compact, accessible React components for product interfaces.", }, ], - links: faviconLinks, + links: [ + ...faviconLinks, + { + rel: "describedby", + href: "/llms.txt", + type: "text/markdown", + }, + ], }), component: RootDocument, }); diff --git a/apps/docs/src/routes/components/collection-controls.tsx b/apps/docs/src/routes/components/collection-controls.tsx new file mode 100644 index 0000000..a7afe03 --- /dev/null +++ b/apps/docs/src/routes/components/collection-controls.tsx @@ -0,0 +1,194 @@ +import { + ArchiveBoxIcon, + EllipsisHorizontalIcon, +} from "@heroicons/react/16/solid"; +import { createFileRoute } from "@tanstack/react-router"; +import { + Button, + FilterBar, + FilterPicker, + HoverActions, + IconButton, + MultiSelectToolbar, + ViewOptions, +} from "@karnstack/dowel"; +import type { + DataTableDensity, + FilterBarFilter, + FilterPickerProperty, +} from "@karnstack/dowel"; +import { useState } from "react"; + +import { Demo } from "../../components/demo"; +import { DocsPage, Section } from "../../components/docs-page"; + +export const Route = createFileRoute("/components/collection-controls")({ + component: CollectionControlsDocs, +}); + +const toc = [ + { id: "filters", title: "Filters and views" }, + { id: "selection", title: "Bulk selection" }, + { id: "row-actions", title: "Row actions" }, +]; + +const initialFilters: FilterBarFilter[] = [ + { id: "status", label: "Status", value: "Active" }, + { id: "assignee", label: "Assignee", value: "Me" }, +]; + +const filterProperties: FilterPickerProperty[] = [ + { + id: "status", + label: "Status", + values: [ + { value: "active", label: "Active" }, + { value: "done", label: "Completed" }, + ], + }, + { + id: "assignee", + label: "Assignee", + values: [ + { value: "me", label: "Me" }, + { value: "unassigned", label: "Unassigned" }, + ], + }, +]; + +function CollectionControlsDocs() { + const [filters, setFilters] = useState(initialFilters); + const [density, setDensity] = useState("compact"); + const [selectedCount, setSelectedCount] = useState(3); + + function addFilter(propertyId: string, value: string) { + const property = filterProperties.find((item) => item.id === propertyId); + const option = property?.values.find((item) => item.value === value); + if (!property || !option) return; + setFilters((current) => [ + ...current.filter((filter) => filter.id !== propertyId), + { id: propertyId, label: property.label, value: option.label }, + ]); + } + + return ( + +
+

+ Keep active constraints visible and place display preferences at the + collection edge. Both components are controlled, so saved views can + own the same state. +

+ +
+ + +
`} + > +
+ + setFilters((current) => + current.filter((filter) => filter.id !== id), + ) + } + onClear={() => setFilters([])} + /> +
+ + {}} + /> +
+
+
+
+ +
+

+ The toolbar appears only while items are selected. Keep its actions + specific to the selected set and always provide a way back to the + unselected state. +

+ + +`} + > +
+ + setSelectedCount(0)} + > + + +
+
+
+ +
+

+ Hover actions reserve no visual attention until the row is hovered or + focus moves inside it. Pass visible when a parent row + tracks pointer state. +

+ + + + +`} + > + + + + + + +
+
+ ); +} diff --git a/apps/docs/src/routes/components/data-table.tsx b/apps/docs/src/routes/components/data-table.tsx index 790ddde..3a68dc2 100644 --- a/apps/docs/src/routes/components/data-table.tsx +++ b/apps/docs/src/routes/components/data-table.tsx @@ -1,9 +1,31 @@ +import { + ArchiveBoxIcon, + CalendarDaysIcon, + EllipsisHorizontalIcon, + QueueListIcon, + UserGroupIcon, +} from "@heroicons/react/16/solid"; import { createFileRoute } from "@tanstack/react-router"; import { Badge, + Button, DataTable, + FilterBar, + FilterPicker, + IconButton, + MultiSelectToolbar, + ViewOptions, createDataTableColumnHelper, } from "@karnstack/dowel"; +import type { + DataTableColumnVisibilityState, + DataTableDensity, + DataTableRowSelectionState, + DataTableSortingState, + FilterBarFilter, + FilterPickerProperty, +} from "@karnstack/dowel"; +import { useState } from "react"; import { Demo } from "../../components/demo"; import { DocsPage, Section } from "../../components/docs-page"; @@ -58,6 +80,12 @@ const issues: Issue[] = [ }, ]; +const statusLabels: Record = { + started: "In progress", + planned: "Planned", + done: "Completed", +}; + const helper = createDataTableColumnHelper(); const columns = helper.columns([ helper.accessor("id", { @@ -84,23 +112,209 @@ const columns = helper.columns([ }), helper.accessor("updated", { header: "Updated", - size: 72, + size: 84, meta: { align: "end", tone: "tertiary" }, }), ]); +const initialFilters: FilterBarFilter[] = [ + { id: "status", label: "Status", value: "Active" }, + { id: "team", label: "Team", value: "All teams" }, +]; + +const filterProperties: FilterPickerProperty[] = [ + { + id: "status", + label: "Status", + icon: , + values: [ + { value: "active", label: "Active", count: 3 }, + { value: "planned", label: "Planned", count: 2 }, + { value: "done", label: "Completed", count: 2 }, + ], + }, + { + id: "team", + label: "Team", + icon: , + values: [ + { value: "platform", label: "Platform", count: 2 }, + { value: "growth", label: "Growth", count: 1 }, + { value: "product", label: "Product", count: 1 }, + { value: "performance", label: "Performance", count: 1 }, + ], + }, + { + id: "updated", + label: "Updated", + icon: , + values: [ + { value: "today", label: "Today" }, + { value: "week", label: "Past week" }, + { value: "month", label: "Past month" }, + ], + }, +]; + const toc = [ - { id: "issue-list", title: "Issue list" }, + { id: "collection-view", title: "Collection view" }, { id: "states", title: "States" }, { id: "behavior", title: "Behavior" }, ]; -function IssueTable({ showHeader = false }: { showHeader?: boolean }) { +function CollectionView() { + const [density, setDensity] = useState("compact"); + const [grouping, setGrouping] = useState("status"); + const [ordering, setOrdering] = useState("updated"); + const [sorting, setSorting] = useState([ + { id: "updated", desc: false }, + ]); + const [columnVisibility, setColumnVisibility] = + useState({}); + const [rowSelection, setRowSelection] = useState( + {}, + ); + const [filters, setFilters] = useState(initialFilters); + + function updateOrdering(value: string) { + setOrdering(value); + setSorting( + value === "title" + ? [{ id: "title", desc: false }] + : [{ id: "updated", desc: false }], + ); + } + + const selectedCount = Object.values(rowSelection).filter(Boolean).length; + + function addFilter(propertyId: string, value: string) { + const property = filterProperties.find((item) => item.id === propertyId); + const option = property?.values.find((item) => item.value === value); + if (!property || !option) return; + setFilters((current) => [ + ...current.filter((filter) => filter.id !== propertyId), + { id: propertyId, label: property.label, value: option.label }, + ]); + } + + return ( +
+
+ + setFilters((current) => + current.filter((filter) => filter.id !== id), + ) + } + onClear={() => setFilters([])} + /> +
+ + + setColumnVisibility((current) => ({ ...current, [id]: visible })) + } + onReset={() => { + setGrouping("status"); + updateOrdering("updated"); + setDensity("compact"); + setColumnVisibility({}); + }} + /> +
+
+ issue.id} + density={density} + groupBy={ + grouping === "status" + ? (issue) => ({ + id: issue.status, + label: statusLabels[issue.status], + }) + : grouping === "team" + ? (issue) => ({ id: issue.team, label: issue.team }) + : undefined + } + columnPinning={{ start: ["id"], end: ["updated"] }} + columnVisibility={columnVisibility} + onColumnVisibilityChange={setColumnVisibility} + sorting={sorting} + onSortingChange={setSorting} + rowSelection={rowSelection} + onRowSelectionChange={setRowSelection} + selectable + showHeader={false} + renderRowActions={(issue) => ( + + + + )} + style={{ maxHeight: "24rem" }} + /> + setRowSelection({})} + > + + +
+ ); +} + +function IssueTable({ + data = issues, + showHeader = false, +}: { + data?: Issue[]; + showHeader?: boolean; +}) { return ( issue.id} selectable showHeader={showHeader} @@ -112,38 +326,42 @@ function DataTableDocs() { return ( -
+

- Rows sit directly on the page without a boxed grid. Identity stays on - the left, the title absorbs available space, and metadata remains - quiet on the right. + Compose the table with filters, display settings, and bulk actions to + build a complete issue view. The collection state remains controlled, + so it can be stored in a URL, workspace preference, or saved view.

(); -const columns = column.columns([ - column.accessor("id", { header: "Issue", size: 96 }), - column.accessor("title", { header: "Title", meta: { grow: true } }), - column.accessor("updated", { header: "Updated", meta: { align: "end" } }), -]); - + code={` +
+ + +
issue.id} + groupBy={(issue) => ({ id: issue.status, label: issue.status })} + columnPinning={{ start: ["id"], end: ["updated"] }} + rowSelection={rowSelection} + onRowSelectionChange={setRowSelection} + renderRowActions={(issue) => } selectable showHeader={false} -/>`} +/> +`} > -
- -
+
@@ -153,32 +371,44 @@ const columns = column.columns([ message instead of a large illustration or promotional copy.

- - +
+
Loading
+ +
+
+
Empty result
+ +

- Show the header to expose sorting and column resizing. Sort controls, - selection boxes, rows, and resize handles all carry visible keyboard - focus. Resize with the pointer or Left and Right Arrow, then double - click the handle to restore its original width. + Show the header to expose sorting and column resizing. Rows support + Arrow, Home, End, Space, Enter, and Shift-range selection. Resize with + the pointer or Arrow keys, then double click the handle to restore its + original width.

-
- +
+
+ Table mode + Sortable and resizable columns +
+
diff --git a/apps/docs/src/routes/components/form.tsx b/apps/docs/src/routes/components/form.tsx new file mode 100644 index 0000000..2f6dd7c --- /dev/null +++ b/apps/docs/src/routes/components/form.tsx @@ -0,0 +1,122 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { useDowelForm } from "@karnstack/dowel"; +import { useState } from "react"; + +import { Demo } from "../../components/demo"; +import { DocsPage, Section } from "../../components/docs-page"; + +export const Route = createFileRoute("/components/form")({ + component: FormDocs, +}); + +const toc = [ + { id: "composed-form", title: "Composed form" }, + { id: "field-components", title: "Field components" }, +]; + +function IssueForm() { + const [savedTitle, setSavedTitle] = useState(""); + const form = useDowelForm({ + defaultValues: { + title: "", + description: "", + }, + onSubmit: ({ value }) => setSavedTitle(value.title), + }); + + return ( +
+ + + + value.trim() ? undefined : "Title is required", + }} + > + {(field) => ( + + )} + + + {(field) => ( + + )} + + + Create issue + + + + {savedTitle ? ( +

+ Created “{savedTitle}” +

+ ) : null} +
+ ); +} + +function FormDocs() { + return ( + +
+

+ useDowelForm keeps TanStack Form's typed state model + while removing the repetitive wiring between fields and Dowel + controls. +

+ createIssue(value), +}); + + + + value ? undefined : "Title is required", + }} + > + {(field) => } + + + {(field) => } + + + Create issue + + +`} + > + + +
+ +
+

+ The adapter currently includes TextField and{" "} + TextareaField. Both bind value, blur, name, touched + validation, descriptions, and accessible error output. Use{" "} + withDowelForm and withDowelFieldGroup to + compose reusable forms without losing inferred value types. +

+
+
+ ); +} diff --git a/apps/docs/src/routes/dependencies.tsx b/apps/docs/src/routes/dependencies.tsx index df7c4f8..8f4cf19 100644 --- a/apps/docs/src/routes/dependencies.tsx +++ b/apps/docs/src/routes/dependencies.tsx @@ -35,6 +35,11 @@ const runtimeDependencies = [ version: packageMetadata.dependencies["@tanstack/react-table"], role: "Its headless, application-controlled state model handles advanced table behavior without coupling Dowel to a table layout or data-fetching opinion.", }, + { + name: "@tanstack/react-form", + version: packageMetadata.dependencies["@tanstack/react-form"], + role: "Its typed, headless state and validation model powers Dowel's pre-bound form hook while leaving schema choice and submission behavior application-owned.", + }, { name: "@internationalized/date", version: packageMetadata.dependencies["@internationalized/date"], @@ -132,7 +137,7 @@ function DependenciesDocs() {

- Dowel does not install a router, form library, or data-fetching + Dowel does not install a router, schema validator, or data-fetching client, and it does not prescribe an application-wide icon system. Consumer-facing icon slots accept ordinary React content, so applications keep control of their visual language. diff --git a/apps/docs/worker.mjs b/apps/docs/worker.mjs new file mode 100644 index 0000000..257de15 --- /dev/null +++ b/apps/docs/worker.mjs @@ -0,0 +1,46 @@ +const markdownType = "text/markdown; charset=utf-8"; +const llmsLink = '; rel="describedby"; type="text/markdown"'; + +function wantsMarkdown(request) { + return request.headers + .get("Accept") + ?.toLowerCase() + .split(",") + .some((value) => value.trim().startsWith("text/markdown")); +} + +function markdownPath(pathname) { + const path = pathname.replace(/\/+$/, "") || "/"; + if (path === "/") return "/index.md"; + if (path === "/components") return "/components/index.md"; + return path + ".md"; +} + +function withDiscoveryHeaders(response, markdown = false) { + const headers = new Headers(response.headers); + headers.set("Link", llmsLink); + headers.append("Vary", "Accept"); + if (markdown) headers.set("Content-Type", markdownType); + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); +} + +export default { + async fetch(request, env) { + const url = new URL(request.url); + if (request.method === "GET" && wantsMarkdown(request)) { + const markdownUrl = new URL(markdownPath(url.pathname), url); + const markdownResponse = await env.ASSETS.fetch( + new Request(markdownUrl, request), + ); + if (markdownResponse.ok) { + return withDiscoveryHeaders(markdownResponse, true); + } + } + + return withDiscoveryHeaders(await env.ASSETS.fetch(request)); + }, +}; diff --git a/apps/docs/wrangler.jsonc b/apps/docs/wrangler.jsonc index 901b59a..8fc1e51 100644 --- a/apps/docs/wrangler.jsonc +++ b/apps/docs/wrangler.jsonc @@ -1,13 +1,15 @@ -// dowel.sh — the docs site, served as Cloudflare Worker static assets. -// Assets-only: no worker script, because the site is fully prerendered. +// dowel.sh — prerendered docs with a small content-negotiation Worker. // // www.dowel.sh cannot be redirected from an assets-only worker; that is a // Cloudflare dashboard Redirect Rule (manual follow-up). { "name": "dowel-sh", - "compatibility_date": "2026-08-09", + "main": "./worker.mjs", + "compatibility_date": "2026-08-08", "assets": { "directory": "./dist", + "binding": "ASSETS", + "run_worker_first": true, // The build emits sub-pages as directories (components/button/index.html), // so /components/button must resolve to that file. auto-trailing-slash // serves /components/button/ directly and 307s /components/button to it. diff --git a/packages/dowel/package.json b/packages/dowel/package.json index 311d97a..b963a3f 100644 --- a/packages/dowel/package.json +++ b/packages/dowel/package.json @@ -55,6 +55,7 @@ "@heroicons/react": "2.2.0", "@internationalized/date": "^3.12.3", "@stylexjs/stylex": "0.19.0", + "@tanstack/react-form": "^1.33.5", "@tanstack/react-table": "^9.1.2", "react-aria-components": "^1.20.0" }, diff --git a/packages/dowel/src/components/badge/badge.stylex.ts b/packages/dowel/src/components/badge/badge.stylex.ts index 7b9d733..a6a82b2 100644 --- a/packages/dowel/src/components/badge/badge.stylex.ts +++ b/packages/dowel/src/components/badge/badge.stylex.ts @@ -5,7 +5,7 @@ import { tokens } from "../../theme/tokens.stylex"; export const badge = stylex.create({ root: { alignItems: "center", - backgroundColor: tokens["--dowel-bg-surface-2"], + backgroundColor: tokens["--dowel-bg-badge"], borderColor: tokens["--dowel-border-default"], borderRadius: tokens["--dowel-radius-pill"], borderStyle: "solid", diff --git a/packages/dowel/src/components/data-table/data-table.stylex.ts b/packages/dowel/src/components/data-table/data-table.stylex.ts index 1847387..39e677a 100644 --- a/packages/dowel/src/components/data-table/data-table.stylex.ts +++ b/packages/dowel/src/components/data-table/data-table.stylex.ts @@ -27,6 +27,12 @@ export const parts = stylex.create({ headerRow: { backgroundColor: "transparent", }, + stickyHeader: { + backgroundColor: tokens["--dowel-bg-surface-1"], + insetBlockStart: 0, + position: "sticky", + zIndex: 4, + }, headerCell: { boxSizing: "border-box", color: tokens["--dowel-text-tertiary"], @@ -41,11 +47,16 @@ export const parts = stylex.create({ verticalAlign: "middle", whiteSpace: "nowrap", }, + pinnedHeaderCell: { + backgroundColor: tokens["--dowel-bg-surface-1"], + position: "sticky", + zIndex: 5, + }, headerButton: { alignItems: "center", appearance: "none", backgroundColor: "transparent", - border: 0, + borderStyle: "none", borderRadius: tokens["--dowel-radius-sm"], color: "inherit", cursor: "pointer", @@ -95,7 +106,7 @@ export const parts = stylex.create({ }, resizeHandle: { backgroundColor: "transparent", - border: 0, + borderStyle: "none", cursor: "col-resize", height: "1.25rem", outline: "none", @@ -144,10 +155,10 @@ export const parts = stylex.create({ transitionProperty: "background-color", transitionTimingFunction: tokens["--dowel-ease-out"], ":hover": { - backgroundColor: tokens["--dowel-bg-hover"], + backgroundColor: tokens["--dowel-bg-row-hover"], }, ":focus-visible": { - backgroundColor: tokens["--dowel-bg-hover"], + backgroundColor: tokens["--dowel-bg-row-hover"], boxShadow: `inset 0 0 0 1px ${tokens["--dowel-focus-ring"]}`, }, "[data-selected]": { @@ -162,11 +173,11 @@ export const parts = stylex.create({ boxSizing: "border-box", color: tokens["--dowel-text-primary"], fontSize: "0.8125rem", - fontWeight: 450, - letterSpacing: "-0.0075rem", - lineHeight: 1.35, + fontWeight: 500, + letterSpacing: "-0.005rem", + lineHeight: 1.2, paddingBlock: 0, - paddingInline: "0.5rem", + paddingInline: "0.375rem", textAlign: "left", verticalAlign: "middle", ":first-child": { @@ -178,8 +189,22 @@ export const parts = stylex.create({ borderTopRightRadius: tokens["--dowel-radius-md"], }, }, + pinnedCell: { + position: "sticky", + zIndex: 2, + }, + pinnedStartEdge: { + borderInlineEndColor: tokens["--dowel-border-default"], + borderInlineEndStyle: "solid", + borderInlineEndWidth: tokens["--dowel-hairline"], + }, + pinnedEndEdge: { + borderInlineStartColor: tokens["--dowel-border-default"], + borderInlineStartStyle: "solid", + borderInlineStartWidth: tokens["--dowel-hairline"], + }, compactCell: { - height: tokens["--dowel-row-md"], + height: "2.5rem", }, comfortableCell: { height: "3.25rem", @@ -198,6 +223,70 @@ export const parts = stylex.create({ selectionCell: { paddingInline: "0.375rem", }, + groupRow: { + backgroundColor: "transparent", + }, + groupCell: { + backgroundColor: tokens["--dowel-bg-surface-1"], + borderRadius: tokens["--dowel-radius-md"], + boxSizing: "border-box", + boxShadow: `inset 0 0 0 ${tokens["--dowel-hairline"]} ${tokens["--dowel-border-subtle"]}`, + padding: "0.125rem 0.375rem", + }, + groupButton: { + alignItems: "center", + appearance: "none", + backgroundColor: "transparent", + borderStyle: "none", + borderRadius: tokens["--dowel-radius-sm"], + color: tokens["--dowel-text-secondary"], + cursor: "pointer", + display: "inline-flex", + fontFamily: tokens["--dowel-font-sans"], + fontSize: "0.75rem", + fontWeight: 550, + gap: "0.25rem", + height: "1.75rem", + maxWidth: "100%", + outline: "none", + paddingInline: "0.25rem 0.5rem", + ":hover": { + backgroundColor: tokens["--dowel-bg-hover"], + color: tokens["--dowel-text-primary"], + }, + ":focus-visible": { + boxShadow: `inset 0 0 0 1px ${tokens["--dowel-focus-ring"]}`, + color: tokens["--dowel-text-primary"], + }, + }, + groupChevron: { + alignItems: "center", + display: "inline-flex", + flexShrink: 0, + height: "0.75rem", + justifyContent: "center", + width: "0.75rem", + }, + groupChevronExpanded: { + transform: "rotate(90deg)", + }, + groupChevronGlyph: { + backgroundColor: "currentColor", + clipPath: "polygon(15% 10%, 85% 50%, 15% 90%)", + display: "block", + height: "0.5rem", + width: "0.4375rem", + }, + groupLabel: { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }, + groupCount: { + color: tokens["--dowel-text-tertiary"], + fontSize: "0.6875rem", + fontWeight: 450, + }, alignCenter: { justifyContent: "center", textAlign: "center", @@ -264,7 +353,7 @@ export const parts = stylex.create({ }, emptyCell: { color: tokens["--dowel-text-secondary"], - padding: "3.5rem 1rem", + padding: "2.5rem 1rem", textAlign: "center", }, emptyTitle: { @@ -287,6 +376,7 @@ export const parts = stylex.create({ animationTimingFunction: "ease-in-out", backgroundColor: tokens["--dowel-bg-surface-3"], borderRadius: tokens["--dowel-radius-pill"], + display: "block", height: "0.375rem", maxWidth: "12rem", width: "68%", diff --git a/packages/dowel/src/components/data-table/data-table.test.tsx b/packages/dowel/src/components/data-table/data-table.test.tsx index 48b5fc8..006d353 100644 --- a/packages/dowel/src/components/data-table/data-table.test.tsx +++ b/packages/dowel/src/components/data-table/data-table.test.tsx @@ -70,6 +70,13 @@ describe("DataTable", () => { ).toBe("ascending"); }); + it("sorts text columns with the registered automatic comparator", async () => { + render(); + const rows = screen.getAllByRole("row").slice(1); + expect(within(rows[0]!).getByText("Add keyboard navigation")).toBeDefined(); + expect(within(rows[1]!).getByText("Polish the empty state")).toBeDefined(); + }); + it("selects one row and all rows with accessible checkboxes", async () => { const onRowSelectionChange = vi.fn(); render(); @@ -99,6 +106,77 @@ describe("DataTable", () => { expect(handle.getAttribute("aria-valuenow")).toBe("48"); }); + it("supports controlled column order, pinning, and sizing", async () => { + const onColumnSizingChange = vi.fn(); + render( + , + ); + const headers = screen.getAllByRole("columnheader"); + expect(within(headers[0]!).getByText("Priority")).toBeDefined(); + expect(headers[0]?.getAttribute("style")).toContain("inset-inline-start"); + + const handle = screen.getByRole("separator", { + name: "Resize priority column", + }); + handle.focus(); + await userEvent.keyboard("{ArrowRight}"); + expect(onColumnSizingChange).toHaveBeenCalled(); + }); + + it("renders collapsible row groups", async () => { + render( + ({ + id: String(issue.priority), + label: `Priority ${issue.priority}`, + })} + />, + ); + const group = screen.getByRole("button", { name: /Priority 21/ }); + expect(group.getAttribute("aria-expanded")).toBe("true"); + expect(screen.getAllByRole("row")).toHaveLength(5); + await userEvent.click(group); + expect(group.getAttribute("aria-expanded")).toBe("false"); + expect(screen.getAllByRole("row")).toHaveLength(4); + expect(screen.queryByText("Add keyboard navigation")).toBeNull(); + }); + + it("uses roving row focus and shift-arrow range selection", async () => { + render(); + const rows = screen.getAllByRole("row").slice(1); + (rows[0] as HTMLTableRowElement).focus(); + await userEvent.keyboard("{Shift>}{ArrowDown}{/Shift}"); + expect(document.activeElement).toBe(rows[1]); + const checkboxes = screen.getAllByRole("checkbox").slice(1); + expect((checkboxes[0] as HTMLInputElement).checked).toBe(true); + expect((checkboxes[1] as HTMLInputElement).checked).toBe(true); + }); + + it("reveals row actions on hover and supports sticky headers", async () => { + render( + ( + + )} + />, + ); + const root = screen.getByRole("table").parentElement!; + expect(root.hasAttribute("data-sticky-header")).toBe(true); + const firstRow = screen.getAllByRole("row")[1]!; + await userEvent.hover(firstRow); + expect( + screen + .getByRole("toolbar", { name: "Actions for row 1" }) + .hasAttribute("data-visible"), + ).toBe(true); + }); + it("activates actionable rows without hijacking embedded controls", async () => { const onRowActivate = vi.fn(); render(); diff --git a/packages/dowel/src/components/data-table/index.tsx b/packages/dowel/src/components/data-table/index.tsx index 0ff8770..b0d08a6 100644 --- a/packages/dowel/src/components/data-table/index.tsx +++ b/packages/dowel/src/components/data-table/index.tsx @@ -1,4 +1,6 @@ import { + columnOrderingFeature, + columnPinningFeature, columnResizingFeature, columnSizingFeature, columnVisibilityFeature, @@ -6,11 +8,17 @@ import { createSortedRowModel, rowSelectionFeature, rowSortingFeature, + sortFn_alphanumeric, + sortFn_text, tableFeatures, useTable, } from "@tanstack/react-table"; import type { ColumnDef, + ColumnOrderState, + ColumnPinningPosition, + ColumnPinningState, + ColumnSizingState, ColumnVisibilityState, OnChangeFn, RowData, @@ -20,21 +28,36 @@ import type { } from "@tanstack/react-table"; import { CheckIcon, ChevronUpIcon, MinusIcon } from "@heroicons/react/16/solid"; import * as stylex from "@stylexjs/stylex"; -import { forwardRef, useEffect, useMemo, useRef, useState } from "react"; +import { + Fragment, + forwardRef, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import type { ChangeEvent, ComponentPropsWithoutRef, CSSProperties, + FocusEvent, KeyboardEvent, MouseEvent, ReactNode, } from "react"; +import { HoverActions } from "../hover-actions"; import * as styles from "./data-table.stylex"; export type DataTableDensity = "compact" | "comfortable"; export type DataTableAlign = "start" | "center" | "end"; export type DataTableCellTone = "primary" | "secondary" | "tertiary"; +export type DataTableColumnOrderState = ColumnOrderState; +export type DataTableColumnPinningState = ColumnPinningState; +export type DataTableColumnSizingState = ColumnSizingState; +export type DataTableColumnVisibilityState = ColumnVisibilityState; +export type DataTableRowSelectionState = RowSelectionState; +export type DataTableSortingState = SortingState; export type DataTableColumnMeta = { /** Cell and header alignment. */ @@ -49,14 +72,34 @@ export type DataTableColumnMeta = { truncate?: boolean; }; +export type DataTableGroup = { + /** Stable identity used by collapsed-group state. */ + id: string; + /** Visible group heading. */ + label: ReactNode; +}; + +export type DataTableGroupContext = { + collapsed: boolean; + group: DataTableGroup; + rows: ReadonlyArray; + toggle: () => void; +}; + const dataTableFeatures = tableFeatures({ columnMeta: {} as DataTableColumnMeta, columnResizingFeature, columnSizingFeature, + columnOrderingFeature, + columnPinningFeature, columnVisibilityFeature, rowSelectionFeature, rowSortingFeature, sortedRowModel: createSortedRowModel(), + sortFns: { + alphanumeric: sortFn_alphanumeric, + text: sortFn_text, + }, }); export type DataTableColumnDef< @@ -73,8 +116,16 @@ type DivProps = ComponentPropsWithoutRef<"div">; export interface DataTableProps extends Omit { "aria-label": string; + collapsedGroupIds?: ReadonlyArray; + columnOrder?: ColumnOrderState; + columnPinning?: ColumnPinningState; + columnSizing?: ColumnSizingState; columns: ReadonlyArray>; data: ReadonlyArray; + defaultCollapsedGroupIds?: ReadonlyArray; + defaultColumnOrder?: ColumnOrderState; + defaultColumnPinning?: ColumnPinningState; + defaultColumnSizing?: ColumnSizingState; defaultColumnVisibility?: ColumnVisibilityState; defaultRowSelection?: RowSelectionState; defaultSorting?: SortingState; @@ -82,17 +133,29 @@ export interface DataTableProps emptyDescription?: ReactNode; emptyTitle?: ReactNode; getRowId?: (row: TData, index: number) => string; + /** Groups the final sorted rows into collapsible sections. */ + groupBy?: (row: TData) => DataTableGroup | null; + /** Enables roving Arrow/Home/End row focus. Defaults to true. */ + keyboardNavigation?: boolean; loading?: boolean; loadingRows?: number; + onCollapsedGroupIdsChange?: (groupIds: ReadonlyArray) => void; + onColumnOrderChange?: OnChangeFn; + onColumnPinningChange?: OnChangeFn; + onColumnSizingChange?: OnChangeFn; onColumnVisibilityChange?: OnChangeFn; onRowActivate?: (row: TData) => void; onRowSelectionChange?: OnChangeFn; onSortingChange?: OnChangeFn; resizable?: boolean; + /** Optional actions revealed when a row is hovered or contains focus. */ + renderRowActions?: (row: TData) => ReactNode; + renderGroupHeader?: (context: DataTableGroupContext) => ReactNode; rowSelection?: RowSelectionState; selectable?: boolean; showHeader?: boolean; sorting?: SortingState; + stickyHeader?: boolean; columnVisibility?: ColumnVisibilityState; } @@ -206,16 +269,39 @@ function triggerRow( onRowActivate: ((row: TData) => void) | undefined, ) { if (!onRowActivate || isInteractiveTarget(event.target)) return; - if ("key" in event && event.key !== "Enter" && event.key !== " ") return; + if ("key" in event && event.key !== "Enter") return; event.preventDefault(); onRowActivate(row); } +function pinnedPositionStyle(column: { + getAfter: (position?: ColumnPinningPosition | "center") => number; + getIsPinned: () => ColumnPinningPosition; + getStart: (position?: ColumnPinningPosition | "center") => number; +}): CSSProperties | undefined { + const pinned = column.getIsPinned(); + if (pinned === "start") { + return { insetInlineStart: column.getStart("start") }; + } + if (pinned === "end") { + return { insetInlineEnd: column.getAfter("end") }; + } + return undefined; +} + export const DataTable = forwardRef(function DataTable( { "aria-label": ariaLabel, + collapsedGroupIds, + columnOrder, + columnPinning, + columnSizing, columns, data, + defaultCollapsedGroupIds, + defaultColumnOrder, + defaultColumnPinning, + defaultColumnSizing, defaultColumnVisibility, defaultRowSelection, defaultSorting, @@ -223,17 +309,26 @@ export const DataTable = forwardRef(function DataTable( emptyDescription, emptyTitle = "No results", getRowId, + groupBy, + keyboardNavigation = true, loading = false, loadingRows = 5, + onCollapsedGroupIdsChange, + onColumnOrderChange, + onColumnPinningChange, + onColumnSizingChange, onColumnVisibilityChange, onRowActivate, onRowSelectionChange, onSortingChange, resizable = true, + renderGroupHeader, + renderRowActions, rowSelection, selectable = false, showHeader = true, sorting, + stickyHeader = false, columnVisibility, className, style, @@ -241,6 +336,12 @@ export const DataTable = forwardRef(function DataTable( }: DataTableProps, ref: React.ForwardedRef, ) { + const selectionAnchorRef = useRef(null); + const rowRefs = useRef(new Map()); + const [activeRowId, setActiveRowId] = useState(null); + const [activeActionsRowId, setActiveActionsRowId] = useState( + null, + ); const selectionColumn = useMemo>( () => ({ id: "__dowel_selection", @@ -259,23 +360,89 @@ export const DataTable = forwardRef(function DataTable( onChange={table.getToggleAllRowsSelectedHandler()} /> ), - cell: ({ row }) => ( + cell: ({ row, table }) => ( { + const checked = event.currentTarget.checked; + const shiftKey = + "shiftKey" in event.nativeEvent && event.nativeEvent.shiftKey; + const modelRows = table.getRowModel().rows; + const anchorIndex = modelRows.findIndex( + (candidate) => candidate.id === selectionAnchorRef.current, + ); + const rowIndex = modelRows.findIndex( + (candidate) => candidate.id === row.id, + ); + + if (shiftKey && anchorIndex >= 0 && rowIndex >= 0) { + const start = Math.min(anchorIndex, rowIndex); + const end = Math.max(anchorIndex, rowIndex); + table.setRowSelection((current) => { + const next = { ...current }; + for (const candidate of modelRows.slice(start, end + 1)) { + if (!candidate.getCanSelect()) continue; + if (checked) next[candidate.id] = true; + else delete next[candidate.id]; + } + return next; + }); + } else { + row.toggleSelected(checked); + } + selectionAnchorRef.current = row.id; + }} /> ), }), [], ); + const actionsColumn = useMemo | null>( + () => + renderRowActions + ? { + id: "__dowel_actions", + enableHiding: false, + enableResizing: false, + enableSorting: false, + size: 88, + minSize: 48, + maxSize: 160, + meta: { align: "end", truncate: false }, + header: "", + cell: ({ row }) => ( + + {renderRowActions(row.original)} + + ), + } + : null, + [activeActionsRowId, renderRowActions], + ); + const resolvedColumns = useMemo( - () => (selectable ? [selectionColumn, ...columns] : columns), - [columns, selectable, selectionColumn], + () => [ + ...(selectable ? [selectionColumn] : []), + ...columns, + ...(actionsColumn ? [actionsColumn] : []), + ], + [actionsColumn, columns, selectable, selectionColumn], ); + const [internalColumnOrder, setInternalColumnOrder] = + useState(defaultColumnOrder ?? []); + const [internalColumnPinning, setInternalColumnPinning] = + useState( + defaultColumnPinning ?? { start: [], end: [] }, + ); + const [internalColumnSizing, setInternalColumnSizing] = + useState(defaultColumnSizing ?? {}); const [internalSorting, setInternalSorting] = useState( defaultSorting ?? [], ); @@ -283,6 +450,28 @@ export const DataTable = forwardRef(function DataTable( useState(defaultRowSelection ?? {}); const [internalColumnVisibility, setInternalColumnVisibility] = useState(defaultColumnVisibility ?? {}); + const [internalCollapsedGroupIds, setInternalCollapsedGroupIds] = useState( + defaultCollapsedGroupIds ?? [], + ); + + const publicColumnPinning = columnPinning ?? internalColumnPinning; + const effectiveColumnPinning = useMemo( + () => ({ + start: [ + ...(selectable ? ["__dowel_selection"] : []), + ...publicColumnPinning.start.filter( + (id) => id !== "__dowel_selection" && id !== "__dowel_actions", + ), + ], + end: [ + ...publicColumnPinning.end.filter( + (id) => id !== "__dowel_selection" && id !== "__dowel_actions", + ), + ...(actionsColumn ? ["__dowel_actions"] : []), + ], + }), + [actionsColumn, publicColumnPinning, selectable], + ); const table = useTable({ features: dataTableFeatures, @@ -294,9 +483,19 @@ export const DataTable = forwardRef(function DataTable( maxSize: 720, }, enableColumnResizing: resizable, + enableColumnPinning: true, enableRowSelection: selectable, getRowId, initialState: { + ...(defaultColumnOrder === undefined + ? null + : { columnOrder: defaultColumnOrder }), + ...(defaultColumnPinning === undefined + ? null + : { columnPinning: defaultColumnPinning }), + ...(defaultColumnSizing === undefined + ? null + : { columnSizing: defaultColumnSizing }), ...(defaultColumnVisibility === undefined ? null : { columnVisibility: defaultColumnVisibility }), @@ -306,11 +505,39 @@ export const DataTable = forwardRef(function DataTable( ...(defaultSorting === undefined ? null : { sorting: defaultSorting }), }, state: { + columnOrder: columnOrder ?? internalColumnOrder, + columnPinning: effectiveColumnPinning, + columnSizing: columnSizing ?? internalColumnSizing, sorting: sorting ?? internalSorting, rowSelection: rowSelection ?? internalRowSelection, columnVisibility: columnVisibility ?? internalColumnVisibility, }, sortDescFirst: false, + onColumnOrderChange: (updater) => { + if (columnOrder === undefined) { + setInternalColumnOrder((current) => applyUpdater(updater, current)); + } + onColumnOrderChange?.(updater); + }, + onColumnPinningChange: (updater) => { + const effective = applyUpdater(updater, effectiveColumnPinning); + const next = { + start: effective.start.filter( + (id) => id !== "__dowel_selection" && id !== "__dowel_actions", + ), + end: effective.end.filter( + (id) => id !== "__dowel_selection" && id !== "__dowel_actions", + ), + }; + if (columnPinning === undefined) setInternalColumnPinning(next); + onColumnPinningChange?.(next); + }, + onColumnSizingChange: (updater) => { + if (columnSizing === undefined) { + setInternalColumnSizing((current) => applyUpdater(updater, current)); + } + onColumnSizingChange?.(updater); + }, onColumnVisibilityChange: (updater) => { if (columnVisibility === undefined) { setInternalColumnVisibility((current) => @@ -336,6 +563,41 @@ export const DataTable = forwardRef(function DataTable( const visibleColumns = table.getVisibleLeafColumns(); const rows = table.getRowModel().rows; + const resolvedCollapsedGroupIds = + collapsedGroupIds ?? internalCollapsedGroupIds; + const collapsedGroupSet = useMemo( + () => new Set(resolvedCollapsedGroupIds), + [resolvedCollapsedGroupIds], + ); + const groupedRows = useMemo(() => { + if (!groupBy) return [{ group: null, rows }]; + + const groups = new Map< + string, + { group: DataTableGroup; rows: typeof rows } + >(); + for (const row of rows) { + const group = groupBy(row.original); + if (!group) { + const fallback = { id: "__dowel_ungrouped", label: "Other" }; + const current = groups.get(fallback.id); + if (current) current.rows.push(row); + else groups.set(fallback.id, { group: fallback, rows: [row] }); + continue; + } + const current = groups.get(group.id); + if (current) current.rows.push(row); + else groups.set(group.id, { group, rows: [row] }); + } + return [...groups.values()]; + }, [groupBy, rows]); + const navigableRows = useMemo( + () => + groupedRows.flatMap((entry) => + entry.group && collapsedGroupSet.has(entry.group.id) ? [] : entry.rows, + ), + [collapsedGroupSet, groupedRows], + ); const tableWidth = Math.max(table.getTotalSize(), 480); const growingColumns = visibleColumns.filter( (column) => column.columnDef.meta?.grow, @@ -345,6 +607,167 @@ export const DataTable = forwardRef(function DataTable( .reduce((total, column) => total + column.getSize(), 0); const rootStyles = stylex.props(styles.parts.root); + useEffect(() => { + if ( + activeRowId === null || + !navigableRows.some((row) => row.id === activeRowId) + ) { + setActiveRowId(navigableRows[0]?.id ?? null); + } + }, [activeRowId, navigableRows]); + + function setCollapsedGroups(next: ReadonlyArray) { + if (collapsedGroupIds === undefined) setInternalCollapsedGroupIds(next); + onCollapsedGroupIdsChange?.(next); + } + + function toggleGroup(groupId: string) { + const next = new Set(resolvedCollapsedGroupIds); + if (next.has(groupId)) next.delete(groupId); + else next.add(groupId); + setCollapsedGroups([...next]); + } + + function selectRowRange(fromId: string, toId: string, selected = true) { + const from = navigableRows.findIndex((row) => row.id === fromId); + const to = navigableRows.findIndex((row) => row.id === toId); + if (from < 0 || to < 0) return; + const start = Math.min(from, to); + const end = Math.max(from, to); + table.setRowSelection((current) => { + const next = { ...current }; + for (const row of navigableRows.slice(start, end + 1)) { + if (!row.getCanSelect()) continue; + if (selected) next[row.id] = true; + else delete next[row.id]; + } + return next; + }); + } + + function handleRowKeyDown( + event: KeyboardEvent, + row: (typeof rows)[number], + ) { + if (isInteractiveTarget(event.target)) return; + const currentIndex = navigableRows.findIndex( + (candidate) => candidate.id === row.id, + ); + let nextIndex = currentIndex; + if (event.key === "ArrowDown") nextIndex = currentIndex + 1; + else if (event.key === "ArrowUp") nextIndex = currentIndex - 1; + else if (event.key === "Home") nextIndex = 0; + else if (event.key === "End") nextIndex = navigableRows.length - 1; + else if (event.key === " " && selectable) { + event.preventDefault(); + row.toggleSelected(!row.getIsSelected()); + selectionAnchorRef.current = row.id; + return; + } else { + triggerRow(event, row.original, onRowActivate); + return; + } + + const nextRow = + navigableRows[Math.max(0, Math.min(nextIndex, navigableRows.length - 1))]; + if (!nextRow || nextRow.id === row.id) return; + event.preventDefault(); + if (event.shiftKey && selectable) { + const anchor = selectionAnchorRef.current ?? row.id; + selectRowRange(anchor, nextRow.id); + selectionAnchorRef.current = anchor; + } else { + selectionAnchorRef.current = nextRow.id; + } + setActiveRowId(nextRow.id); + rowRefs.current.get(nextRow.id)?.focus(); + } + + function handleRowBlur(event: FocusEvent) { + if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { + const rowId = event.currentTarget.dataset.rowId; + setActiveActionsRowId((current) => (current === rowId ? null : current)); + } + } + + function renderDataRow(row: (typeof rows)[number]) { + const rowIsFocusable = + keyboardNavigation && + Boolean(onRowActivate || selectable || renderRowActions); + return ( + { + if (node) rowRefs.current.set(row.id, node); + else rowRefs.current.delete(row.id); + }} + {...stylex.props(styles.parts.row)} + data-row-id={row.id} + data-selected={row.getIsSelected() ? "" : undefined} + data-actionable={onRowActivate ? "" : undefined} + tabIndex={ + rowIsFocusable ? (activeRowId === row.id ? 0 : -1) : undefined + } + onBlurCapture={handleRowBlur} + onClick={(event) => triggerRow(event, row.original, onRowActivate)} + onFocusCapture={() => { + setActiveRowId(row.id); + setActiveActionsRowId(row.id); + }} + onKeyDown={(event) => + keyboardNavigation + ? handleRowKeyDown(event, row) + : triggerRow(event, row.original, onRowActivate) + } + onMouseEnter={() => setActiveActionsRowId(row.id)} + onMouseLeave={() => + setActiveActionsRowId((current) => + current === row.id ? null : current, + ) + } + > + {row.getVisibleCells().map((cell) => { + const meta = cell.column.columnDef.meta; + const pinned = cell.column.getIsPinned(); + return ( + +

+ +
+ + ); + })} + + ); + } + return (
( style={{ ...rootStyles.style, ...style }} data-dowel-component="data-table" data-density={density} + data-sticky-header={stickyHeader || undefined} > ( ))} {showHeader ? ( - + {table.getHeaderGroups().map((headerGroup) => ( ( alignmentStyle(meta?.align), header.column.id === "__dowel_selection" && styles.parts.selectionCell, + header.column.getIsPinned() && + styles.parts.pinnedHeaderCell, + header.column.getIsPinned() === "start" && + header.column.getIsLastColumn("start") && + styles.parts.pinnedStartEdge, + header.column.getIsPinned() === "end" && + header.column.getIsFirstColumn("end") && + styles.parts.pinnedEndEdge, )} + style={pinnedPositionStyle(header.column)} scope="col" aria-sort={ sorted === "asc" @@ -499,55 +932,65 @@ export const DataTable = forwardRef(function DataTable( ), ) - : rows.map((row) => ( - - triggerRow(event, row.original, onRowActivate) - } - onKeyDown={(event) => - triggerRow(event, row.original, onRowActivate) - } - > - {row.getVisibleCells().map((cell) => { - const meta = cell.column.columnDef.meta; - return ( + : groupedRows.map((entry, index) => { + if (!entry.group) { + return ( + + {entry.rows.map(renderDataRow)} + + ); + } + + const collapsed = collapsedGroupSet.has(entry.group.id); + const context: DataTableGroupContext = { + collapsed, + group: entry.group, + rows: entry.rows.map((row) => row.original), + toggle: () => toggleGroup(entry.group!.id), + }; + return ( + + - ); - })} - - ))} + + {collapsed ? null : entry.rows.map(renderDataRow)} + + ); + })} {!loading && rows.length === 0 ? (
-
- -
+ {renderGroupHeader ? ( + renderGroupHeader(context) + ) : ( + + )}
{ + it("edits, removes, and clears filters", async () => { + const onFilterClick = vi.fn(); + const onRemove = vi.fn(); + const onClear = vi.fn(); + const { container } = render( + , + ); + await userEvent.click( + screen.getByRole("button", { name: /StatusStarted/i }), + ); + expect(onFilterClick).toHaveBeenCalledWith("status"); + await userEvent.click( + screen.getByRole("button", { name: "Remove Status filter" }), + ); + expect(onRemove).toHaveBeenCalledWith("status"); + await userEvent.click(screen.getByRole("button", { name: "Clear all" })); + expect(onClear).toHaveBeenCalled(); + await expectNoA11yViolations(container); + }); +}); diff --git a/packages/dowel/src/components/filter-bar/index.tsx b/packages/dowel/src/components/filter-bar/index.tsx new file mode 100644 index 0000000..c92d9fa --- /dev/null +++ b/packages/dowel/src/components/filter-bar/index.tsx @@ -0,0 +1,97 @@ +import { XMarkIcon } from "@heroicons/react/16/solid"; +import * as stylex from "@stylexjs/stylex"; +import { forwardRef } from "react"; +import type { ComponentPropsWithoutRef, ReactNode } from "react"; + +import * as styles from "./filter-bar.stylex"; + +export interface FilterBarFilter { + id: string; + label: string; + value?: ReactNode; + disabled?: boolean; +} + +type DivProps = Omit< + ComponentPropsWithoutRef<"div">, + "children" | "className" | "style" +>; + +export interface FilterBarProps extends DivProps { + filters: ReadonlyArray; + onFilterClick?: (filterId: string) => void; + onRemove: (filterId: string) => void; + onClear?: () => void; + /** Usually an Add filter button or picker trigger. */ + children?: ReactNode; +} + +export const FilterBar = forwardRef( + function FilterBar( + { filters, onFilterClick, onRemove, onClear, children, ...props }, + ref, + ) { + return ( +
+ {filters.map((filter) => ( + + {onFilterClick ? ( + + ) : ( + + {filter.label} + {filter.value === undefined ? null : ( + + {filter.value} + + )} + + )} + + + ))} + {children} + {onClear && filters.length > 1 ? ( + + ) : null} +
+ ); + }, +); diff --git a/packages/dowel/src/components/filter-picker/filter-picker.stylex.ts b/packages/dowel/src/components/filter-picker/filter-picker.stylex.ts new file mode 100644 index 0000000..dbcaec5 --- /dev/null +++ b/packages/dowel/src/components/filter-picker/filter-picker.stylex.ts @@ -0,0 +1,125 @@ +import * as stylex from "@stylexjs/stylex"; + +import { tokens } from "../../theme/tokens.stylex"; + +const MOBILE = "@media (max-width: 639px)"; + +export const picker = stylex.create({ + root: { + margin: "-0.75rem", + minWidth: "18rem", + position: "relative", + width: "18rem", + }, + panel: { + minWidth: 0, + }, + valuePanel: { + backgroundColor: tokens["--dowel-bg-elevated"], + borderColor: tokens["--dowel-border-default"], + borderRadius: "10px", + borderStyle: "solid", + borderWidth: tokens["--dowel-hairline"], + boxShadow: tokens["--dowel-shadow-popover"], + boxSizing: "border-box", + overflow: "hidden", + position: "absolute", + top: { + default: 0, + [MOBILE]: "calc(100% + 0.375rem)", + }, + width: "18rem", + zIndex: 1, + }, + submenuRight: { + insetInlineStart: { + default: "calc(100% + 0.375rem)", + [MOBILE]: 0, + }, + }, + submenuLeft: { + insetInlineEnd: { + default: "calc(100% + 0.375rem)", + [MOBILE]: 0, + }, + }, + header: { + alignItems: "center", + borderBlockEndColor: tokens["--dowel-border-subtle"], + borderBlockEndStyle: "solid", + borderBlockEndWidth: tokens["--dowel-hairline"], + display: "flex", + minHeight: "2.75rem", + paddingInline: "0.75rem", + }, + list: { + maxHeight: "22rem", + overflowY: "auto", + paddingBlock: "0.375rem", + }, + item: { + alignItems: "center", + appearance: "none", + backgroundColor: "transparent", + borderStyle: "none", + borderRadius: tokens["--dowel-radius-md"], + color: tokens["--dowel-text-secondary"], + cursor: "pointer", + display: "flex", + fontFamily: tokens["--dowel-font-sans"], + fontSize: "0.8125rem", + fontWeight: 500, + gap: "0.5rem", + minHeight: "2rem", + marginInline: "0.375rem", + outline: "none", + paddingInline: "0.625rem", + textAlign: "left", + width: "calc(100% - 0.75rem)", + ":hover": { + backgroundColor: tokens["--dowel-bg-surface-3"], + color: tokens["--dowel-text-primary"], + }, + ":focus-visible": { + backgroundColor: tokens["--dowel-bg-surface-3"], + boxShadow: `inset 0 0 0 1px ${tokens["--dowel-focus-ring"]}`, + color: tokens["--dowel-text-primary"], + }, + ":disabled": { + cursor: "not-allowed", + opacity: 0.45, + }, + }, + activeItem: { + backgroundColor: tokens["--dowel-bg-active"], + color: tokens["--dowel-text-primary"], + }, + icon: { + alignItems: "center", + color: tokens["--dowel-text-tertiary"], + display: "inline-flex", + flexShrink: 0, + height: "1rem", + justifyContent: "center", + width: "1rem", + }, + label: { + flexGrow: 1, + minWidth: 0, + }, + count: { + color: tokens["--dowel-text-tertiary"], + fontSize: "0.75rem", + }, + chevron: { + color: tokens["--dowel-text-tertiary"], + flexShrink: 0, + }, + empty: { + color: tokens["--dowel-text-tertiary"], + fontSize: "0.8125rem", + margin: 0, + padding: "1.5rem 0.75rem", + textAlign: "center", + }, +}); diff --git a/packages/dowel/src/components/filter-picker/filter-picker.test.tsx b/packages/dowel/src/components/filter-picker/filter-picker.test.tsx new file mode 100644 index 0000000..6d0057d --- /dev/null +++ b/packages/dowel/src/components/filter-picker/filter-picker.test.tsx @@ -0,0 +1,48 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; + +import { FilterPicker } from "./index"; + +describe("FilterPicker", () => { + it("searches properties and adds a selected value", async () => { + const onAddFilter = vi.fn(); + render( + , + ); + + await userEvent.click(screen.getByRole("button", { name: "Filter" })); + await userEvent.type( + await screen.findByRole("searchbox", { + name: "Search filter properties", + }), + "status", + ); + await userEvent.hover(screen.getByRole("button", { name: "Status" })); + expect( + screen.getByRole("searchbox", { name: "Search filter properties" }), + ).toBeTruthy(); + expect( + screen.getByRole("searchbox", { name: "Search Status" }), + ).toBeTruthy(); + await userEvent.click(screen.getByRole("button", { name: "Completed" })); + expect(onAddFilter).toHaveBeenCalledWith("status", "done"); + }); +}); diff --git a/packages/dowel/src/components/filter-picker/index.tsx b/packages/dowel/src/components/filter-picker/index.tsx new file mode 100644 index 0000000..148356c --- /dev/null +++ b/packages/dowel/src/components/filter-picker/index.tsx @@ -0,0 +1,222 @@ +import { ChevronRightIcon, FunnelIcon } from "@heroicons/react/16/solid"; +import * as stylex from "@stylexjs/stylex"; +import { useMemo, useRef, useState } from "react"; +import type { ReactElement, ReactNode } from "react"; + +import { IconButton } from "../icon-button"; +import { Popover } from "../popover"; +import { SearchField } from "../search-field"; +import * as styles from "./filter-picker.stylex"; + +export interface FilterPickerValue { + value: string; + label: string; + count?: number; + disabled?: boolean; +} + +export interface FilterPickerProperty { + id: string; + label: string; + icon?: ReactNode; + values: ReadonlyArray; + disabled?: boolean; +} + +export interface FilterPickerProps { + properties: ReadonlyArray; + onAddFilter: (propertyId: string, value: string) => void; + label?: string; + trigger?: ReactElement; +} + +export function FilterPicker({ + properties, + onAddFilter, + label = "Filter", + trigger, +}: FilterPickerProps) { + const [open, setOpen] = useState(false); + const [propertyQuery, setPropertyQuery] = useState(""); + const [valueQuery, setValueQuery] = useState(""); + const [activePropertyId, setActivePropertyId] = useState(null); + const [submenuSide, setSubmenuSide] = useState<"left" | "right">("right"); + const propertyPanelRef = useRef(null); + const activeProperty = + properties.find((property) => property.id === activePropertyId) ?? null; + const visibleProperties = useMemo(() => { + const query = propertyQuery.trim().toLocaleLowerCase(); + if (!query) return properties; + return properties.filter((property) => + property.label.toLocaleLowerCase().includes(query), + ); + }, [properties, propertyQuery]); + const visibleValues = useMemo(() => { + if (!activeProperty) return []; + const query = valueQuery.trim().toLocaleLowerCase(); + if (!query) return activeProperty.values; + return activeProperty.values.filter((value) => + value.label.toLocaleLowerCase().includes(query), + ); + }, [activeProperty, valueQuery]); + + function changeOpen(nextOpen: boolean) { + setOpen(nextOpen); + if (!nextOpen) { + setPropertyQuery(""); + setValueQuery(""); + setActivePropertyId(null); + } + } + + function showProperty(propertyId: string) { + const panel = propertyPanelRef.current; + if (panel) { + const rect = panel.getBoundingClientRect(); + const gap = 6; + const edgePadding = 8; + const hasRoomOnRight = + rect.right + gap + rect.width <= window.innerWidth - edgePadding; + setSubmenuSide(hasRoomOnRight ? "right" : "left"); + } + setActivePropertyId(propertyId); + setValueQuery(""); + } + + return ( + + + + ); +} diff --git a/packages/dowel/src/components/form/form.stylex.ts b/packages/dowel/src/components/form/form.stylex.ts new file mode 100644 index 0000000..0c83d68 --- /dev/null +++ b/packages/dowel/src/components/form/form.stylex.ts @@ -0,0 +1,22 @@ +import * as stylex from "@stylexjs/stylex"; + +import { tokens } from "../../theme/tokens.stylex"; + +export const form = stylex.create({ + root: { + display: "flex", + flexDirection: "column", + gap: "1rem", + minWidth: 0, + }, + actions: { + alignItems: "center", + display: "flex", + gap: "0.5rem", + justifyContent: "flex-end", + paddingBlockStart: "0.25rem", + }, + fieldError: { + color: tokens["--dowel-danger"], + }, +}); diff --git a/packages/dowel/src/components/form/form.test.tsx b/packages/dowel/src/components/form/form.test.tsx new file mode 100644 index 0000000..5e82ea6 --- /dev/null +++ b/packages/dowel/src/components/form/form.test.tsx @@ -0,0 +1,60 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; + +import { expectNoA11yViolations } from "../../../test/setup"; +import { useDowelForm } from "./index"; + +function Example({ + onSubmit, +}: { + onSubmit: (value: { title: string }) => void; +}) { + const form = useDowelForm({ + defaultValues: { title: "" }, + onSubmit: ({ value }) => onSubmit(value), + }); + + return ( + + + (value ? undefined : "Title is required"), + }} + > + {(field) => ( + + )} + + + Create issue + + + + ); +} + +describe("useDowelForm", () => { + it("binds Dowel fields to TanStack Form submission", async () => { + const onSubmit = vi.fn(); + render(); + await userEvent.type( + screen.getByRole("textbox", { name: "Title" }), + "Ship it", + ); + await userEvent.click(screen.getByRole("button", { name: "Create issue" })); + expect(onSubmit).toHaveBeenCalledWith({ title: "Ship it" }); + }); + + it("renders touched field errors accessibly", async () => { + const { container } = render( {}} />); + const input = screen.getByRole("textbox", { name: "Title" }); + input.focus(); + await userEvent.tab(); + expect(await screen.findByText("Title is required")).toBeDefined(); + expect(input.getAttribute("aria-invalid")).toBe("true"); + await expectNoA11yViolations(container); + }); +}); diff --git a/packages/dowel/src/components/form/index.tsx b/packages/dowel/src/components/form/index.tsx new file mode 100644 index 0000000..0748eb4 --- /dev/null +++ b/packages/dowel/src/components/form/index.tsx @@ -0,0 +1,169 @@ +import { createFormHook, createFormHookContexts } from "@tanstack/react-form"; +import * as stylex from "@stylexjs/stylex"; +import type { ComponentPropsWithoutRef, ReactNode } from "react"; + +import { Button } from "../button"; +import type { ButtonProps } from "../button"; +import { Field, Input, Textarea } from "../input"; +import type { InputProps, TextareaProps } from "../input"; +import * as styles from "./form.stylex"; + +const { fieldContext, formContext, useFieldContext, useFormContext } = + createFormHookContexts(); + +function firstError(errors: unknown[]): string | null { + const error = errors.find((value) => value !== undefined && value !== null); + if (error === undefined) return null; + if (typeof error === "string") return error; + if (error instanceof Error) return error.message; + return String(error); +} + +export interface FormTextFieldProps + extends Omit< + InputProps, + "aria-label" | "defaultValue" | "name" | "onBlur" | "onChange" | "value" + > { + label: ReactNode; + description?: ReactNode; +} + +function TextField({ label, description, ...inputProps }: FormTextFieldProps) { + const field = useFieldContext(); + const error = firstError(field.state.meta.errors); + const invalid = field.state.meta.isTouched && error !== null; + + return ( + + {label} + field.handleChange(event.currentTarget.value)} + /> + {description ? ( + {description} + ) : null} + {invalid ? {error} : null} + + ); +} + +export interface FormTextareaFieldProps + extends Omit< + TextareaProps, + "aria-label" | "defaultValue" | "name" | "onBlur" | "onChange" | "value" + > { + label: ReactNode; + description?: ReactNode; +} + +function TextareaField({ + label, + description, + ...textareaProps +}: FormTextareaFieldProps) { + const field = useFieldContext(); + const error = firstError(field.state.meta.errors); + const invalid = field.state.meta.isTouched && error !== null; + + return ( + + {label} +