@devslab/dds-table-solid 0.0.0-stage → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,74 @@
1
+ DevsLab Source-Available License 1.0
2
+
3
+ Copyright (c) 2026 DevsLab. All rights reserved.
4
+
5
+ This license governs the DDS software, design tokens, components, icons,
6
+ documentation, and other materials distributed with this license (the
7
+ "Software").
8
+
9
+ 1. Permitted use
10
+
11
+ Subject to this license, DevsLab grants you a limited, worldwide,
12
+ non-exclusive, non-transferable, revocable, royalty-free right to:
13
+
14
+ - download and install unmodified copies of the Software;
15
+ - use and execute the Software for internal or commercial applications;
16
+ - compile, minify, tree-shake, and bundle unmodified Software solely as a
17
+ technical part of building an application; and
18
+ - distribute the resulting object code only as an inseparable part of that
19
+ application and not as a standalone library, design system, component kit,
20
+ template, or asset collection.
21
+
22
+ Normal application configuration through documented public APIs, design-token
23
+ overrides, CSS custom properties, and component properties is permitted and is
24
+ not considered modification of the Software.
25
+
26
+ 2. Restrictions
27
+
28
+ Except where applicable law does not allow the restriction, you may not:
29
+
30
+ - modify, adapt, translate, reverse engineer, or create derivative works of the
31
+ Software itself;
32
+ - publish, republish, sublicense, sell, rent, host, or redistribute the
33
+ Software or any modified version as a standalone package or downloadable
34
+ collection;
35
+ - use the Software or a substantial portion of it to create or operate a
36
+ competing general-purpose design system, UI kit, component library, icon
37
+ library, token library, template marketplace, or hosted equivalent;
38
+ - remove or alter copyright, license, attribution, or provenance notices; or
39
+ - use DevsLab names, logos, product marks, or trade dress except to make a
40
+ truthful statement that an application uses the Software.
41
+
42
+ No patent, trademark, design, or branding rights are granted except the
43
+ copyright permissions stated in Section 1.
44
+
45
+ 3. Ownership and feedback
46
+
47
+ The Software is licensed, not sold. DevsLab and its licensors retain all
48
+ right, title, and interest in the Software. You grant DevsLab a perpetual,
49
+ worldwide, irrevocable, royalty-free right to use feedback you voluntarily
50
+ provide about the Software.
51
+
52
+ 4. Termination
53
+
54
+ This license terminates automatically if you breach it. Upon termination, you
55
+ must stop using and distributing the Software and delete all copies under your
56
+ control. Sections 2, 3, 5, and 6 survive termination.
57
+
58
+ 5. Disclaimer and limitation of liability
59
+
60
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
61
+ EXPRESS OR IMPLIED, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR
62
+ PURPOSE, TITLE, AND NON-INFRINGEMENT. TO THE MAXIMUM EXTENT PERMITTED BY LAW,
63
+ DEVSLAB WILL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
64
+ CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, OR FOR LOSS OF DATA, PROFITS,
65
+ REVENUE, OR BUSINESS, ARISING FROM OR RELATED TO THE SOFTWARE.
66
+
67
+ 6. Governing law
68
+
69
+ This license is governed by the laws of the Republic of Korea, excluding
70
+ conflict-of-law rules. The Seoul Central District Court has exclusive
71
+ jurisdiction over disputes arising from this license.
72
+
73
+ This is a source-available license, not an open-source license approved by the
74
+ Open Source Initiative.
package/README.ko.md ADDED
@@ -0,0 +1,165 @@
1
+ # @devslab/dds-table-solid
2
+
3
+ ## 선택적으로 사용하는 업무 화면 기능
4
+
5
+ - `resizing={{ label: "{column} 너비 조절", minWidth: 80, maxWidth: 1200, onChange }}`로 컬럼 드래그와 키보드 너비 조절을 켭니다. `Column.resizable = false`인 컬럼은 제외합니다.
6
+ - 방향키는 12px, Shift와 함께 누르면 50px씩 조절합니다. Home/End는 최소·최대 너비입니다. 더블클릭 또는 Escape는 **전체 컬럼**의 조절값을 지우고 원래 선언한 너비로 복원합니다.
7
+ - `onChange(widths, reason)`은 조절 완료(`resize`) 또는 초기화(`reset`) 때 호출합니다. 분석 이벤트와 너비 저장은 사용하는 앱에서 처리합니다.
8
+ - `sort={{ value, onChange }}`는 서버 정렬을 위한 제어 방식입니다. 오름차순 → 내림차순 → `null` 순서로 요청하며, 서버에서 받은 현재 페이지를 브라우저에서 재정렬하지 않습니다. 서버 조회와 결과 전달은 앱이 맡습니다. 기존 `sort="client"`와 `{ statedOrder }` 방식은 유지합니다.
9
+ - `selection={{ rowId, selectedId, onSelect }}`로 행 선택을 켭니다. 클릭·Enter·Space를 지원하고, 행 안의 버튼·링크·입력 요소와 충돌하지 않습니다. 상세 화면 열기 등 업무 동작은 앱의 콜백에서 연결합니다.
10
+ - `SortValue`, `SortMode`, `ColumnResizing`, `RowSelection` 타입을 제공합니다. 기존 사용처는 새 옵션 없이 그대로 동작합니다.
11
+
12
+
13
+ TanStack Table 기반 DDS 데이터 테이블 패키지(정확히 `9.2.4` 버전에 고정 —
14
+ 고정 이유는 `docs/decisions.md` D-021 참고). `DataTable` 컴포넌트 하나와
15
+ 이를 기술하는 `Column<T>`·`DataTableLabels` 타입만 제공한다. 테이블 자체는
16
+ 어떤 단어도, 어떤 경로도, "다음 페이지"가 무엇을 뜻하는지도 정하지 않는다
17
+ — 방문자가 읽는 모든 문자열은 prop이다.
18
+
19
+ ```sh
20
+ pnpm add @devslab/dds-table-solid
21
+ ```
22
+
23
+ 이 패키지는 자체 스타일시트를 내지 않는다 — 애플리케이션 셸에서
24
+ `@devslab/dds-css`의 테이블 스타일을, 그것이 의존하는 토큰 CSS와 함께 한 번
25
+ 불러온다:
26
+
27
+ ```ts
28
+ import "@devslab/dds-tokens/tokens.css";
29
+ import "@devslab/dds-css/components/table.css";
30
+ ```
31
+
32
+ 불러오지 않으면 `<DataTable>`은 스타일 없이 렌더링된다 — `fold`는 아무
33
+ 효과가 없고(컬럼 접기는 전적으로 `table.css`의 미디어 쿼리다), 시각적으로
34
+ 숨겨져야 할 caption과 액션 컬럼 헤더가 눈에 보이게 렌더링된다.
35
+
36
+ ```tsx
37
+ import { DataTable, type Column, type DataTableLabels } from "@devslab/dds-table-solid";
38
+
39
+ type Job = { id: string; name: string; count: number };
40
+
41
+ const columns: Column<Job>[] = [
42
+ { id: "name", label: "Name", cell: (row) => row.name, sortBy: (row) => row.name, rowHeader: true },
43
+ { id: "count", label: "Count", cell: (row) => String(row.count), sortBy: (row) => row.count, numeric: true, fold: true },
44
+ ];
45
+
46
+ const labels: DataTableLabels = {
47
+ sortBy: "Sort by {column}",
48
+ actions: "Actions",
49
+ nextPage: "Next page",
50
+ };
51
+
52
+ <DataTable rows={jobs} columns={columns} caption="Jobs" labels={labels} sort="client" />;
53
+ ```
54
+
55
+ ## `DataTable` props
56
+
57
+ | Prop | 타입 | 필수/기본값 | 의미 |
58
+ | --- | --- | --- | --- |
59
+ | `rows` | `readonly T[]` | 필수 | 데이터(어떤 순서로 렌더링할지는 아래 정렬 절에서 누가 책임지는지 설명). |
60
+ | `columns` | `readonly Column<T>[]` | 필수 | 컬럼 선언 — 아래 `Column<T>` 참고. |
61
+ | `caption` | `string` | 필수 | 테이블의 접근성 이름, 시각적으로 숨겨져 렌더링(`<caption class="dds-visually-hidden">`). |
62
+ | `labels` | `DataTableLabels` | 필수 | 컴포넌트가 렌더링할 수 있는 모든 문구 — 아래 "라벨은 필수다" 참고. |
63
+ | `sort` | `"client" \| { statedOrder: string }` | 선택, 생략 시 정렬 버튼도 순서 문구도 없음 | 아래 정렬 절 참고. |
64
+ | `density` | `"comfortable" \| "dense"` | 선택, 기본값 `"comfortable"`(40px 행) | `"dense"`는 `.dds-table--dense`를 붙인다(36px 행, 셀 패딩 축소). 어느 쪽이든 행 액션과 셀 안 작은 버튼은 행 높이에 맞춰진다(40px 행에 32px 버튼, 36px 행에 24px; 터치에서는 44px) — `docs/components.ko.md` Table 참고. |
65
+ | `scroll` | `"auto" \| "tall"` | 선택, 기본값 `"auto"` | `"tall"`은 `.dds-table-wrap--tall`을 붙인다 — `max-block-size: 70vh`로 스크롤되는 본문 + 고정(sticky) 헤더, 페이지를 밀어내면 안 되는 긴 인페이지 목록용. |
66
+ | `minWidth` | `string`(CSS 길이) | 선택 | `<table>`의 `min-inline-size`를 정한다 — 좁은 화면에서 컬럼이 짓눌리는 대신 `.dds-table-wrap` 안에서 가로 스크롤된다. |
67
+ | `actions` | `(row: T) => JSX.Element` | 선택 | 행마다 트레일링 셀을 렌더링(예: 행 단위 버튼); `labels.actions`로부터 시각적으로 숨겨진 헤더 셀을 추가한다. |
68
+ | `detail` | `(row: T) => JSX.Element` | 선택 | 주어진 행 아래에 펼침 행을 렌더링한다. `null`을 반환하면(예: 펼쳐지지 않은 행) 그 행에는 빈 행이 아니라 아예 detail `<tr>`이 생기지 않는다. |
69
+ | `page` | `{ nextHref?: string }` | 선택 | 아래 페이징 절 참고. |
70
+ | `empty` | `JSX.Element` | 선택 | `rows`가 비어 있을 때 `<table>` 대신 렌더링된다. 순서 문구와(있다면) 페이징 링크는 그대로 렌더링된다 — 커서 페이징 목록의 빈 페이지도 순서가 있고 여전히 다음으로 갈 방법이 필요하기 때문이다. |
71
+
72
+ ## `Column<T>`
73
+
74
+ | 필드 | 타입 | 의미 |
75
+ | --- | --- | --- |
76
+ | `id` | `string` | 컬럼 고유 식별자 — 정렬 키와 `data-fold`/`data-column` 속성에 쓰인다. |
77
+ | `label` | `string` | 헤더 텍스트(정렬 버튼의 화면 표시 라벨이기도 하다). |
78
+ | `cell` | `(row: T) => JSX.Element` | 셀 본문을 렌더링한다. 시각적 마크업은 소비자 몫이고, TanStack이 보는 컬럼 정의에는 정렬 접근자 외 아무것도 담기지 않는다. |
79
+ | `sortBy?` | `(row: T) => string \| number \| null` | 있으면 → (오직 `sort="client"` 모드에서만) 정렬 가능해지고 정렬 버튼이 붙는다. 없으면 → 평범한 헤더, 버튼이 절대 안 붙는다. "값 없음"은 `null`을 반환하면 되고, 그 행들은 맨 뒤로 정렬된다(`sortUndefined: "last"`) — 맨 앞이 아니다. |
80
+ | `width?` | `string` | CSS 너비. 아무 컬럼이든 이 값을 주는 순간 테이블 전체가 `table-layout: fixed`로 바뀌고 `<colgroup>`이 생성된다. |
81
+ | `fold?` | `boolean` | 좁은 화면에서 뺄 수 있는 컬럼으로 표시한다(아래 "컬럼 접기" 참고). |
82
+ | `numeric?` | `boolean` | 오른쪽 정렬 + 고정폭 숫자체를 적용한다. |
83
+ | `rowHeader?` | `boolean` | 이 컬럼의 셀을 `<td>` 대신 `<th scope="row">`로 렌더링한다 — 테이블당 정확히 한 컬럼만 이 값을 켜야 한다. |
84
+
85
+ ## 정렬: `sort`
86
+
87
+ `sort`는 두 모양 중 하나를 받는다 — 이 타입은 내보내지지 않는다(값을 그대로
88
+ 써넣으면 된다, import할 것이 없다):
89
+
90
+ ```ts
91
+ "client" | { statedOrder: string }
92
+ ```
93
+
94
+ - **`sort="client"`** — 테이블이 전체 목록을 메모리에 들고 있는 경우. `sortBy`가
95
+ 있는 컬럼에 정렬 버튼이 렌더링되고, 누르면 TanStack Table이 이미 손에 쥔 행을
96
+ 재정렬한다. 이게 안전한 이유는 정확히 "전체 행"과 "화면에 보이는 행"이 같은
97
+ 집합이기 때문이다.
98
+ - **`sort={{ statedOrder: "…" }}`** — 서버가 정렬해 커서로 페이징한 한 페이지를
99
+ 보여주는 테이블일 때. 이 모드에서는 `sortBy`가 있어도 어떤 컬럼도 정렬 버튼을
100
+ 렌더링하지 않는다 — 클라이언트 정렬은 현재 페이지에 있는 행만 재배열할 수
101
+ 있는데, 테이블의 진짜 순서는 브라우저가 아직 본 적 없는 여러 페이지에 걸쳐
102
+ 백엔드 쿼리가 만든 순서이기 때문이다. 그런 버튼은 데이터를 정렬한 것처럼
103
+ 보이지만 실제로는 잘못 보고하는 것이다. 대신 테이블은 자기 위에 실제 순서를
104
+ 텍스트로 찍는다(`.dds-table__order`) — `statedOrder`가 그 문장이다(예:
105
+ "최신순"). 백엔드의 정렬 어휘를 DDS는 모르므로 이 문장은 소비자가 넘긴다.
106
+
107
+ 서버 정렬 테이블이 "이 컬럼만" 클라이언트 정렬을 허용하도록 하는 세 번째
108
+ 모드는 없다 — 컬럼이 몇 개든 실패 양상(페이지 하나의 순서를 전체 목록의
109
+ 순서인 양 조용히 보고하는 것)은 동일하기 때문이다.
110
+
111
+ ## 페이징: `page`
112
+
113
+ ```ts
114
+ page?: { nextHref?: string }
115
+ ```
116
+
117
+ `nextHref`는 다음 페이지 컨트롤을 `<a class="dds-btn dds-btn--secondary" href="…">`
118
+ (라벨이 붙은 `<nav>` 안)로 렌더링한다 — `<button onClick>`이 아니다. 커서
119
+ 페이징 목록은 결국 쿼리 문자열만 다른 URL이고, 진짜 링크여야 JavaScript가
120
+ 꺼져 있을 때·하이드레이션 전 느린 연결 상태에서·브라우저의 "새 탭에서 열기"에서
121
+ 그대로 동작한다. `nextHref`(또는 `page` prop 전체)를 생략하면 페이징 `<nav>`는
122
+ 비활성 컨트롤을 그리지 않고 아예 사라진다.
123
+
124
+ ## 컬럼 접기: `fold`
125
+
126
+ `fold: true`로 표시한 컬럼은 고정된 분기점(`dds-css`의 `table.css`에 있는
127
+ `@media (max-width: 56.25rem)`) 아래에서 `[data-fold]`에 걸린 순수 CSS
128
+ 규칙으로 사라진다 — JavaScript로 만든 컬럼 표시 상태도, resize observer도,
129
+ 뷰포트별 재렌더도 없다. 컬럼은 DOM에 항상 존재하고 그릴지 말지는 브라우저가
130
+ 정한다. 그 덕에 접기는 SSR에서도 그대로 작동하고(스크립트가 돌기 전에도
131
+ 서버가 렌더한 좁은 화면은 이미 올바르게 그 컬럼을 숨긴 상태다), 정렬·페이징
132
+ 상태와 맞춰야 할 상태 자체가 없으므로 동기화 비용이 없다.
133
+
134
+ ## 라벨은 필수다, 기본값이 없다
135
+
136
+ `DataTableLabels`에는 선택 필드가 하나도 없고 `DataTable`은 대체 문자열을
137
+ 전혀 갖고 있지 않다:
138
+
139
+ ```ts
140
+ type DataTableLabels = {
141
+ sortBy: string; // "{column}"이 컬럼 라벨로 치환된다
142
+ actions: string; // 트레일링 액션 컬럼의 헤더(시각적으로 숨김)
143
+ nextPage: string; // 다음 페이지 링크의 접근성 이름이자 표시 텍스트
144
+ };
145
+ ```
146
+
147
+ DDS는 소비자의 사용자가 어떤 언어를 읽는지 정하지 않으므로, 화면 낭독기가
148
+ 읽을 영어(또는 다른 어떤 언어의) 문구도 대신 실어 보내지 않는다. 라벨을
149
+ 빠뜨린 소비자는 조용히 영어가 나가거나 빈칸이 나가는 대신 TypeScript
150
+ 에러를 받는다.
151
+
152
+ ## 이 패키지가 내보내지 않는 것
153
+
154
+ `DataTable`이 유일한 컴포넌트다. TanStack Table 자체 문서가 테이블을
155
+ 조립하는 재료로 쓰는 행/셀 프리미티브(`Th`, `Td`, `useDataTable` 훅)는
156
+ 의도적으로 미노출이다 — 오늘 소비자는 정확히 하나이고, 유지해야 할 API
157
+ 표면을 두 벌 두는 비용은 필요해지기 전에 미리 치를 이유가 없다. `DataTable`의
158
+ props로 표현할 수 없는 테이블 모양이 필요한 두 번째 소비자가 나타나면 그때
159
+ 프리미티브를 내보낸다.
160
+
161
+ 클라이언트 페이징·필터링(TanStack Table의 `getPaginationRowModel` 등)도
162
+ 마찬가지로 배선하지 않았다: 이 컴포넌트를 부르는 곳은 언제나 메모리에 다
163
+ 올라가는 작은 목록(`sort="client"`)이거나 이미 페이지가 나뉜 서버 응답
164
+ (`page.nextHref`)이라, 그 기능은 발화할 일이 없이 모든 소비자의 번들에
165
+ 얹히기만 할 것이다.
package/README.md CHANGED
@@ -1,3 +1,190 @@
1
- # Temporary Holding Version
1
+ # @devslab/dds-table-solid
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Declarative data table for DDS, built on TanStack Table (pinned to the exact
4
+ version `9.2.4` — see `docs/decisions.md` D-021 for why an exact pin). One
5
+ component, `DataTable`, plus the `Column<T>` and `DataTableLabels` types used
6
+ to describe it. The table itself decides nothing about words, routes, or
7
+ what "next page" means — every string a visitor can read is a prop.
8
+
9
+ ```sh
10
+ pnpm add @devslab/dds-table-solid
11
+ ```
12
+
13
+ This package ships no stylesheet of its own — import `@devslab/dds-css`'s
14
+ table styles once in the application shell, alongside the tokens CSS it
15
+ depends on:
16
+
17
+ ```ts
18
+ import "@devslab/dds-tokens/tokens.css";
19
+ import "@devslab/dds-css/components/table.css";
20
+ ```
21
+
22
+ Without it, `<DataTable>` renders an unstyled table: `fold` does nothing
23
+ (folding is entirely the CSS media query in `table.css`), and the
24
+ visually-hidden caption and actions-column header render visible.
25
+
26
+ ```tsx
27
+ import { DataTable, type Column, type DataTableLabels } from "@devslab/dds-table-solid";
28
+
29
+ type Job = { id: string; name: string; count: number };
30
+
31
+ const columns: Column<Job>[] = [
32
+ { id: "name", label: "Name", cell: (row) => row.name, sortBy: (row) => row.name, rowHeader: true },
33
+ { id: "count", label: "Count", cell: (row) => String(row.count), sortBy: (row) => row.count, numeric: true, fold: true },
34
+ ];
35
+
36
+ const labels: DataTableLabels = {
37
+ sortBy: "Sort by {column}",
38
+ actions: "Actions",
39
+ nextPage: "Next page",
40
+ };
41
+
42
+ <DataTable rows={jobs} columns={columns} caption="Jobs" labels={labels} sort="client" />;
43
+ ```
44
+
45
+ ## `DataTable` props
46
+
47
+ | Prop | Type | Required / default | Meaning |
48
+ | --- | --- | --- | --- |
49
+ | `rows` | `readonly T[]` | required | The data, in the order to render (see Sorting for who is responsible for that order). |
50
+ | `columns` | `readonly Column<T>[]` | required | Column declarations — see `Column<T>` below. |
51
+ | `caption` | `string` | required | The table's accessible name, rendered visually hidden (`<caption class="dds-visually-hidden">`). |
52
+ | `labels` | `DataTableLabels` | required | Every word the component can render — see "Labels are required" below. |
53
+ | `sort` | `SortMode` | optional, no sort buttons or order text when omitted | Client sorting, a server-order description, or controlled server sorting. |
54
+ | `resizing` | `ColumnResizing` | optional, disabled by default | Header resize handles, bounds, accessible label, and commit/reset callback. |
55
+ | `selection` | `RowSelection<T>` | optional, disabled by default | Stable row identity, controlled selection, and selection callback. |
56
+ | `density` | `"comfortable" \| "dense"` | optional, default `"comfortable"` (40px rows) | `"dense"` adds `.dds-table--dense` (36px rows, tighter cell padding). Either way, row actions and small buttons in cells are held to the row height (32px buttons in 40px rows, 24px in 36px rows; 44px on touch) — see `docs/components.md` Table. |
57
+ | `scroll` | `"auto" \| "tall"` | optional, default `"auto"` | `"tall"` adds `.dds-table-wrap--tall`: a `max-block-size: 70vh` scrolling body with a sticky header, for a long in-page list that shouldn't push the rest of the page down. |
58
+ | `minWidth` | `string` (a CSS length) | optional | Sets the `<table>`'s `min-inline-size`, so narrow viewports scroll the table horizontally (inside `.dds-table-wrap`) instead of crushing its columns. |
59
+ | `actions` | `(row: T) => JSX.Element` | optional | Renders a trailing cell per row (e.g. row-level buttons); adds a visually-hidden header cell from `labels.actions`. |
60
+ | `detail` | `(row: T) => JSX.Element` | optional | Renders an expandable row under a given row. Returning `null` (e.g. for a row that is not expanded) renders no detail `<tr>` for that row at all — not an empty one. |
61
+ | `page` | `{ nextHref?: string }` | optional | See Paging below. |
62
+ | `empty` | `JSX.Element` | optional | Rendered instead of the `<table>` when `rows` is empty. The stated-order text and the pagination link (if any) still render — an empty page of a cursor-paginated list still has an order and still needs a way forward. |
63
+
64
+ ## `Column<T>`
65
+
66
+ | Field | Type | Meaning |
67
+ | --- | --- | --- |
68
+ | `id` | `string` | Stable column identity — used for the sort key and the `data-fold`/`data-column` attributes. |
69
+ | `label` | `string` | Header text (and the sort button's visible label). |
70
+ | `cell` | `(row: T) => JSX.Element` | Renders the cell body. Visual markup stays yours; the column def TanStack sees carries nothing but the sort accessor. |
71
+ | `sortBy?` | `(row: T) => string \| number \| null` | Present → the column is sortable (in `sort="client"` mode only) and gets a sort button. Absent → a plain header, never a button. Return `null` for "no value"; those rows sort last (`sortUndefined: "last"`), never first. |
72
+ | `width?` | `string` | A CSS width. Setting it on any column switches the whole table to `table-layout: fixed` and emits a `<colgroup>`. |
73
+ | `resizable?` | `boolean` | Set to `false` to omit this column's handle when resizing is enabled. |
74
+ | `fold?` | `boolean` | Marks the column droppable on narrow viewports (see Folding below). |
75
+ | `numeric?` | `boolean` | Right-aligns the column and applies tabular figures. |
76
+ | `rowHeader?` | `boolean` | Renders this column's cell as `<th scope="row">` instead of `<td>` — exactly one column per table should set it. |
77
+
78
+ ## Sorting: `sort`
79
+
80
+ `SortMode` is exported and accepts three shapes:
81
+
82
+ ```ts
83
+ "client" | { statedOrder: string } | { value: SortValue; onChange: (value: SortValue) => void; statedOrder?: string }
84
+ ```
85
+
86
+ - **`sort="client"`** — the table holds its whole list in memory. Columns
87
+ with `sortBy` render a sort button; clicking one asks TanStack Table to
88
+ reorder the rows already in hand. This is safe precisely because "all the
89
+ rows" and "the rows shown" are the same set.
90
+ - **`sort={{ statedOrder: "…" }}`** — for a table that shows a server-ordered,
91
+ cursor-paged slice. No column renders a sort button in this mode, even if
92
+ its `sortBy` is set: a client-side sort could only reorder the rows on the
93
+ current page, while the table's real order is whatever the backend query
94
+ produced across pages the browser has never seen. A button that did that
95
+ would look like it sorted the data and would actually misreport it. Instead
96
+ the table prints the actual order as text above itself (`.dds-table__order`)
97
+ — `statedOrder` is that sentence, e.g. `"Newest first"`, supplied by the
98
+ consumer because DDS does not know the backend's ordering vocabulary.
99
+
100
+ There is no third mode that lets a server-ordered table opt into client
101
+ sorting for "just this column" — the failure mode (silently reporting a
102
+ per-page order as the whole list's order) is the same regardless of how many
103
+ columns are involved.
104
+
105
+ ## Optional workspace interactions
106
+
107
+ All interactions below are opt-in; existing tables retain their behavior.
108
+
109
+ ```tsx
110
+ const [sort, setSort] = createSignal<SortValue>(null);
111
+ const [selectedId, setSelectedId] = createSignal<string | null>(null);
112
+ <DataTable rows={rows} columns={columns} caption="Jobs" labels={labels}
113
+ sort={{ value: sort(), onChange: setSort }}
114
+ resizing={{ label: "Resize {column}", minWidth: 80, maxWidth: 1200,
115
+ onChange: (widths, reason) => recordLayoutChange(widths, reason) }}
116
+ selection={{ rowId: row => row.id, selectedId: selectedId(),
117
+ onSelect: row => setSelectedId(row.id) }} />;
118
+ ```
119
+
120
+ Import `SortValue`, `ColumnResizing`, and `RowSelection` from this package for
121
+ typed application adapters. Controlled sorting cycles ascending, descending,
122
+ then `null`. It only requests a change; the application must fetch and supply
123
+ the correctly ordered server result. It never sorts the current page locally.
124
+ The existing `{ statedOrder }` mode remains a description without sort buttons.
125
+
126
+ `resizing.label` is the application-owned accessible label (`{column}` is
127
+ replaced with the column name). Drag a header separator to change its width.
128
+ Left/Right change width by 12px (Shift: 50px); Home/End use the bounds.
129
+ Double-click or Escape clears **all** width overrides, restoring declared
130
+ column widths and the original layout. Set `column.resizable = false` to omit
131
+ an individual handle. Pointer cancellation restores the pre-drag layout;
132
+ unmounting removes pending pointer listeners. Resize callbacks fire on commit
133
+ or reset, rather than every pointer move. Width overrides last for this mounted
134
+ table only; persistence and application analytics remain application-owned.
135
+
136
+ Selection is controlled by a stable row ID. Click, Enter, and Space select a
137
+ row; embedded buttons, links and form controls keep their own behavior.
138
+ Selection does not navigate, open dialogs, or change business data by itself.
139
+
140
+ ## Paging: `page`
141
+
142
+ ```ts
143
+ page?: { nextHref?: string }
144
+ ```
145
+
146
+ `nextHref` renders the next-page control as `<a class="dds-btn dds-btn--secondary" href="…">`, wrapped in a labeled `<nav>` — never a `<button onClick>`. A cursor-paginated list is just a URL with a different query string, and a real link keeps that working with JavaScript disabled, on a slow connection before hydration, and in a browser's native "open in new tab". Omitting `nextHref` (or the whole `page` prop) hides the pagination `<nav>` entirely rather than rendering a disabled control.
147
+
148
+ ## Folding: `fold`
149
+
150
+ Columns marked `fold: true` disappear below a fixed breakpoint
151
+ (`@media (max-width: 56.25rem)` in `dds-css`'s `table.css`) via a plain CSS
152
+ rule on `[data-fold]` — there is no JavaScript column-visibility state, no
153
+ resize observer, and no per-viewport re-render. The column is always present
154
+ in the DOM; the browser decides whether to paint it. This means folding
155
+ survives SSR (a server-rendered narrow viewport already hides the column,
156
+ correctly, before any script runs) and costs nothing to keep in sync with
157
+ sorting or paging state, because there is no state to keep in sync.
158
+
159
+ ## Labels are required, not defaulted
160
+
161
+ `DataTableLabels` has no optional fields and `DataTable` ships no fallback
162
+ strings:
163
+
164
+ ```ts
165
+ type DataTableLabels = {
166
+ sortBy: string; // "{column}" is replaced with the column's label
167
+ actions: string; // header of the trailing actions column (visually hidden)
168
+ nextPage: string; // accessible name and text of the next-page link
169
+ };
170
+ ```
171
+
172
+ DDS does not choose the language a consumer's users read in, so it does not
173
+ ship English (or any other) copy for a screen reader to announce. A consumer
174
+ that forgets a label gets a TypeScript error, not a silently English or
175
+ silently blank control.
176
+
177
+ ## What this package does not export
178
+
179
+ `DataTable` is the only component. The row/cell primitives TanStack Table's
180
+ own docs build tables from (a `Th`, a `Td`, a `useDataTable` hook) are
181
+ deliberately unexported — there is exactly one consumer today, and a second
182
+ API surface to keep stable is a cost paid before anything needs it. When a
183
+ second consumer needs a table shape `DataTable`'s props cannot express,
184
+ export the primitives then.
185
+
186
+ Client-side pagination and filtering (TanStack Table's `getPaginationRowModel`
187
+ et al.) are similarly not wired in: this component's callers are always
188
+ either a small in-memory list (`sort="client"`) or an already-paged server
189
+ response (`page.nextHref`), so neither feature would ever fire — it would only
190
+ sit in every consumer's bundle.
@@ -0,0 +1,90 @@
1
+ import type { JSX } from "solid-js";
2
+ import { createSortedRowModel, sortFn_alphanumeric, sortFn_text, type ColumnDef } from "@tanstack/solid-table";
3
+ export type Column<T> = {
4
+ id: string;
5
+ label: string;
6
+ cell: (row: T) => JSX.Element;
7
+ sortBy?: (row: T) => string | number | null;
8
+ width?: string;
9
+ resizable?: boolean;
10
+ fold?: boolean;
11
+ numeric?: boolean;
12
+ rowHeader?: boolean;
13
+ };
14
+ export type DataTableLabels = {
15
+ /** Accessible name for a sort button; `{column}` is replaced with the column label. */
16
+ sortBy: string;
17
+ /** Header of the trailing actions column, rendered visually hidden. */
18
+ actions: string;
19
+ /** Label of the next-page link. */
20
+ nextPage: string;
21
+ };
22
+ /**
23
+ * The feature set every `DataTable` instance is built from: the core row
24
+ * model plus client-side sorting. Built once (per `tableFeatures`'
25
+ * recommendation to use it statically outside a component) so `toColumnDefs`
26
+ * and `createTable` agree on the same `TFeatures` type.
27
+ */
28
+ declare const baseFeatures: {
29
+ coreTablesFeature: import("@tanstack/solid-table").TableFeature;
30
+ coreRowsFeature: import("@tanstack/solid-table").TableFeature;
31
+ coreColumnsFeature: import("@tanstack/solid-table").TableFeature;
32
+ coreHeadersFeature: import("@tanstack/solid-table").TableFeature;
33
+ coreCellsFeature: import("@tanstack/solid-table").TableFeature;
34
+ coreRowModelsFeature: import("@tanstack/solid-table").TableFeature;
35
+ rowSortingFeature: import("@tanstack/solid-table").TableFeature;
36
+ };
37
+ /** The `TFeatures` type used everywhere a table/column type needs one. Kept
38
+ * separate from the `sortedRowModel` factory below: `createSortedRowModel`'s
39
+ * return type embeds `Table<TFeatures, TData>`, so folding it into the very
40
+ * type it closes over makes `TFeatures` self-referential — TypeScript's
41
+ * structural comparison of that recursive shape (observed while wiring this
42
+ * up) reports unrelated `ColumnDef`/`Column`/`HeaderContext` instantiations
43
+ * a few frames down a completely unrelated diff, because it can't resolve
44
+ * the cycle. Naming the narrower, non-recursive type here and using it as
45
+ * the explicit `TFeatures` argument everywhere avoids that. */
46
+ export type TableFeatureSet = typeof baseFeatures;
47
+ /**
48
+ * Registering `rowSortingFeature` alone is not enough for sorting to reach
49
+ * the screen — `sortedRowModel` is the factory `table.getRowModel()` actually
50
+ * calls to produce sorted rows; without it, `state.sorting` changes but the
51
+ * rendered row order never does. Added here, after `TableFeatureSet` is
52
+ * already named, so its function type closes over the narrow type instead of
53
+ * over itself.
54
+ *
55
+ * `sortFns` registers the names `sortFn: "auto"` (the default every column
56
+ * gets — see `toColumnDefs`) can actually resolve to for a `Column<T>` whose
57
+ * `sortBy` returns `string | number | null`: plain strings resolve to
58
+ * `"text"`, strings containing digit runs (`"Row 10"`, `"SKU100"`) resolve to
59
+ * `"alphanumeric"`, and numbers/`null` never set a name at all and fall
60
+ * straight to the always-available `sortFn_basic` — so `"datetime"` is never
61
+ * reachable and is deliberately not registered. Leaving either reachable name
62
+ * unregistered means `"auto"` silently falls back to `sortFn_basic`, which
63
+ * compares raw values instead of lowercasing first: a case-sensitive sort
64
+ * that also warns on every column in development
65
+ * (`rowSortingFeature.utils.js`'s `column_getAutoSortFn`). Importing the two
66
+ * named functions (rather than the `sortFns` barrel export, whose own
67
+ * docstring says registering it "opts out of tree-shaking") keeps every
68
+ * other built-in sorting function — including the case-sensitive and
69
+ * datetime variants this component never produces — out of the bundle.
70
+ */
71
+ export declare const tableFeatureSet: TableFeatureSet & {
72
+ sortedRowModel: ReturnType<typeof createSortedRowModel<TableFeatureSet, Record<string, unknown>>>;
73
+ sortFns: {
74
+ text: typeof sortFn_text;
75
+ alphanumeric: typeof sortFn_alphanumeric;
76
+ };
77
+ };
78
+ /** Turns our declarations into TanStack column defs. Sorting is enabled per
79
+ * column by the presence of `sortBy`; everything visual stays in our own
80
+ * markup, so the def carries only what the model needs.
81
+ *
82
+ * `Column<T>` deliberately leaves `T` unconstrained (any row shape a
83
+ * consumer wants to render), but TanStack's `RowData` requires
84
+ * `Record<string, any> | Array<any>`. Every real row is one of those at
85
+ * runtime, so the helper is instantiated at `Record<string, unknown>` and
86
+ * the accessor casts its parameter back to `T` — a boundary cast, not a
87
+ * behavior change. */
88
+ export declare function toColumnDefs<T>(columns: readonly Column<T>[]): ColumnDef<TableFeatureSet, Record<string, unknown>, unknown>[];
89
+ export {};
90
+ //# sourceMappingURL=columns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"columns.d.ts","sourceRoot":"","sources":["../src/columns.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAQL,oBAAoB,EAEpB,mBAAmB,EACnB,WAAW,EAEX,KAAK,SAAS,EACf,MAAM,uBAAuB,CAAC;AAE/B,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,GAAG,CAAC,OAAO,CAAC;IAC9B,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,uFAAuF;IACvF,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;GAKG;AACH,QAAA,MAAM,YAAY;;;;;;;;CAQhB,CAAC;AAEH;;;;;;;;gEAQgE;AAChE,MAAM,MAAM,eAAe,GAAG,OAAO,YAAY,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,eAAe,EAAE,eAAe,GAAG;IAC9C,cAAc,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAClG,OAAO,EAAE;QAAE,IAAI,EAAE,OAAO,WAAW,CAAC;QAAC,YAAY,EAAE,OAAO,mBAAmB,CAAA;KAAE,CAAC;CAKjF,CAAC;AAEF;;;;;;;;;uBASuB;AACvB,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,SAAS,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,CAsB7H"}
@@ -0,0 +1,43 @@
1
+ import { type JSX } from "solid-js";
2
+ import { type Column, type DataTableLabels } from "./columns";
3
+ export type SortValue = {
4
+ id: string;
5
+ desc: boolean;
6
+ } | null;
7
+ export type SortMode = "client" | {
8
+ statedOrder: string;
9
+ } | {
10
+ value: SortValue;
11
+ onChange: (value: SortValue) => void;
12
+ statedOrder?: string;
13
+ };
14
+ export type ColumnResizing = {
15
+ label: string;
16
+ minWidth?: number;
17
+ maxWidth?: number;
18
+ onChange?: (widths: Readonly<Record<string, number>>, reason: "resize" | "reset") => void;
19
+ };
20
+ export type RowSelection<T> = {
21
+ rowId: (row: T) => string;
22
+ selectedId?: string | null;
23
+ onSelect: (row: T) => void;
24
+ };
25
+ export declare function DataTable<T>(props: {
26
+ rows: readonly T[];
27
+ columns: readonly Column<T>[];
28
+ caption: string;
29
+ labels: DataTableLabels;
30
+ sort?: SortMode;
31
+ density?: "comfortable" | "dense";
32
+ scroll?: "auto" | "tall";
33
+ minWidth?: string;
34
+ actions?: (row: T) => JSX.Element;
35
+ detail?: (row: T) => JSX.Element;
36
+ page?: {
37
+ nextHref?: string | undefined;
38
+ };
39
+ empty?: JSX.Element;
40
+ resizing?: ColumnResizing;
41
+ selection?: RowSelection<T>;
42
+ }): JSX.Element;
43
+ //# sourceMappingURL=data-table.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-table.d.ts","sourceRoot":"","sources":["../src/data-table.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA2D,KAAK,GAAG,EAAE,MAAM,UAAU,CAAC;AAE7F,OAAO,EAAiC,KAAK,MAAM,EAAE,KAAK,eAAe,EAAwB,MAAM,WAAW,CAAC;AAEnH,MAAM,MAAM,SAAS,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC;AAC7D,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAC7I,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,KAAK,IAAI,CAAC;CAC3F,CAAC;AACF,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;IAAE,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAA;CAAE,CAAC;AAWpH,wBAAgB,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE;IAClC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC;IACnB,OAAO,EAAE,SAAS,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,eAAe,CAAC;IACxB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,GAAG,CAAC,OAAO,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,GAAG,CAAC,OAAO,CAAC;IACjC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IACzC,KAAK,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,SAAS,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;CAC7B,GAAG,GAAG,CAAC,OAAO,CA6Kd"}
@@ -0,0 +1,4 @@
1
+ export { DataTable } from "./data-table";
2
+ export type { SortMode, SortValue, ColumnResizing, RowSelection } from "./data-table";
3
+ export type { Column, DataTableLabels } from "./columns";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACtF,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC"}