diff --git a/.changeset/quiet-saas-restyle.md b/.changeset/quiet-saas-restyle.md new file mode 100644 index 0000000..f7b17a6 --- /dev/null +++ b/.changeset/quiet-saas-restyle.md @@ -0,0 +1,12 @@ +--- +'@rowkit/tokens': minor +'rowkit': minor +--- + +Restyle rowkit on a shadcn/ui-derived language, then tune it for data-dense SaaS — cool chrome, indigo primary, one control geometry. + +**Tokens (breaking if you override theme variables or write rowkit utility classes by hand).** Seven core semantics rename to shadcn’s names: `surface` → `card`, `surface-subtle` → `muted`, `surface-hover` → `accent`, `text` → `foreground`, `text-muted` → `muted-foreground`, `border-control` → `input`, `focus-ring` → `ring`. The greys start from shadcn’s zero-chroma ramp, then pick up rowkit identity: cooler, lighter decorative borders, a cool off-white page, brand indigo primary with a matching focus ring (not near-black), and selected rows on a quiet primary wash. Corners derive from a single `--radius`. New: overlay blur, sticky-header inset shadow, stronger sticky-column scroll shadow. Status families keep the solid/subtle/outline axis Badge and Button already expose. + +**Components.** The shared focus recipe (border + translucent ring) lands on every control. Button, Input and Select share height, radius, padding and `text-sm` from `sm` up; Button adds `xs` and `icon`. Secondary is a muted fill so it never reads as another field; fields stay the outlined hollow shell. Chromatic Badge `subtle` is a soft tinted chip. Tooltip inverts foreground/background instead of painting as a primary bubble. DataTable: opaque sticky header with an inset edge that travels while scrolling, unified loaded/loading row heights, quieter hover vs selection. Dialog: blurred scrim, denser padding, footer rule, close matches an icon button. FilterBar, Field, Toast, EmptyState and Pagination follow the same chrome. Docs demos stop inheriting VitePress’s unlayered table grid and zebra over DataTable. + +**API (0.x breaking).** `TablePagination` is now `Pagination` — same props, events and slots; docs move to `/components/pagination`. Marked `minor` on purpose: on a 0.x line changesets would turn a `major` into `1.0.0`, and 1.0 should wait for real apps, not a rename. diff --git a/.storybook/preview.ts b/.storybook/preview.ts index e92953d..48764c1 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -36,7 +36,7 @@ const preview: Preview = { document.documentElement.classList.toggle('dark', theme === 'dark') return { components: { story }, - template: `
`, + template: `
`, } }, ], diff --git a/README.md b/README.md index 78b9d67..845eef3 100644 --- a/README.md +++ b/README.md @@ -117,3 +117,5 @@ pnpm docs:dev # documentation site ## License MIT © Nikolai Kushner + +Design language based on [shadcn/ui](https://ui.shadcn.com) by shadcn, adapted for Vue. shadcn/ui is MIT licensed; rowkit adopts its token values and class recipes, not its code. diff --git a/ROADMAP.md b/ROADMAP.md index 706ac26..0398107 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -40,7 +40,7 @@ Every component is labeled with its current stage. Nothing is marked Stable unti ### Data layer - [x] 🟢 **DataTable** — typed column defs, sorting, row selection, sticky header -- [x] 🟢 **TablePagination** — page size, jump-to-page, total count +- [x] 🟢 **Pagination** — page size, jump-to-page, total count - [x] 🟢 **FilterBar** — composable filter chips with applied-state display - [x] 🟢 **EmptyState** — the screen every dashboard needs and nobody designs - [x] 🟢 **Skeleton** — loading placeholders matched to the data components @@ -55,26 +55,8 @@ Every component is labeled with its current stage. Nothing is marked Stable unti ## Build phases -- [x] **Phase 0 — Foundation.** Monorepo, Vite library mode, TypeScript strict, ESLint, Vitest, CI. - *Done when:* a throwaway component builds, emits correct `.d.ts`, and imports with working types in the playground. - -- [x] **Phase 1 — Token system.** Color scales, semantic mappings, spacing, typography, radii, shadows, z-index, motion. Dark mode. Standalone `@rowkit/tokens` package. - *Done when:* dark mode toggles with zero hardcoded colors anywhere, and `import { tokens } from '@rowkit/tokens'` is fully typed. - -- [x] **Phase 2 — Core components.** The four foundations, one at a time, each fully complete before the next. - *Done when:* all four at Stable, playground renders a working form using only rowkit components. - -- [x] **Phase 3 — Data layer.** The five data components. DataTable is the centerpiece. - *Done when:* playground has a working "users admin" page — filterable, sortable, paginated, with loading and empty states. - -- [x] **Phase 4 — Overlays.** Dialog, Toast, Tooltip. SSR-safe. - *Done when:* all three keyboard-navigable, focus-managed, no hydration errors in the Nuxt playground. - -- [ ] **Phase 5 — Documentation site.** VitePress on rowkit.dev. Foundations, components, patterns, contributing. - *Done when:* every Stable component documented, Storybook deployed and linked. - -- [ ] **Phase 6 — Ship.** Changesets, trusted publishing via GitHub Actions, `v0.1.0` on npm. - *Done when:* `npm i rowkit` works in a fresh project, following only the docs. +Planning docs live in `docs/phases/` when they exist. The previous phase specs +were retired; new ones will replace them. --- diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 39a5ee7..4dd9aef 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -15,7 +15,7 @@ export default defineConfig({ lang: 'en-GB', cleanUrls: true, - // The internal planning specs are not product documentation. + // Internal planning specs (`docs/phases/`) stay off the published site. srcExclude: ['phases/**'], sitemap: { hostname: 'https://rowkit.dev' }, @@ -91,7 +91,7 @@ export default defineConfig({ text: 'Data', items: [ { text: 'DataTable', link: '/components/data-table' }, - { text: 'TablePagination', link: '/components/table-pagination' }, + { text: 'Pagination', link: '/components/pagination' }, { text: 'FilterBar', link: '/components/filter-bar' }, { text: 'EmptyState', link: '/components/empty-state' }, { text: 'Skeleton', link: '/components/skeleton' }, diff --git a/docs/.vitepress/theme/components/ColorScale.vue b/docs/.vitepress/theme/components/ColorScale.vue index dfb8083..bdbaa27 100644 --- a/docs/.vitepress/theme/components/ColorScale.vue +++ b/docs/.vitepress/theme/components/ColorScale.vue @@ -20,13 +20,13 @@ const { copied, copy } = useCopyToken() -

+

{{ selected.length }} selected · {{ sort ? `sorted by ${sort.key}, ${sort.direction}` : 'unsorted' }}

@@ -109,7 +109,7 @@ pnpm add rowkit Both lines are required, and that second one is the step people miss — see [installation](/installation) for why, and for the Nuxt path. -**v0.1.0 is on npm.** Every component above is built, tested and published — you +** is on npm.** Every component above is built, tested and published — you are looking at them running. The API is stabilising toward v1.0, so breaking changes are still possible until then. diff --git a/docs/installation.md b/docs/installation.md index 0fcafdb..4ea1f12 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -35,7 +35,7 @@ document.documentElement.classList.toggle('dark', isDark) ``` Only semantic tokens change under `.dark`; the colour primitives stay fixed. A -component never knows which theme is active — it reads `--color-surface` and the +component never knows which theme is active — it reads `--color-card` and the answer differs. ## Nuxt diff --git a/docs/introduction.md b/docs/introduction.md index d5182e4..f533756 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -99,7 +99,7 @@ it is designed to sit beside a general-purpose kit rather than replace it. **v0.x.** The API is stabilising toward v1.0, every component has reached the project's definition of done, and breaking changes are still possible until v1. -`v0.1.0` is on npm, published from CI with provenance attestation. The source +Version is on npm, published from CI with provenance attestation. The source and the full roadmap are on [GitHub](https://github.com/NikolaiKushner/rowkit). ## Where to go next diff --git a/docs/patterns/data-table-page.md b/docs/patterns/data-table-page.md index b29e0b2..2a5348b 100644 --- a/docs/patterns/data-table-page.md +++ b/docs/patterns/data-table-page.md @@ -125,7 +125,7 @@ watch([search, role, status, sort, pageSize], () => { - + Search for a name, narrow by role, sort a column, page through. Then filter down @@ -137,7 +137,7 @@ that you have no users. ```vue -``` - -> **`undefined`, not `null`, for unsorted.** An absent Vue prop is already `undefined`, so `DataTableSort | null` gives three states for a two-state concept. Clearing the sort emits `update:sort` with `undefined`. - -Decisions embedded here, each worth stating in the PR: - -- **`TRow extends { id: string | number }`.** Requiring a stable id makes `:key` correct and selection unambiguous. The alternative — a `rowKey` prop — is more flexible and much worse: it makes the common case verbose to serve a rare one. - - **Ids must be stable across renders**: present in the data, or assigned once at fetch or ingest time — never derived in a computed. An id minted by a `.map()` inside a computed produces a fresh object for every row on every change, which defeats reference equality: `:key` churns and the `v-memo` in §4.6 never hits, because it compares `row` by reference. An earlier draft offered exactly that `.map()` as the escape hatch. It is the one thing not to do. - -- **`key: keyof TRow & string`** is the payoff line. `columns: [{ key: 'emial' }]` fails compilation. This single constraint is what "typed column defs" means, and it's your best demo material. -- **Selection is `Array`, not `TRow[]`.** Ids survive refetches; object references don't. A refetch replacing row objects would silently orphan an object-based selection. -- **Sorting is controlled and means "request", not "behavior".** The table emits what the user asked for; the consumer sorts (or forwards it to an API). The table never sorts data itself — this keeps server-side and client-side workflows identical from the table's point of view. Ship a `useClientSort(rows, sort, columns?)` composable for the client-side case so convenience isn't lost; logic in a composable rather than baked into the component is the pattern that keeps table components maintainable. - - > Built and shipped. An interim version put client sorting **inside** the component behind a `sortMode` prop; it was removed in favour of the composable, which is testable without mounting and cannot be reached by a server-paged table by accident. - -### 4.2 Slots — typed cell rendering - -```ts -defineSlots< - { - /** Per-column cell override. Slot name = column key. */ - [K in keyof TRow & string as `cell:${K}`]?: (props: { value: TRow[K]; row: TRow }) => unknown - } & { - empty?: () => unknown - loading?: () => unknown - } ->() -``` - -Usage: `#cell:status="{ value, row }"` — with `value` typed as that column's actual type. - -> ⚠️ **Known tooling caveat:** template-literal slot names combined with generics sit at the edge of what `vue-tsc` handles — there are open language-tools issues around exactly this pattern. Verify early in session (a) that autocomplete and type errors actually work in the playground. If the DX is broken in practice, fall back to a single `#cell="{ column, value, row }"` slot with a `column` discriminator: slightly weaker types, reliable tooling. **Working DX beats impressive types.** -> -> Whichever way it lands, the investigation is written up in `docs/decisions/003-cell-slot-typing.md` — what was tried, what `vue-tsc` did, and which way it went. Not in the PR description: a decision that lives only in a merged PR is a decision nobody will find in six months. That write-up is also your best LinkedIn post of the project. - -### 4.3 Composition with the earlier components - -- **Loading:** `loading=true` renders skeleton rows _matching the column layout_ (real column widths, one Skeleton per cell) — not a spinner replacing the table. Layout stability is the whole point of skeletons. The table gets `aria-busy="true"`. - - **Do not disable the sort buttons while loading.** They stay focusable and take `aria-disabled="true"`, with the handler a no-op. Disabling the control a keyboard user just activated destroys their focus mid-request and throws them to the top of the document — the same failure §5 rightly guards against for chips. Focus must survive a request cycle, and there is an interaction test asserting it does. - -- **Empty:** `rows.length === 0 && !loading` renders the `#empty` slot, defaulting to `EmptyState reason="no-data"`. Docs show overriding with `no-results` + a clear-filters action when filters are active. -- **The states are exclusive and priority-ordered:** loading > empty > data. An explicit internal `state` computed with exactly one value prevents the "skeleton and empty state at once" class of bug. Write a test asserting each state is exclusive. - -### 4.4 Accessibility — tables have real semantics - -This is where `addon-a11y` won't catch everything; part of the checklist is manual: - -- Semantic `//` — never divs-as-grid. Screen readers navigate real tables; div grids require re-implementing everything for zero benefit here. -- **Sortable headers:** the `
/
` carries `aria-sort="ascending" | "descending" | "none"`, and contains a real `