{"_id":"@asnewyla/unstyled-table","name":"@asnewyla/unstyled-table","dist-tags":{"latest":"0.2.0"},"versions":{"0.2.0":{"name":"@asnewyla/unstyled-table","version":"0.2.0","description":"Unstyled table primitive with sorting, row selection, and pagination","license":"MIT","repository":{"type":"git","url":"git+https://github.com/xavierDelaFuente/xd-components.git","directory":"packages/unstyled-table"},"publishConfig":{"access":"public"},"main":"dist/index.js","module":"dist/index.mjs","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./test-utils":{"types":"./dist/test-utils.d.ts","import":"./dist/test-utils.mjs","require":"./dist/test-utils.js"}},"sideEffects":false,"keywords":["react","table","data-grid","unstyled","accessible"],"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0","@testing-library/react":"^16.0.0","@testing-library/user-event":"^14.5.0"},"peerDependenciesMeta":{"@testing-library/react":{"optional":true},"@testing-library/user-event":{"optional":true}},"devDependencies":{"react":"^18.3.0","react-dom":"^18.3.0","tsup":"^8.1.0","vitest":"^2.0.0","@vitest/coverage-v8":"^2.0.0","@testing-library/react":"^16.0.0","@testing-library/user-event":"^14.5.0","@testing-library/jest-dom":"^6.4.0","jsdom":"^24.0.0","typescript":"^5.5.0"},"scripts":{"build":"tsup","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","type-check":"tsc --noEmit"},"_nodeVersion":"24.19.0","_id":"@asnewyla/unstyled-table@0.2.0","dist":{"integrity":"sha512-/3Ytjr8gJndeBZ8O37cPiTM53rZXy+a3IbjYVeAqaijgvu9nhgTwcSc0gsFtbuODplaDJ2BaAODYzNV1iDhvcQ==","shasum":"b5d21a5fbe72d9e4818f5cddcf75a220aceabd28","tarball":"https://registry.npmjs.org/@asnewyla/unstyled-table/-/unstyled-table-0.2.0.tgz","fileCount":15,"unpackedSize":88508,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCOd/2A9SCAm1QJ5+DqHOaih3EBxHftYD4oGXxCl1+JewIhAPZO3SMO56wc5OaeniiyySD8X9eXr8su6Or0b5GLS0r1"}]},"_npmUser":{"name":"asnewyla","email":"asnewyla@gmail.com"},"directories":{},"maintainers":[{"name":"asnewyla","email":"asnewyla@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/unstyled-table_0.2.0_1787860427624_0.39712231273814447"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-27T19:53:47.418Z","0.2.0":"2026-08-27T19:53:47.816Z","modified":"2026-08-27T19:53:48.009Z"},"maintainers":[{"name":"asnewyla","email":"asnewyla@gmail.com"}],"description":"Unstyled table primitive with sorting, row selection, and pagination","keywords":["react","table","data-grid","unstyled","accessible"],"repository":{"type":"git","url":"git+https://github.com/xavierDelaFuente/xd-components.git","directory":"packages/unstyled-table"},"license":"MIT","readme":"# @asnewyla/unstyled-table\n\nUnstyled, accessible table primitive. Renders rows and columns from plain\ndata, with single-column sorting, client-side global search, row\nselection, and pagination, without imposing any visual styling.\n\n## Install\n\n```bash\nnpm install @asnewyla/unstyled-table\n```\n\n## Usage\n\n```tsx\nimport { UnstyledTable } from '@asnewyla/unstyled-table';\n\ninterface Person {\n  name: string;\n  age: number;\n}\n\nconst people: Person[] = [\n  { name: 'Ada Lovelace', age: 36 },\n  { name: 'Alan Turing', age: 41 },\n];\n\nconst columns = [\n  { key: 'name', header: 'Name', sortable: true },\n  { key: 'age', header: 'Age' },\n];\n\n// Plain rendering\n<UnstyledTable data={people} columns={columns} />\n\n// With a search box (client-side, filters across every column's value)\n<UnstyledTable data={people} columns={columns} filterable />\n\n// Controlled sort\n<UnstyledTable\n  data={people}\n  columns={columns}\n  sort={sort}\n  onSortChange={setSort}\n/>\n\n// A custom cell renderer per column\nconst columnsWithRender = [\n  { key: 'name', header: 'Name' },\n  { key: 'age', header: 'Age', render: (row) => `${row.age} yrs` },\n];\n\n// Row selection (controlled) — see \"Row identity\" below for getRowKey\n<UnstyledTable\n  data={people}\n  columns={columns}\n  getRowKey={(row) => row.id}\n  selectable\n  selected={selected}\n  onSelectionChange={setSelected}\n/>\n\n// Pagination — fixed page size, no built-in page-size selector\n<UnstyledTable\n  data={people}\n  columns={columns}\n  paginated\n  pageSize={10}\n/>\n```\n\n### Props\n\n| Prop | Type | Default |\n|---|---|---|\n| `columns` | `TableColumn<T>[]` | — |\n| `data` | `T[]` | — |\n| `sort` / `defaultSort` | `{ key: keyof T; direction: 'asc' \\| 'desc' } \\| null` | — |\n| `onSortChange` | `(sort: SortState<T> \\| null) => void` | — |\n| `filterable` | `boolean` | `false` |\n| `getRowKey` | `(row: T) => RowId` (`RowId = string \\| number`) | — |\n| `selectable` | `boolean` | `false` |\n| `selected` / `defaultSelected` | `RowId[]` | — |\n| `onSelectionChange` | `(selected: RowId[]) => void` | — |\n| `paginated` | `boolean` | `false` |\n| `pageSize` | `number` | `10` |\n| `page` / `defaultPage` | `number` | — |\n| `onPageChange` | `(page: number) => void` | — |\n\nAlso accepts every native `<table>` attribute (`className`, `style`, `id`,\netc.) via passthrough — `UnstyledTable` composes\n`ComponentPropsWithoutRef<'table'>` like every other primitive in this\nlibrary.\n\n`TableColumn<T>`:\n\n| Field | Type | Default |\n|---|---|---|\n| `key` | `keyof T` | — |\n| `header` | `string` | — |\n| `render` | `(row: T) => ReactNode` | — |\n| `sortable` | `boolean` | `false` |\n\nUncontrolled by default — pass `sort` + `onSortChange` to control the sort\nstate yourself. `render` overrides how a cell displays a column's value;\nwithout it, a cell just shows `String(row[column.key])`.\n\n### Sorting\n\nOnly one column can be sorted at a time. Clicking a `sortable` column's\nheader cycles ascending → descending → unsorted; clicking a different\nsortable column resets to ascending on that column instead. Non-sortable\ncolumns render as plain `<th>` text — no button, no `aria-sort`.\n\n### Filtering\n\n`filterable` renders a single `<input aria-label=\"Search table\">` above\nthe table. Typing filters `data` client-side by a case-insensitive\nsubstring match against every column's raw value (`row[column.key]`, not\nits rendered output) — there's no async/remote search, matching the same\nclient-side-only scope as `@asnewyla/unstyled-select`'s search. Filtering\nis always applied before sorting.\n\n### Row identity and selection\n\n`getRowKey` gives each row a stable identity — used both for React's own\n`key` (so a row's DOM state, like an uncontrolled input inside a custom\n`render`, stays attached to that row across a sort/filter reorder) and for\ntracking `selected`. Without it, identity falls back to\n`JSON.stringify(row)`, which works as long as rows are unique by content.\n\n`selectable` adds a checkbox column: a per-row checkbox plus a \"select\nall\" checkbox in the header (`aria-label=\"Select all rows\"`, native\n`indeterminate` when some but not all *currently rendered* rows are\nselected). `UnstyledTable` never mutates `data` — deleting/exporting/\nacting on a selection is entirely the consumer's job, using `selected` +\nwhatever action they wire up next to the table (see the\n`BulkDeleteSelectedRows` Storybook story).\n\n### Pagination\n\n`paginated` slices the final (filtered, then sorted) row set into pages of\n`pageSize` rows and renders a footer below the table: four buttons —\n\"First page\", \"Previous page\", \"Next page\", \"Last page\" (plain text\ncontent, which doubles as the accessible name) — around a plain-text\n`\"Page X of Y\"` indicator. First/Previous are disabled on the first page,\nNext/Last on the last page, via native `disabled` rather than just visual\nstyling. Each button also carries `data-pagination-action=\"first\" |\n\"previous\" | \"next\" | \"last\"`, a stable hook for consumers to style them\n(e.g. as icon buttons) without depending on their text content. There's no\nbuilt-in page-size selector; `pageSize` is a fixed prop. The\ncurrent page clamps automatically if the underlying row count shrinks out\nfrom under it (e.g. filtering down to fewer rows/pages than the page you\nwere on) — this happens on every render as a plain derived value, not via\nan effect, so it can't ever momentarily show a page that no longer exists.\n\n### Accessibility\n\nRenders a real semantic `<table>`/`<thead>`/`<tbody>` — row/column-header/\ncell roles all come from the native elements, not ARIA overrides. Sortable\nheaders use a real `<button>` (keyboard-reachable, Enter/Space activates\nfor free) with `aria-sort` on the parent `<th>` reflecting the current\nstate (`\"ascending\"`, `\"descending\"`, or `\"none\"`).\n\n### Not yet supported\n\nThis primitive is still growing. Not implemented yet:\n\n- **Native form participation** — nothing here submits through\n  `FormData`.\n\n## License\n\nMIT\n","readmeFilename":"","_rev":"1-625330c8e588eba7eea08fb64d613e19"}