@shadow-js/ui 0.0.0-stage → 1.0.0

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.
Files changed (190) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +598 -2
  4. package/dist/cli.mjs +2293 -0
  5. package/dist/dev/cli/schema.d.ts +73 -0
  6. package/dist/dev/index.d.ts +6 -0
  7. package/dist/dev/index.js +5 -0
  8. package/dist/dev/locales/ar.d.ts +141 -0
  9. package/dist/dev/locales/ar.js +128 -0
  10. package/dist/dev/locales/ar.js.map +1 -0
  11. package/dist/dev/locales/en.d.ts +141 -0
  12. package/dist/dev/locales/en.js +128 -0
  13. package/dist/dev/locales/en.js.map +1 -0
  14. package/dist/dev/registry/utils/utils.d.ts +29 -0
  15. package/dist/dev/registry/utils/utils.js +27 -0
  16. package/dist/dev/registry/utils/utils.js.map +1 -0
  17. package/dist/dev/theme/theme-logic.d.ts +187 -0
  18. package/dist/dev/theme/theme-logic.js +184 -0
  19. package/dist/dev/theme/theme-logic.js.map +1 -0
  20. package/dist/dev/theme/theme-provider.d.ts +90 -0
  21. package/dist/dev/theme/theme-provider.js +150 -0
  22. package/dist/dev/theme/theme-provider.js.map +1 -0
  23. package/dist/prod/index.js +1 -0
  24. package/dist/prod/locales/ar.js +2 -0
  25. package/dist/prod/locales/ar.js.map +1 -0
  26. package/dist/prod/locales/en.js +2 -0
  27. package/dist/prod/locales/en.js.map +1 -0
  28. package/dist/prod/registry/utils/utils.js +2 -0
  29. package/dist/prod/registry/utils/utils.js.map +1 -0
  30. package/dist/prod/theme/theme-logic.js +2 -0
  31. package/dist/prod/theme/theme-logic.js.map +1 -0
  32. package/dist/prod/theme/theme-provider.js +2 -0
  33. package/dist/prod/theme/theme-provider.js.map +1 -0
  34. package/package.json +102 -4
  35. package/registry/accordion/accordion.tsx +210 -0
  36. package/registry/accordion/item.json +4 -0
  37. package/registry/alert/alert.tsx +200 -0
  38. package/registry/alert/item.json +4 -0
  39. package/registry/alert-dialog/alert-dialog.tsx +530 -0
  40. package/registry/alert-dialog/item.json +4 -0
  41. package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
  42. package/registry/aspect-ratio/item.json +4 -0
  43. package/registry/attachment/attachment.tsx +459 -0
  44. package/registry/attachment/item.json +4 -0
  45. package/registry/avatar/avatar.tsx +385 -0
  46. package/registry/avatar/item.json +4 -0
  47. package/registry/badge/badge.tsx +108 -0
  48. package/registry/badge/item.json +4 -0
  49. package/registry/breadcrumb/breadcrumb.tsx +354 -0
  50. package/registry/breadcrumb/item.json +4 -0
  51. package/registry/bubble/bubble.tsx +260 -0
  52. package/registry/bubble/item.json +4 -0
  53. package/registry/button/button.tsx +125 -0
  54. package/registry/button/item.json +4 -0
  55. package/registry/button-group/button-group.tsx +189 -0
  56. package/registry/button-group/item.json +4 -0
  57. package/registry/calendar/calendar.tsx +348 -0
  58. package/registry/calendar/item.json +4 -0
  59. package/registry/card/card.tsx +311 -0
  60. package/registry/card/item.json +4 -0
  61. package/registry/carousel/carousel.tsx +851 -0
  62. package/registry/carousel/item.json +4 -0
  63. package/registry/chart/chart.tsx +1956 -0
  64. package/registry/chart/item.json +4 -0
  65. package/registry/chart-scale/chart-scale.ts +670 -0
  66. package/registry/chart-scale/item.json +5 -0
  67. package/registry/checkbox/checkbox.tsx +79 -0
  68. package/registry/checkbox/item.json +4 -0
  69. package/registry/code-block/code-block.tsx +470 -0
  70. package/registry/code-block/item.json +4 -0
  71. package/registry/collapsible/collapsible.tsx +117 -0
  72. package/registry/collapsible/item.json +4 -0
  73. package/registry/combobox/combobox.tsx +977 -0
  74. package/registry/combobox/item.json +4 -0
  75. package/registry/command/command.tsx +757 -0
  76. package/registry/command/item.json +4 -0
  77. package/registry/context-menu/context-menu.tsx +697 -0
  78. package/registry/context-menu/item.json +4 -0
  79. package/registry/data-table/data-table.tsx +437 -0
  80. package/registry/data-table/item.json +4 -0
  81. package/registry/date-picker/date-picker.tsx +349 -0
  82. package/registry/date-picker/item.json +4 -0
  83. package/registry/dialog/dialog.tsx +412 -0
  84. package/registry/dialog/item.json +4 -0
  85. package/registry/direction/direction.tsx +88 -0
  86. package/registry/direction/item.json +4 -0
  87. package/registry/drawer/drawer.tsx +787 -0
  88. package/registry/drawer/item.json +4 -0
  89. package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
  90. package/registry/dropdown-menu/item.json +4 -0
  91. package/registry/empty/empty.tsx +240 -0
  92. package/registry/empty/item.json +4 -0
  93. package/registry/field/field.tsx +624 -0
  94. package/registry/field/item.json +4 -0
  95. package/registry/form/form.tsx +1112 -0
  96. package/registry/form/item.json +4 -0
  97. package/registry/hover-card/hover-card.tsx +144 -0
  98. package/registry/hover-card/item.json +4 -0
  99. package/registry/icons/icons.tsx +1691 -0
  100. package/registry/icons/item.json +4 -0
  101. package/registry/input/input.tsx +43 -0
  102. package/registry/input/item.json +4 -0
  103. package/registry/input-group/input-group.tsx +315 -0
  104. package/registry/input-group/item.json +4 -0
  105. package/registry/input-otp/input-otp.tsx +329 -0
  106. package/registry/input-otp/item.json +4 -0
  107. package/registry/item/item.json +4 -0
  108. package/registry/item/item.tsx +439 -0
  109. package/registry/kbd/item.json +4 -0
  110. package/registry/kbd/kbd.tsx +65 -0
  111. package/registry/label/item.json +4 -0
  112. package/registry/label/label.tsx +60 -0
  113. package/registry/marker/item.json +4 -0
  114. package/registry/marker/marker.tsx +175 -0
  115. package/registry/menubar/item.json +4 -0
  116. package/registry/menubar/menubar.tsx +694 -0
  117. package/registry/message/item.json +4 -0
  118. package/registry/message/message.tsx +280 -0
  119. package/registry/message-scroller/item.json +4 -0
  120. package/registry/message-scroller/message-scroller.tsx +262 -0
  121. package/registry/native-select/item.json +4 -0
  122. package/registry/native-select/native-select.tsx +113 -0
  123. package/registry/navigation-menu/item.json +4 -0
  124. package/registry/navigation-menu/navigation-menu.tsx +362 -0
  125. package/registry/pagination/item.json +4 -0
  126. package/registry/pagination/pagination.tsx +460 -0
  127. package/registry/password-input/item.json +5 -0
  128. package/registry/password-input/password-input.tsx +494 -0
  129. package/registry/popover/item.json +4 -0
  130. package/registry/popover/popover.tsx +284 -0
  131. package/registry/progress/item.json +4 -0
  132. package/registry/progress/progress.tsx +190 -0
  133. package/registry/questionnaire/item.json +4 -0
  134. package/registry/questionnaire/questionnaire.tsx +569 -0
  135. package/registry/radio-group/item.json +4 -0
  136. package/registry/radio-group/radio-group.tsx +101 -0
  137. package/registry/resizable/item.json +4 -0
  138. package/registry/resizable/resizable.tsx +455 -0
  139. package/registry/scroll-area/item.json +4 -0
  140. package/registry/scroll-area/scroll-area.tsx +149 -0
  141. package/registry/select/item.json +4 -0
  142. package/registry/select/select.tsx +571 -0
  143. package/registry/separator/item.json +4 -0
  144. package/registry/separator/separator.tsx +52 -0
  145. package/registry/sheet/item.json +4 -0
  146. package/registry/sheet/sheet.tsx +476 -0
  147. package/registry/sidebar/item.json +4 -0
  148. package/registry/sidebar/sidebar.tsx +1870 -0
  149. package/registry/skeleton/item.json +4 -0
  150. package/registry/skeleton/skeleton.tsx +47 -0
  151. package/registry/slider/item.json +4 -0
  152. package/registry/slider/slider.tsx +149 -0
  153. package/registry/spinner/item.json +4 -0
  154. package/registry/spinner/spinner.tsx +51 -0
  155. package/registry/switch/item.json +4 -0
  156. package/registry/switch/switch.tsx +93 -0
  157. package/registry/table/item.json +4 -0
  158. package/registry/table/table.tsx +373 -0
  159. package/registry/tabs/item.json +4 -0
  160. package/registry/tabs/tabs.tsx +209 -0
  161. package/registry/textarea/item.json +4 -0
  162. package/registry/textarea/textarea.tsx +51 -0
  163. package/registry/toast/item.json +4 -0
  164. package/registry/toast/toast.tsx +949 -0
  165. package/registry/toggle/item.json +4 -0
  166. package/registry/toggle/toggle.tsx +110 -0
  167. package/registry/toggle-group/item.json +4 -0
  168. package/registry/toggle-group/toggle-group.tsx +189 -0
  169. package/registry/tooltip/item.json +4 -0
  170. package/registry/tooltip/tooltip.tsx +169 -0
  171. package/registry/trace/item.json +4 -0
  172. package/registry/trace/trace.tsx +356 -0
  173. package/registry/ui-strings/item.json +5 -0
  174. package/registry/ui-strings/ui-strings.tsx +454 -0
  175. package/registry/use-highlighted-code/item.json +5 -0
  176. package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
  177. package/registry/use-mobile/item.json +5 -0
  178. package/registry/use-mobile/use-mobile.ts +46 -0
  179. package/registry/utils/item.json +5 -0
  180. package/registry/utils/utils.ts +38 -0
  181. package/registry.json +3274 -0
  182. package/styles/animations.css +321 -0
  183. package/styles/theme-core.css +55 -0
  184. package/styles/theme.css +12 -0
  185. package/styles/themes/gray.css +69 -0
  186. package/styles/themes/neutral.css +69 -0
  187. package/styles/themes/slate.css +69 -0
  188. package/styles/themes/stone.css +69 -0
  189. package/styles/themes/zinc.css +69 -0
  190. package/styles/typeset.css +251 -0
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Context Menu",
3
+ "description": "Displays a menu to the user, such as a set of actions or functions, triggered by a right click or long press."
4
+ }
@@ -0,0 +1,437 @@
1
+ import { untrack } from "@shadow-js/core";
2
+ import type { Direction } from "@shadow-js/primitives";
3
+ import type { Column, Header, Row, Table } from "@shadow-js/table";
4
+ import { For, type JSX, type JsxChild } from "@shadow-js/web";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuCheckboxItem,
9
+ DropdownMenuContent,
10
+ DropdownMenuTrigger,
11
+ } from "@/components/ui/dropdown-menu";
12
+ import {
13
+ TableBody,
14
+ TableCell,
15
+ TableHead,
16
+ TableHeader,
17
+ Table as TableRoot,
18
+ TableRow,
19
+ } from "@/components/ui/table";
20
+ import { useUiStrings } from "@/lib/ui-strings";
21
+ import { cn } from "@/lib/utils";
22
+
23
+ /**
24
+ * Props of {@link DataTable}: the table to render and the options of its element.
25
+ * @see {@link DataTable}
26
+ * @since 1.0.0
27
+ */
28
+ export interface DataTableProps<TData> {
29
+ /** The table from `createTable` in `@shadow-js/table`. @default none (required) */
30
+ readonly table: Table<TData>;
31
+ /** Classes for the `table` element, merged with the table's own classes. @default none */
32
+ readonly class?: string;
33
+ /**
34
+ * Shown in one cell spanning every column while there are no rows.
35
+ * @default "No results."
36
+ */
37
+ readonly empty?: JsxChild;
38
+ /**
39
+ * The table's accessible name. It names the table and the scrollable region around it, so a wide
40
+ * table is a labeled region that keyboard users can scroll. Pass it or `aria-labelledby`.
41
+ * @default none
42
+ */
43
+ readonly "aria-label"?: string | undefined;
44
+ /**
45
+ * The id of a visible heading that names the table. Use it instead of `aria-label` when the page
46
+ * already has the heading.
47
+ * @default none
48
+ */
49
+ readonly "aria-labelledby"?: string | undefined;
50
+ }
51
+
52
+ /**
53
+ * The `aria-sort` value of a column header: the direction the column is sorted in, `"none"` when it
54
+ * is sortable but not sorted, and `undefined` (no attribute) when it cannot be sorted at all.
55
+ * @internal
56
+ */
57
+ function sortState<TData>(column: Column<TData>): "ascending" | "descending" | "none" | undefined {
58
+ if (!column.getCanSort()) return undefined;
59
+ const sorted = column.getIsSorted();
60
+ return sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "none";
61
+ }
62
+
63
+ /**
64
+ * Renders a column's `header` or `cell` definition. A function is called with the context; a cell
65
+ * without a definition shows its value (`renderValue`, passed as the fallback).
66
+ */
67
+ function render(definition: unknown, context: unknown): JsxChild {
68
+ if (typeof definition === "function") return definition(context) as JsxChild;
69
+ if (definition === undefined || definition === null) return "";
70
+ return String(definition);
71
+ }
72
+
73
+ /**
74
+ * A table that renders a `@shadow-js/table` table: one header cell per visible column (a group header
75
+ * spans its children), one row per row of the current row model, and `data-state="selected"` on the
76
+ * rows that are selected. A sort or a page change updates only the parts that changed. Use
77
+ * {@link DataTableColumnHeader} for sortable headers and {@link DataTablePagination} for the footer.
78
+ *
79
+ * Keyboard and ARIA: the element is a native table. Each header cell is a `columnheader` with
80
+ * `aria-sort` set to `"ascending"`, `"descending"` or `"none"` when the column can be sorted, and no
81
+ * `aria-sort` when it cannot. Sort buttons and pagination buttons are native buttons, so Enter and
82
+ * Space activate them, and a disabled button is skipped. Name the table with `aria-label` or
83
+ * `aria-labelledby` so its scrollable region has a name.
84
+ *
85
+ * shadcn/ui differences: the table is driven by `@shadow-js/table` (`createTable`) rather than by
86
+ * TanStack Table, so `table.getRowModel()`, `column.toggleSorting()` and the other table methods come
87
+ * from that package. The prop is `class`, not `className`, and every element has a `data-slot`.
88
+ *
89
+ * @param props The table and its options: see {@link DataTableProps}.
90
+ * @returns The table element.
91
+ * @example
92
+ * import { createColumnHelper, createTable } from "@shadow-js/table";
93
+ * import { layout } from "@shadow-js/table/layout";
94
+ * import { DataTable, DataTableColumnHeader, DataTablePagination } from "@/components/ui/data-table";
95
+ *
96
+ * interface Person {
97
+ * id: string;
98
+ * name: string;
99
+ * age: number;
100
+ * }
101
+ *
102
+ * const helper = createColumnHelper<Person>();
103
+ * const people: Person[] = [
104
+ * { id: "1", name: "Ada", age: 36 },
105
+ * { id: "2", name: "Grace", age: 85 },
106
+ * ];
107
+ *
108
+ * export function PeopleTable() {
109
+ * const table = createTable({
110
+ * data: people,
111
+ * getRowId: (row) => row.id,
112
+ * features: [layout],
113
+ * columns: [
114
+ * helper.accessor("name", {
115
+ * header: (context) => <DataTableColumnHeader column={context.column} title="Name" />,
116
+ * }),
117
+ * helper.accessor("age", { header: "Age" }),
118
+ * ],
119
+ * });
120
+ * return (
121
+ * <>
122
+ * <DataTable table={table} aria-label="People" />
123
+ * <DataTablePagination table={table} />
124
+ * </>
125
+ * );
126
+ * }
127
+ * @see {@link DataTableColumnHeader}
128
+ * @see {@link DataTablePagination}
129
+ * @since 1.0.0
130
+ */
131
+ export function DataTable<TData>(props: DataTableProps<TData>): JSX.Element {
132
+ const table = props.table;
133
+ const t = useUiStrings();
134
+ return (
135
+ <TableRoot
136
+ class={cn(props.class)}
137
+ aria-label={props["aria-label"]}
138
+ aria-labelledby={props["aria-labelledby"]}
139
+ >
140
+ <TableHeader>
141
+ {() =>
142
+ table.getHeaderGroups().map((group) => (
143
+ <TableRow>
144
+ {group.headers.map((header: Header<TData>) =>
145
+ // A placeholder is covered by the header above it (its rowSpan), so it is no cell.
146
+ header.isPlaceholder ? null : (
147
+ <TableHead
148
+ colSpan={header.colSpan}
149
+ rowSpan={header.rowSpan}
150
+ aria-sort={sortState(header.column)}
151
+ >
152
+ {render(header.column.columnDef.header, header.getContext())}
153
+ </TableHead>
154
+ ),
155
+ )}
156
+ </TableRow>
157
+ ))
158
+ }
159
+ </TableHeader>
160
+ <TableBody>
161
+ {() => {
162
+ const rows = table.getRowModel().rows;
163
+ if (rows.length === 0) {
164
+ return (
165
+ <TableRow>
166
+ <TableCell colSpan={table.getVisibleLeafColumns().length}>
167
+ {props.empty ?? t("dataTable.empty", "No results.")}
168
+ </TableCell>
169
+ </TableRow>
170
+ );
171
+ }
172
+ return rows.map((row: Row<TData>) => (
173
+ <TableRow data-state={row.getIsSelected() ? "selected" : undefined}>
174
+ {row.getVisibleCells().map((cell) => (
175
+ <TableCell>
176
+ {render(cell.column.columnDef.cell ?? cell.renderValue, cell.getContext())}
177
+ </TableCell>
178
+ ))}
179
+ </TableRow>
180
+ ));
181
+ }}
182
+ </TableBody>
183
+ </TableRoot>
184
+ );
185
+ }
186
+
187
+ /**
188
+ * Props of {@link DataTableColumnHeader}: the column to sort and the visible title.
189
+ * @see {@link DataTableColumnHeader}
190
+ * @since 1.0.0
191
+ */
192
+ export interface DataTableColumnHeaderProps<TData> {
193
+ /** The column this header sorts, from the header context (`context.column`). @default none (required) */
194
+ readonly column: Column<TData>;
195
+ /**
196
+ * The visible title of the header.
197
+ * @default the column id
198
+ */
199
+ readonly title?: string;
200
+ }
201
+
202
+ /**
203
+ * A button in a column header that sorts its column. Each click cycles the column through ascending,
204
+ * descending and unsorted (`column.toggleSorting()`). The header is disabled when the column cannot
205
+ * be sorted. The direction is shown as an arrow and announced by `aria-sort` on the column header
206
+ * cell, which {@link DataTable} sets; the button itself has no `aria-sort`.
207
+ *
208
+ * @param props The header props: see {@link DataTableColumnHeaderProps}.
209
+ * @returns The sort button.
210
+ * @example
211
+ * import { createColumnHelper } from "@shadow-js/table";
212
+ * import { DataTableColumnHeader } from "@/components/ui/data-table";
213
+ *
214
+ * interface Person {
215
+ * name: string;
216
+ * }
217
+ *
218
+ * const helper = createColumnHelper<Person>();
219
+ * export const nameColumn = helper.accessor("name", {
220
+ * header: (context) => <DataTableColumnHeader column={context.column} title="Name" />,
221
+ * });
222
+ * @see {@link DataTable}
223
+ * @since 1.0.0
224
+ */
225
+ export function DataTableColumnHeader<TData>(
226
+ props: DataTableColumnHeaderProps<TData>,
227
+ ): JSX.Element {
228
+ const column = props.column;
229
+ return (
230
+ <Button
231
+ variant="ghost"
232
+ size="sm"
233
+ disabled={!column.getCanSort()}
234
+ onClick={() => column.toggleSorting()}
235
+ >
236
+ {props.title ?? column.id}
237
+ {() => (column.getIsSorted() === "desc" ? " ↓" : column.getIsSorted() === "asc" ? " ↑" : "")}
238
+ </Button>
239
+ );
240
+ }
241
+
242
+ /**
243
+ * The words of {@link DataTablePagination}. Each one defaults to English; an app in another language
244
+ * passes its own.
245
+ * @see {@link DataTablePagination}
246
+ * @since 1.0.0
247
+ */
248
+ export interface DataTablePaginationLabels {
249
+ /**
250
+ * The text of the previous-page button.
251
+ * @default "Previous"
252
+ */
253
+ readonly previous?: string | undefined;
254
+ /**
255
+ * The text of the next-page button.
256
+ * @default "Next"
257
+ */
258
+ readonly next?: string | undefined;
259
+ /**
260
+ * The page status, given the current page (from 1) and the page count.
261
+ * @default `Page ${page} of ${pages}`
262
+ */
263
+ readonly page?: ((page: number, pages: number) => string) | undefined;
264
+ /**
265
+ * The status shown when the table has no pages.
266
+ * @default "No pages"
267
+ */
268
+ readonly noPages?: string | undefined;
269
+ }
270
+
271
+ /** The words of the footer, each one set (the defaults filled in). @internal */
272
+ type PaginationWords = {
273
+ readonly previous: string;
274
+ readonly next: string;
275
+ readonly page: (page: number, pages: number) => string;
276
+ readonly noPages: string;
277
+ };
278
+
279
+ /**
280
+ * Props of {@link DataTablePagination}: the table to page through and the footer's words.
281
+ * @see {@link DataTablePagination}
282
+ * @since 1.0.0
283
+ */
284
+ export interface DataTablePaginationProps<TData> {
285
+ /** The table from `createTable` in `@shadow-js/table`, whose page state the footer changes. @default none (required) */
286
+ readonly table: Table<TData>;
287
+ /** The words of the footer, for a language other than English. @default none */
288
+ readonly labels?: DataTablePaginationLabels | undefined;
289
+ }
290
+
291
+ /**
292
+ * The footer of a {@link DataTable}: the page status, and Previous and Next buttons. Previous is
293
+ * disabled on the first page and Next on the last one. The status reads "No pages" when there is
294
+ * nothing to page through.
295
+ *
296
+ * Keyboard and ARIA: the buttons are native buttons, so Enter and Space activate them, and a disabled
297
+ * one is skipped. The page status is plain text.
298
+ *
299
+ * @param props The footer props: see {@link DataTablePaginationProps}.
300
+ * @returns The footer element.
301
+ * @example
302
+ * import { createColumnHelper, createTable } from "@shadow-js/table";
303
+ * import { DataTablePagination } from "@/components/ui/data-table";
304
+ *
305
+ * interface Person {
306
+ * id: string;
307
+ * name: string;
308
+ * }
309
+ *
310
+ * const helper = createColumnHelper<Person>();
311
+ *
312
+ * export function Footer() {
313
+ * const table = createTable({
314
+ * data: [{ id: "1", name: "Ada" }],
315
+ * getRowId: (row) => row.id,
316
+ * columns: [helper.accessor("name", { header: "Name" })],
317
+ * });
318
+ * return <DataTablePagination table={table} labels={{ previous: "Zurück", next: "Weiter" }} />;
319
+ * }
320
+ * @see {@link DataTable}
321
+ * @since 1.0.0
322
+ */
323
+ export function DataTablePagination<TData>(props: DataTablePaginationProps<TData>): JSX.Element {
324
+ const table = props.table;
325
+ const t = useUiStrings();
326
+ const label = (): PaginationWords => ({
327
+ previous: props.labels?.previous ?? t("dataTable.previous", "Previous"),
328
+ next: props.labels?.next ?? t("dataTable.next", "Next"),
329
+ page:
330
+ props.labels?.page ??
331
+ ((page: number, pages: number) =>
332
+ t("dataTable.page", "Page {page} of {pages}", { page, pages })),
333
+ noPages: props.labels?.noPages ?? t("dataTable.noPages", "No pages"),
334
+ });
335
+ return (
336
+ <div data-slot="data-table-pagination" class="flex items-center justify-end gap-2 py-2">
337
+ <span class="text-sm text-muted-foreground">
338
+ {() => {
339
+ const count = table.getPageCount();
340
+ const page = table.getState().pagination.pageIndex + 1;
341
+ return count > 0 ? label().page(page, count) : label().noPages;
342
+ }}
343
+ </span>
344
+ <Button
345
+ variant="outline"
346
+ size="sm"
347
+ disabled={!table.getCanPreviousPage()}
348
+ onClick={() => table.previousPage()}
349
+ >
350
+ {() => label().previous}
351
+ </Button>
352
+ <Button
353
+ variant="outline"
354
+ size="sm"
355
+ disabled={!table.getCanNextPage()}
356
+ onClick={() => table.nextPage()}
357
+ >
358
+ {() => label().next}
359
+ </Button>
360
+ </div>
361
+ );
362
+ }
363
+
364
+ /**
365
+ * Props of {@link DataTableViewOptions}.
366
+ * @see {@link DataTableViewOptions}
367
+ * @since 1.0.0
368
+ */
369
+ export interface DataTableViewOptionsProps<TData> {
370
+ /** Table with the `visibility` feature installed. @default none (required) */
371
+ readonly table: Table<TData>;
372
+ /** Accessible name and text of the menu trigger. @default "View" */
373
+ readonly label?: string | undefined;
374
+ /** Label for each column, called untracked. @default the column id */
375
+ readonly columnLabel?: ((column: Column<TData>) => JsxChild) | undefined;
376
+ /** Trigger classes, merged with the button's defaults. @default none */
377
+ readonly class?: string | undefined;
378
+ /** Reading direction of the portaled menu. @default inherited direction */
379
+ readonly dir?: Direction | undefined;
380
+ }
381
+
382
+ /**
383
+ * A checkbox menu for showing and hiding leaf columns. Columns with `enableHiding: false` are
384
+ * excluded. Checked state follows the table, including changes made outside the menu.
385
+ *
386
+ * Keyboard and ARIA: a named menu button opens with Enter, Space or ArrowDown. The menu uses
387
+ * `menuitemcheckbox` roles; arrow keys move focus, Enter or Space toggles, and Escape closes and
388
+ * returns focus. Supply `label` and `columnLabel` for the app's language.
389
+ *
390
+ * @param props The table, labels, trigger classes and reading direction.
391
+ * @returns The column visibility menu.
392
+ * @example
393
+ * import { createColumnHelper, createTable } from "@shadow-js/table";
394
+ * import { visibility } from "@shadow-js/table/visibility";
395
+ * import { DataTableViewOptions } from "@/components/ui/data-table";
396
+ *
397
+ * export function Columns() {
398
+ * const helper = createColumnHelper<{ name: string }>();
399
+ * const table = createTable({ data: [], columns: [helper.accessor("name")], features: [visibility] });
400
+ * return <DataTableViewOptions table={table} />;
401
+ * }
402
+ * @see {@link DataTable}
403
+ * @since 1.0.0
404
+ */
405
+ export function DataTableViewOptions<TData>(props: DataTableViewOptionsProps<TData>): JSX.Element {
406
+ const t = useUiStrings();
407
+ const columns = () => props.table.getAllLeafColumns().filter((column) => column.getCanHide());
408
+ return (
409
+ <DropdownMenu dir={props.dir}>
410
+ <DropdownMenuTrigger
411
+ as={Button}
412
+ data-slot="data-table-view-options"
413
+ variant="outline"
414
+ size="sm"
415
+ class={props.class}
416
+ disabled={columns().length === 0}
417
+ >
418
+ {props.label ?? t("dataTable.view", "View")}
419
+ </DropdownMenuTrigger>
420
+ <DropdownMenuContent align="end">
421
+ <For each={columns()}>
422
+ {(column) => (
423
+ <DropdownMenuCheckboxItem
424
+ checked={column.getIsVisible()}
425
+ onCheckedChange={(checked) => column.toggleVisibility(checked)}
426
+ >
427
+ {() => {
428
+ const label = props.columnLabel;
429
+ return label === undefined ? column.id : untrack(() => label(column));
430
+ }}
431
+ </DropdownMenuCheckboxItem>
432
+ )}
433
+ </For>
434
+ </DropdownMenuContent>
435
+ </DropdownMenu>
436
+ );
437
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Data table",
3
+ "description": "A data table over @shadow-js/table: sortable headers, pagination, column visibility and selection state."
4
+ }