@cl-light-ds/ui 0.1.0 → 0.3.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 (148) hide show
  1. package/package.json +1 -1
  2. package/src/components/accordion/CHANGELOG.md +41 -41
  3. package/src/components/accordion/index.ts +4 -4
  4. package/src/components/alert/CHANGELOG.md +5 -5
  5. package/src/components/alert/index.ts +4 -4
  6. package/src/components/avatar/CHANGELOG.md +44 -44
  7. package/src/components/avatar/index.ts +6 -6
  8. package/src/components/badge/CHANGELOG.md +49 -49
  9. package/src/components/badge/index.ts +2 -2
  10. package/src/components/breadcrumb/CHANGELOG.md +24 -24
  11. package/src/components/breadcrumb/index.ts +5 -5
  12. package/src/components/button/CHANGELOG.md +41 -41
  13. package/src/components/button/index.ts +2 -2
  14. package/src/components/button-group/CHANGELOG.md +34 -34
  15. package/src/components/button-group/index.ts +3 -3
  16. package/src/components/card/CHANGELOG.md +162 -119
  17. package/src/components/card/Card.astro +23 -8
  18. package/src/components/card/CardContent.astro +1 -1
  19. package/src/components/card/CardDescription.astro +1 -1
  20. package/src/components/card/CardMedia.astro +1 -1
  21. package/src/components/card/CardMediaActions.astro +21 -0
  22. package/src/components/card/CardMediaSelect.astro +21 -0
  23. package/src/components/card/index.ts +12 -10
  24. package/src/components/carousel/CHANGELOG.md +5 -5
  25. package/src/components/carousel/index.ts +3 -3
  26. package/src/components/checkbox/CHANGELOG.md +58 -58
  27. package/src/components/checkbox/Checkbox.astro +2 -2
  28. package/src/components/checkbox/index.ts +1 -1
  29. package/src/components/collapsible/CHANGELOG.md +5 -5
  30. package/src/components/collapsible/index.ts +4 -4
  31. package/src/components/combobox/CHANGELOG.md +27 -27
  32. package/src/components/combobox/index.ts +11 -11
  33. package/src/components/date/CHANGELOG.md +18 -18
  34. package/src/components/date/index.ts +2 -2
  35. package/src/components/dialog/CHANGELOG.md +39 -39
  36. package/src/components/dialog/index.ts +9 -9
  37. package/src/components/dropdown-menu/CHANGELOG.md +194 -194
  38. package/src/components/dropdown-menu/DropdownMenuItemContent.astro +79 -79
  39. package/src/components/dropdown-menu/DropdownMenuPortal.astro +13 -13
  40. package/src/components/dropdown-menu/DropdownMenuRadioGroup.astro +13 -13
  41. package/src/components/dropdown-menu/dropdown-menu-item-variants.ts +24 -24
  42. package/src/components/dropdown-menu/index.ts +12 -12
  43. package/src/components/field/CHANGELOG.md +33 -33
  44. package/src/components/field/index.ts +10 -10
  45. package/src/components/hover-card/CHANGELOG.md +20 -20
  46. package/src/components/hover-card/index.ts +3 -3
  47. package/src/components/input/CHANGELOG.md +29 -29
  48. package/src/components/input/index.ts +2 -2
  49. package/src/components/input-group/CHANGELOG.md +49 -49
  50. package/src/components/input-group/controlClasses.ts +2 -2
  51. package/src/components/input-group/index.ts +6 -6
  52. package/src/components/item/CHANGELOG.md +27 -27
  53. package/src/components/item/index.ts +10 -10
  54. package/src/components/kbd/CHANGELOG.md +5 -5
  55. package/src/components/kbd/index.ts +2 -2
  56. package/src/components/label/CHANGELOG.md +28 -28
  57. package/src/components/label/index.ts +2 -2
  58. package/src/components/link-group/CHANGELOG.md +75 -75
  59. package/src/components/link-group/index.ts +5 -5
  60. package/src/components/link-group/link-group-list-variants.ts +15 -15
  61. package/src/components/marquee/CHANGELOG.md +5 -5
  62. package/src/components/marquee/index.ts +2 -2
  63. package/src/components/native-select/CHANGELOG.md +5 -5
  64. package/src/components/native-select/NativeSelectOption.astro +9 -9
  65. package/src/components/native-select/index.ts +3 -3
  66. package/src/components/navigation-menu/CHANGELOG.md +63 -63
  67. package/src/components/navigation-menu/index.ts +10 -10
  68. package/src/components/organisms/lightning-sidebar-nav/CHANGELOG.md +50 -0
  69. package/src/components/organisms/lightning-sidebar-nav/LightningSidebarNav.astro +176 -0
  70. package/src/components/organisms/lightning-sidebar-nav/SidebarNav.astro +50 -0
  71. package/src/components/organisms/lightning-sidebar-nav/SidebarNavGroup.astro +74 -0
  72. package/src/components/organisms/lightning-sidebar-nav/SidebarNavItem.astro +94 -0
  73. package/src/components/organisms/lightning-sidebar-nav/SidebarNavProBadge.astro +26 -0
  74. package/src/components/organisms/lightning-sidebar-nav/SidebarNavSection.astro +71 -0
  75. package/src/components/organisms/lightning-sidebar-nav/SidebarNavSkeleton.astro +67 -0
  76. package/src/components/organisms/lightning-sidebar-nav/SidebarNavSubItem.astro +69 -0
  77. package/src/components/organisms/lightning-sidebar-nav/SidebarNavUsage.astro +78 -0
  78. package/src/components/organisms/lightning-sidebar-nav/SidebarNavWorkspaceAvatar.astro +46 -0
  79. package/src/components/organisms/lightning-sidebar-nav/SidebarNavWorkspaceSwitcher.astro +130 -0
  80. package/src/components/organisms/lightning-sidebar-nav/icons.ts +46 -0
  81. package/src/components/organisms/lightning-sidebar-nav/index.ts +18 -0
  82. package/src/components/organisms/lightning-sidebar-nav/types.ts +77 -0
  83. package/src/components/popover/CHANGELOG.md +20 -5
  84. package/src/components/popover/Popover.astro +1 -0
  85. package/src/components/popover/PopoverContent.astro +6 -5
  86. package/src/components/popover/PopoverHeader.astro +1 -1
  87. package/src/components/popover/PopoverTrigger.astro +7 -1
  88. package/src/components/popover/index.ts +7 -7
  89. package/src/components/radio-group/CHANGELOG.md +31 -9
  90. package/src/components/radio-group/RadioGroup.astro +1 -1
  91. package/src/components/radio-group/RadioGroupItem.astro +12 -3
  92. package/src/components/radio-group/index.ts +3 -3
  93. package/src/components/select/CHANGELOG.md +39 -39
  94. package/src/components/select/index.ts +8 -8
  95. package/src/components/separator/CHANGELOG.md +5 -5
  96. package/src/components/separator/index.ts +2 -2
  97. package/src/components/skeleton/CHANGELOG.md +5 -5
  98. package/src/components/skeleton/index.ts +1 -1
  99. package/src/components/slider/CHANGELOG.md +5 -5
  100. package/src/components/slider/index.ts +1 -1
  101. package/src/components/spinner/CHANGELOG.md +18 -5
  102. package/src/components/spinner/Spinner.astro +3 -2
  103. package/src/components/spinner/index.ts +1 -1
  104. package/src/components/sticky-surface/CHANGELOG.md +5 -5
  105. package/src/components/sticky-surface/index.ts +2 -2
  106. package/src/components/switch/CHANGELOG.md +29 -9
  107. package/src/components/switch/Switch.astro +4 -4
  108. package/src/components/switch/index.ts +1 -1
  109. package/src/components/table/CHANGELOG.md +24 -5
  110. package/src/components/table/Table.astro +120 -12
  111. package/src/components/table/TableBody.astro +1 -2
  112. package/src/components/table/TableBulkToolbar.astro +37 -0
  113. package/src/components/table/TableCell.astro +25 -1
  114. package/src/components/table/TableCellText.astro +17 -0
  115. package/src/components/table/TableColumnMenu.astro +50 -0
  116. package/src/components/table/TableEditableCell.astro +287 -0
  117. package/src/components/table/TableEmpty.astro +17 -0
  118. package/src/components/table/TableFolderCell.astro +38 -0
  119. package/src/components/table/TableFooter.astro +1 -1
  120. package/src/components/table/TableHead.astro +71 -2
  121. package/src/components/table/TableHeader.astro +1 -1
  122. package/src/components/table/TablePagination.astro +57 -0
  123. package/src/components/table/TableRow.astro +32 -1
  124. package/src/components/table/TableSelectAllBanner.astro +34 -0
  125. package/src/components/table/TableSelectCell.astro +50 -0
  126. package/src/components/table/TableSkeletonRow.astro +21 -0
  127. package/src/components/table/index.ts +19 -9
  128. package/src/components/table/table-cell-variants.ts +29 -0
  129. package/src/components/tabs/CHANGELOG.md +5 -5
  130. package/src/components/tabs/index.ts +6 -6
  131. package/src/components/textarea/CHANGELOG.md +22 -5
  132. package/src/components/textarea/Textarea.astro +25 -3
  133. package/src/components/textarea/index.ts +2 -2
  134. package/src/components/toggle/CHANGELOG.md +23 -5
  135. package/src/components/toggle/Toggle.astro +13 -1
  136. package/src/components/toggle/index.ts +1 -1
  137. package/src/components/toggle-group/CHANGELOG.md +18 -5
  138. package/src/components/toggle-group/ToggleGroup.astro +14 -2
  139. package/src/components/toggle-group/index.ts +2 -2
  140. package/src/components/tooltip/CHANGELOG.md +22 -5
  141. package/src/components/tooltip/Tooltip.astro +26 -0
  142. package/src/components/tooltip/TooltipContent.astro +34 -9
  143. package/src/components/tooltip/index.ts +3 -3
  144. package/src/lib/table-controller.ts +1090 -0
  145. package/src/lib/toggle-group-controller.ts +113 -113
  146. package/src/lib/toggle-shared.ts +27 -26
  147. package/src/lib/utils.ts +6 -6
  148. package/src/styles/globals.css +197 -177
@@ -0,0 +1,50 @@
1
+ ---
2
+ import { Columns3 } from "@lucide/astro";
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuCheckboxItem,
6
+ DropdownMenuContent,
7
+ DropdownMenuLabel,
8
+ DropdownMenuTrigger,
9
+ } from "../dropdown-menu";
10
+
11
+ interface ColumnOption {
12
+ column: string;
13
+ label: string;
14
+ /** Initially hidden. */
15
+ hidden?: boolean;
16
+ /** Always visible (e.g. the name column). */
17
+ disabled?: boolean;
18
+ }
19
+
20
+ interface Props {
21
+ columns: ColumnOption[];
22
+ label?: string;
23
+ class?: string;
24
+ }
25
+
26
+ const { columns, label = "Columns", class: className = "" } = Astro.props as Props;
27
+ ---
28
+
29
+ <DropdownMenu
30
+ data-table-column-menu
31
+ data-columns={JSON.stringify(columns.map((c) => c.column))}
32
+ closeOnSelect={false}
33
+ defaultValues={columns.filter((c) => !c.hidden).map((c) => c.column)}
34
+ class={className}
35
+ >
36
+ <DropdownMenuTrigger variant="outline" size="sm">
37
+ <Columns3 aria-hidden="true" />
38
+ {label}
39
+ </DropdownMenuTrigger>
40
+ <DropdownMenuContent align="end" class="w-auto min-w-44">
41
+ <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
42
+ {
43
+ columns.map((c) => (
44
+ <DropdownMenuCheckboxItem value={c.column} defaultChecked={!c.hidden} disabled={c.disabled}>
45
+ {c.label}
46
+ </DropdownMenuCheckboxItem>
47
+ ))
48
+ }
49
+ </DropdownMenuContent>
50
+ </DropdownMenu>
@@ -0,0 +1,287 @@
1
+ ---
2
+ import { Check, ChevronDown, Pencil, X } from "@lucide/astro";
3
+ import { cn } from "@cl-light-ds/ui/lib/utils";
4
+ import type { HTMLAttributes } from "astro/types";
5
+ import { Badge } from "../badge";
6
+ import { Button } from "../button";
7
+ import {
8
+ Combobox,
9
+ ComboboxContent,
10
+ ComboboxEmpty,
11
+ ComboboxInput,
12
+ ComboboxItem,
13
+ ComboboxList,
14
+ } from "../combobox";
15
+ import {
16
+ DropdownMenu,
17
+ DropdownMenuCheckboxItem,
18
+ DropdownMenuContent,
19
+ DropdownMenuRadioGroup,
20
+ DropdownMenuRadioItem,
21
+ DropdownMenuTrigger,
22
+ } from "../dropdown-menu";
23
+ import { Input } from "../input";
24
+ import { Spinner } from "../spinner";
25
+ import { cellAlign, cellBase, levelIndentStyle, type CellAlign } from "./table-cell-variants";
26
+
27
+ type BadgeVariant =
28
+ | "default"
29
+ | "secondary"
30
+ | "destructive"
31
+ | "outline"
32
+ | "success"
33
+ | "warning"
34
+ | "accent"
35
+ | "ghost";
36
+
37
+ export interface TableCellOption {
38
+ value: string;
39
+ label: string;
40
+ /** Badge variant used to render this option in select/multiselect cells. */
41
+ variant?: BadgeVariant;
42
+ }
43
+
44
+ interface Props extends Omit<HTMLAttributes<"td">, "value"> {
45
+ class?: string;
46
+ /** Column key reported in `table:edit`. */
47
+ column: string;
48
+ /** Current value. Use a string array for `multiselect`. */
49
+ value?: string | string[];
50
+ /**
51
+ * - `text`: click to edit, confirm with ✓ / Enter, cancel with ✕ / Esc.
52
+ * - `select`: badge dropdown, commits on pick.
53
+ * - `combobox`: searchable single select, commits on pick.
54
+ * - `multiselect`: badge tags with checkboxes, commits when the menu closes.
55
+ */
56
+ editor?: "text" | "select" | "combobox" | "multiselect";
57
+ options?: TableCellOption[];
58
+ /** Accessible column name, e.g. "Status". Defaults to `column`. */
59
+ label?: string;
60
+ /** Secondary line under a text value (not editable). */
61
+ subtitle?: string;
62
+ placeholder?: string;
63
+ /** Renders the value without edit affordances. */
64
+ readOnly?: boolean;
65
+ align?: CellAlign;
66
+ numeric?: boolean;
67
+ level?: 1 | 2 | 3;
68
+ /** Badges shown before collapsing the rest into "+N" (multiselect). */
69
+ maxVisible?: number;
70
+ /** Native validation for the text editor. */
71
+ required?: boolean;
72
+ pattern?: string;
73
+ minLength?: number;
74
+ maxLength?: number;
75
+ inputType?: "text" | "number" | "email" | "url";
76
+ }
77
+
78
+ const {
79
+ class: className = "",
80
+ column,
81
+ value = "",
82
+ editor = "text",
83
+ options = [],
84
+ label = column,
85
+ subtitle,
86
+ placeholder = "Empty",
87
+ readOnly = false,
88
+ numeric = false,
89
+ align = numeric ? "right" : "left",
90
+ level,
91
+ maxVisible = 2,
92
+ required = false,
93
+ pattern,
94
+ minLength,
95
+ maxLength,
96
+ inputType = "text",
97
+ ...rest
98
+ } = Astro.props as Props;
99
+
100
+ const values = Array.isArray(value) ? value : value ? [value] : [];
101
+ const optionFor = (v: string): TableCellOption =>
102
+ options.find((o) => o.value === v) ?? { value: v, label: v };
103
+ const selected = values.map(optionFor);
104
+ const visible = selected.slice(0, maxVisible);
105
+ const overflow = selected.length - visible.length;
106
+ const textValue = editor === "text" ? String(values[0] ?? "") : selected.map((o) => o.label).join(", ");
107
+ const usesBadges = editor === "select" || editor === "multiselect";
108
+ const dataValue = editor === "multiselect" ? JSON.stringify(values) : String(values[0] ?? "");
109
+
110
+ const affordance =
111
+ "group/editable -mx-2 flex min-h-8 max-w-full items-center gap-1.5 rounded-md px-2 text-left outline-none transition-[background-color,box-shadow] hover:bg-background hover:ring-1 hover:ring-border focus-visible:ring-2 focus-visible:ring-ring";
112
+ const affordanceIcon =
113
+ "size-3.5 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover/editable:opacity-100 group-focus-visible/editable:opacity-100 group-focus/cell:opacity-100";
114
+ ---
115
+
116
+ <td
117
+ data-slot="table-cell"
118
+ data-column={column}
119
+ data-editable={readOnly ? undefined : ""}
120
+ data-readonly={readOnly ? "" : undefined}
121
+ data-editor={editor}
122
+ data-value={dataValue}
123
+ data-label={label}
124
+ data-max-visible={maxVisible}
125
+ data-placeholder={placeholder}
126
+ data-state="idle"
127
+ style={levelIndentStyle(level)}
128
+ class={cn(
129
+ cellBase,
130
+ cellAlign({ align }),
131
+ "group/cell h-12 py-2 font-medium",
132
+ numeric && "font-semibold tabular-nums",
133
+ className,
134
+ )}
135
+ {...rest}
136
+ >
137
+ {
138
+ readOnly ? (
139
+ usesBadges ? (
140
+ <span class="inline-flex items-center gap-1">
141
+ {visible.map((o) => <Badge size="sm" variant={o.variant ?? "secondary"}>{o.label}</Badge>)}
142
+ {overflow > 0 && <Badge size="sm" variant="outline">+{overflow}</Badge>}
143
+ </span>
144
+ ) : (
145
+ <span class="block truncate">{textValue}</span>
146
+ )
147
+ ) : editor === "text" ? (
148
+ <>
149
+ <div
150
+ data-slot="table-cell-display"
151
+ role="button"
152
+ tabindex="-1"
153
+ aria-label={`Edit ${label}`}
154
+ class={cn(affordance, "cursor-text group-data-[state=editing]/cell:hidden group-data-[state=saving]/cell:hidden group-data-[state=error]/cell:hidden", align === "right" && "justify-end")}
155
+ >
156
+ <div class="min-w-0 flex-1">
157
+ <span data-slot="table-cell-value" class={cn("block truncate", !textValue && "text-muted-foreground font-normal")}>
158
+ {textValue || placeholder}
159
+ </span>
160
+ {subtitle && <span class="block truncate text-xs leading-4 font-normal text-muted-foreground">{subtitle}</span>}
161
+ </div>
162
+ <Pencil class={affordanceIcon} aria-hidden="true" />
163
+ </div>
164
+ <div
165
+ data-slot="table-cell-editor"
166
+ class="-mx-2 hidden items-center gap-1 group-data-[state=editing]/cell:flex group-data-[state=saving]/cell:flex group-data-[state=error]/cell:flex"
167
+ >
168
+ <Input
169
+ data-table-input
170
+ size="sm"
171
+ type={inputType}
172
+ value={textValue}
173
+ aria-label={label}
174
+ required={required}
175
+ pattern={pattern}
176
+ minlength={minLength}
177
+ maxlength={maxLength}
178
+ class={cn("min-w-24 flex-1", align === "right" && "text-right")}
179
+ />
180
+ <Button data-table-action="confirm" variant="ghost" size="icon-sm" aria-label="Save" class="text-primary">
181
+ <Check class="group-data-[state=saving]/cell:hidden" aria-hidden="true" />
182
+ <Spinner class="hidden group-data-[state=saving]/cell:block" aria-label="Saving" />
183
+ </Button>
184
+ <Button data-table-action="cancel" variant="ghost" size="icon-sm" aria-label="Cancel">
185
+ <X aria-hidden="true" />
186
+ </Button>
187
+ </div>
188
+ </>
189
+ ) : editor === "combobox" ? (
190
+ <>
191
+ <button
192
+ type="button"
193
+ data-slot="table-cell-display"
194
+ tabindex="-1"
195
+ aria-label={`Edit ${label}`}
196
+ class={cn(affordance, "cursor-pointer group-data-[state=editing]/cell:hidden")}
197
+ >
198
+ <span data-slot="table-cell-value" class={cn("truncate", !textValue && "text-muted-foreground font-normal")}>
199
+ {textValue || placeholder}
200
+ </span>
201
+ <ChevronDown class={affordanceIcon} aria-hidden="true" />
202
+ </button>
203
+ <div data-slot="table-cell-editor" class="-mx-2 hidden group-data-[state=editing]/cell:block">
204
+ <Combobox data-table-combobox defaultValue={values[0]} autoHighlight>
205
+ <ComboboxInput size="sm" placeholder={`Search ${label.toLowerCase()}…`} aria-label={label} class="w-full min-w-40" />
206
+ <ComboboxContent align="start">
207
+ <ComboboxList>
208
+ {options.map((o) => (
209
+ <ComboboxItem value={o.value} label={o.label}>{o.label}</ComboboxItem>
210
+ ))}
211
+ </ComboboxList>
212
+ <ComboboxEmpty>No results</ComboboxEmpty>
213
+ </ComboboxContent>
214
+ </Combobox>
215
+ </div>
216
+ </>
217
+ ) : (
218
+ <DropdownMenu
219
+ data-table-menu
220
+ defaultValue={editor === "select" ? values[0] ?? null : undefined}
221
+ defaultValues={editor === "multiselect" ? values : undefined}
222
+ closeOnSelect={editor === "select"}
223
+ class="-mx-2 max-w-[calc(100%+1rem)]"
224
+ >
225
+ <DropdownMenuTrigger asChild>
226
+ <button
227
+ type="button"
228
+ data-slot="table-cell-display"
229
+ tabindex="-1"
230
+ aria-label={`Edit ${label}`}
231
+ class={cn(affordance, "mx-0 cursor-pointer group-data-[state=saving]/cell:opacity-60")}
232
+ >
233
+ <span data-slot="table-cell-value" class="inline-flex min-w-0 items-center gap-1">
234
+ {visible.map((o) => <Badge size="sm" variant={o.variant ?? "secondary"}>{o.label}</Badge>)}
235
+ {overflow > 0 && <Badge size="sm" variant="outline">+{overflow}</Badge>}
236
+ {selected.length === 0 && <span class="text-muted-foreground font-normal">{placeholder}</span>}
237
+ </span>
238
+ <ChevronDown class={affordanceIcon} aria-hidden="true" />
239
+ </button>
240
+ </DropdownMenuTrigger>
241
+ <DropdownMenuContent class="w-auto min-w-44">
242
+ {
243
+ editor === "select" ? (
244
+ <DropdownMenuRadioGroup>
245
+ {options.map((o) => (
246
+ <DropdownMenuRadioItem value={o.value}>
247
+ <Badge size="sm" variant={o.variant ?? "secondary"}>{o.label}</Badge>
248
+ </DropdownMenuRadioItem>
249
+ ))}
250
+ </DropdownMenuRadioGroup>
251
+ ) : (
252
+ options.map((o) => (
253
+ <DropdownMenuCheckboxItem value={o.value} defaultChecked={values.includes(o.value)}>
254
+ <Badge size="sm" variant={o.variant ?? "secondary"}>{o.label}</Badge>
255
+ </DropdownMenuCheckboxItem>
256
+ ))
257
+ )
258
+ }
259
+ </DropdownMenuContent>
260
+ </DropdownMenu>
261
+ )
262
+ }
263
+ {
264
+ !readOnly && editor !== "text" && (
265
+ <template data-slot="table-cell-templates">
266
+ {options.map((o) =>
267
+ usesBadges ? (
268
+ <Badge data-option={o.value} size="sm" variant={o.variant ?? "secondary"}>{o.label}</Badge>
269
+ ) : (
270
+ <span data-option={o.value}>{o.label}</span>
271
+ ),
272
+ )}
273
+ {usesBadges && <Badge data-option-overflow size="sm" variant="outline">+0</Badge>}
274
+ </template>
275
+ )
276
+ }
277
+ {
278
+ !readOnly && (
279
+ <div
280
+ data-slot="table-cell-error"
281
+ role="alert"
282
+ hidden
283
+ class="fixed z-50 max-w-64 rounded-md bg-destructive px-2 py-1 text-xs font-medium whitespace-normal text-destructive-foreground shadow-md"
284
+ />
285
+ )
286
+ }
287
+ </td>
@@ -0,0 +1,17 @@
1
+ ---
2
+ import { cn } from "@cl-light-ds/ui/lib/utils";
3
+
4
+ interface Props {
5
+ /** Number of columns to span. */
6
+ colspan: number;
7
+ class?: string;
8
+ }
9
+
10
+ const { colspan, class: className = "" } = Astro.props as Props;
11
+ ---
12
+
13
+ <tr data-slot="table-empty" class="bg-background">
14
+ <td colspan={colspan} class={cn("h-40 border-b border-border px-6 text-center text-sm text-muted-foreground", className)}>
15
+ <slot>No results.</slot>
16
+ </td>
17
+ </tr>
@@ -0,0 +1,38 @@
1
+ ---
2
+ import { ChevronRight, Folder, FolderOpen } from "@lucide/astro";
3
+ import { cn } from "@cl-light-ds/ui/lib/utils";
4
+ import type { HTMLAttributes } from "astro/types";
5
+ import { cellBase, levelIndentStyle } from "./table-cell-variants";
6
+
7
+ interface Props extends HTMLAttributes<"td"> {
8
+ class?: string;
9
+ column?: string;
10
+ /** Nesting level (1–3). Match the parent `TableRow` `level`. */
11
+ level?: 1 | 2 | 3;
12
+ }
13
+
14
+ const { class: className = "", column, level, ...rest } = Astro.props as Props;
15
+ ---
16
+
17
+ <td
18
+ data-slot="table-cell"
19
+ data-column={column}
20
+ data-folder-cell
21
+ style={levelIndentStyle(level)}
22
+ class={cn(cellBase, "h-12 py-2 font-medium", className)}
23
+ {...rest}
24
+ >
25
+ <div class="flex items-center gap-1.5">
26
+ <button
27
+ type="button"
28
+ data-slot="table-row-toggle"
29
+ aria-label="Expand row"
30
+ class="-ml-1 inline-flex size-6 shrink-0 items-center justify-center rounded-md outline-none hover:bg-background-hover focus-visible:ring-2 focus-visible:ring-ring"
31
+ >
32
+ <ChevronRight class="size-4 transition-transform group-data-[expanded]/row:rotate-90" aria-hidden="true" />
33
+ </button>
34
+ <Folder class="size-4 shrink-0 group-data-[expanded]/row:hidden" aria-hidden="true" />
35
+ <FolderOpen class="hidden size-4 shrink-0 group-data-[expanded]/row:block" aria-hidden="true" />
36
+ <span class="truncate"><slot /></span>
37
+ </div>
38
+ </td>
@@ -14,7 +14,7 @@ const {
14
14
 
15
15
  <tfoot
16
16
  data-slot="table-footer"
17
- class={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
17
+ class={cn("font-medium [&_tr]:bg-muted [&_tr]:hover:bg-muted [&_td]:font-semibold", className)}
18
18
  {...rest}
19
19
  >
20
20
  <slot />
@@ -1,21 +1,90 @@
1
1
  ---
2
+ import { ArrowDown, ArrowUp, ArrowUpDown } from "@lucide/astro";
2
3
  import { cn } from "@cl-light-ds/ui/lib/utils";
3
4
  import type { HTMLAttributes } from "astro/types";
5
+ import { cellAlign, cellBase, type CellAlign } from "./table-cell-variants";
6
+
7
+ type SortDirection = "asc" | "desc" | "none";
4
8
 
5
9
  interface Props extends HTMLAttributes<"th"> {
6
10
  class?: string;
11
+ /** Column key. Used by sorting, resize/hide and pinned columns, and matched against `TableCell` `column`. */
12
+ column?: string;
13
+ /** Renders the header label as a sort button (cycles asc → desc → none). */
14
+ sortable?: boolean;
15
+ /** Initial sort direction for this column. */
16
+ sort?: SortDirection;
17
+ align?: CellAlign;
18
+ /** Shows a drag handle to resize the column. */
19
+ resizable?: boolean;
20
+ /** Initial column width, e.g. "240px". */
21
+ width?: string;
7
22
  }
8
23
 
9
24
  const {
10
25
  class: className = "",
26
+ column,
27
+ sortable = false,
28
+ sort = "none",
29
+ align = "left",
30
+ resizable = false,
31
+ width,
11
32
  ...rest
12
33
  } = Astro.props as Props;
34
+
35
+ const ariaSort =
36
+ sortable ? (sort === "asc" ? "ascending" : sort === "desc" ? "descending" : "none") : undefined;
13
37
  ---
14
38
 
15
39
  <th
16
40
  data-slot="table-head"
17
- class={cn("text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)}
41
+ data-column={column}
42
+ data-sortable={sortable ? "" : undefined}
43
+ data-sort={sortable ? sort : undefined}
44
+ data-align={align}
45
+ aria-sort={ariaSort}
46
+ scope="col"
47
+ style={width ? `width: ${width}; min-width: ${width}` : undefined}
48
+ class={cn(
49
+ cellBase,
50
+ cellAlign({ align }),
51
+ "group/th h-11 bg-muted text-xs font-semibold text-foreground uppercase",
52
+ "group-data-[sticky-header]/table:sticky group-data-[sticky-header]/table:top-0 group-data-[sticky-header]/table:z-10 data-[pinned]:z-20 data-[pinned]:bg-muted",
53
+ "[&:has([type=checkbox])]:w-15 [&:has([type=checkbox])]:text-center",
54
+ className,
55
+ )}
18
56
  {...rest}
19
57
  >
20
- <slot />
58
+ {
59
+ sortable ? (
60
+ <button
61
+ type="button"
62
+ data-slot="table-sort-button"
63
+ class={cn(
64
+ "-mx-1.5 inline-flex w-[calc(100%+0.75rem)] items-center justify-between gap-2 rounded-md px-1.5 py-1 uppercase",
65
+ "cursor-pointer outline-none hover:bg-background-hover focus-visible:ring-2 focus-visible:ring-ring",
66
+ "disabled:cursor-default disabled:hover:bg-transparent",
67
+ align === "right" && "flex-row-reverse",
68
+ )}
69
+ >
70
+ <span class="truncate"><slot /></span>
71
+ <span class="shrink-0 [&_svg]:size-3.5 [button:disabled_&]:hidden" aria-hidden="true">
72
+ <ArrowUpDown class="text-muted-foreground group-data-[sort=asc]/th:hidden group-data-[sort=desc]/th:hidden group-has-[button:hover]/th:text-foreground" />
73
+ <ArrowUp class="hidden text-primary group-data-[sort=asc]/th:block" />
74
+ <ArrowDown class="hidden text-primary group-data-[sort=desc]/th:block" />
75
+ </span>
76
+ </button>
77
+ ) : (
78
+ <slot />
79
+ )
80
+ }
81
+ {
82
+ resizable && (
83
+ <span
84
+ data-slot="table-resize-handle"
85
+ aria-hidden="true"
86
+ class="absolute top-0 right-0 z-[1] h-full w-1.5 cursor-col-resize touch-none select-none after:absolute after:inset-y-2 after:right-0.5 after:w-px after:bg-border hover:after:w-0.5 hover:after:bg-primary"
87
+ />
88
+ )
89
+ }
21
90
  </th>
@@ -12,6 +12,6 @@ const {
12
12
  } = Astro.props as Props;
13
13
  ---
14
14
 
15
- <thead data-slot="table-header" class={cn("[&_tr]:border-b", className)} {...rest}>
15
+ <thead data-slot="table-header" class={cn("", className)} {...rest}>
16
16
  <slot />
17
17
  </thead>
@@ -0,0 +1,57 @@
1
+ ---
2
+ import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "@lucide/astro";
3
+ import { cn } from "@cl-light-ds/ui/lib/utils";
4
+ import type { HTMLAttributes } from "astro/types";
5
+ import { Button } from "../button";
6
+ import { ButtonGroup } from "../button-group";
7
+ import { NativeSelect, NativeSelectOption } from "../native-select";
8
+
9
+ interface Props extends HTMLAttributes<"nav"> {
10
+ class?: string;
11
+ /** Current page, 1-based. */
12
+ page?: number;
13
+ pageSize?: number;
14
+ totalCount: number;
15
+ pageSizes?: number[];
16
+ }
17
+
18
+ const {
19
+ class: className = "",
20
+ page = 1,
21
+ pageSize = 10,
22
+ totalCount,
23
+ pageSizes = [10, 25, 50],
24
+ ...rest
25
+ } = Astro.props as Props;
26
+
27
+ const pageCount = Math.max(1, Math.ceil(totalCount / pageSize));
28
+ const from = totalCount === 0 ? 0 : (page - 1) * pageSize + 1;
29
+ const to = Math.min(page * pageSize, totalCount);
30
+ ---
31
+
32
+ <nav
33
+ data-slot="table-pagination"
34
+ aria-label="Pagination"
35
+ data-page={page}
36
+ data-page-size={pageSize}
37
+ data-total-count={totalCount}
38
+ class={cn("flex flex-wrap items-center justify-between gap-4 px-6 py-3 text-sm text-muted-foreground", className)}
39
+ {...rest}
40
+ >
41
+ <p data-slot="table-pagination-summary">Showing {from}–{to} of {totalCount}</p>
42
+ <div class="flex flex-wrap items-center gap-4">
43
+ <label class="flex items-center gap-2">
44
+ <span>Rows per page</span>
45
+ <NativeSelect size="sm" data-table-page-size aria-label="Rows per page">
46
+ {pageSizes.map((s) => <NativeSelectOption value={String(s)} selected={s === pageSize}>{s}</NativeSelectOption>)}
47
+ </NativeSelect>
48
+ </label>
49
+ <span data-slot="table-pagination-status" class="text-foreground">Page {page} of {pageCount}</span>
50
+ <ButtonGroup>
51
+ <Button variant="outline" size="icon-sm" data-table-page="first" aria-label="First page" disabled={page <= 1}><ChevronsLeft /></Button>
52
+ <Button variant="outline" size="icon-sm" data-table-page="prev" aria-label="Previous page" disabled={page <= 1}><ChevronLeft /></Button>
53
+ <Button variant="outline" size="icon-sm" data-table-page="next" aria-label="Next page" disabled={page >= pageCount}><ChevronRight /></Button>
54
+ <Button variant="outline" size="icon-sm" data-table-page="last" aria-label="Last page" disabled={page >= pageCount}><ChevronsRight /></Button>
55
+ </ButtonGroup>
56
+ </div>
57
+ </nav>
@@ -4,17 +4,48 @@ import type { HTMLAttributes } from "astro/types";
4
4
 
5
5
  interface Props extends HTMLAttributes<"tr"> {
6
6
  class?: string;
7
+ /** Stable id reported in `table:selection`, `table:edit` and `table:toggle-row` events. */
8
+ rowId?: string;
9
+ /** Row id of the parent folder row. Rows under a collapsed parent are hidden. */
10
+ parentId?: string;
11
+ /** Nesting level (1–3) for tree tables. */
12
+ level?: 1 | 2 | 3;
13
+ /** Marks a folder row that can expand/collapse its children. */
14
+ folder?: boolean;
15
+ /** Initial expanded state of a folder row. */
16
+ expanded?: boolean;
17
+ /** Initial selected state. Pair with a checked `TableSelectCell`. */
18
+ selected?: boolean;
7
19
  }
8
20
 
9
21
  const {
10
22
  class: className = "",
23
+ rowId,
24
+ parentId,
25
+ level,
26
+ folder = false,
27
+ expanded = false,
28
+ selected = false,
11
29
  ...rest
12
30
  } = Astro.props as Props;
13
31
  ---
14
32
 
15
33
  <tr
16
34
  data-slot="table-row"
17
- class={cn("hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", className)}
35
+ data-row-id={rowId}
36
+ data-parent-id={parentId}
37
+ data-level={level}
38
+ data-folder={folder ? "" : undefined}
39
+ data-expanded={folder && expanded ? "" : undefined}
40
+ data-state={selected ? "selected" : undefined}
41
+ aria-level={level}
42
+ aria-expanded={folder ? String(expanded) : undefined}
43
+ aria-selected={selected ? "true" : undefined}
44
+ class={cn(
45
+ "group/row bg-background transition-colors",
46
+ "[tbody_&]:hover:bg-table-row-hover data-[state=selected]:bg-table-row-selected [tbody_&]:data-[state=selected]:hover:bg-table-row-selected",
47
+ className,
48
+ )}
18
49
  {...rest}
19
50
  >
20
51
  <slot />
@@ -0,0 +1,34 @@
1
+ ---
2
+ import { cn } from "@cl-light-ds/ui/lib/utils";
3
+ import type { HTMLAttributes } from "astro/types";
4
+
5
+ interface Props extends HTMLAttributes<"div"> {
6
+ class?: string;
7
+ /** Plural noun for the rows, e.g. "invoices". */
8
+ noun?: string;
9
+ }
10
+
11
+ const { class: className = "", noun = "results", ...rest } = Astro.props as Props;
12
+ const linkClass =
13
+ "cursor-pointer rounded-sm font-semibold text-primary outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring";
14
+ ---
15
+
16
+ <div
17
+ data-slot="table-select-all-banner"
18
+ hidden
19
+ class={cn("items-center justify-center gap-2 border-b border-border bg-muted/60 px-4 py-2 text-sm [&:not([hidden])]:flex", className)}
20
+ {...rest}
21
+ >
22
+ <span data-banner-page>
23
+ All <span data-slot="table-page-count">0</span> rows on this page are selected.
24
+ </span>
25
+ <button type="button" data-banner-page data-table-action="select-all-results" class={linkClass}>
26
+ Select all <span data-slot="table-total-count">0</span> {noun}
27
+ </button>
28
+ <span data-banner-all hidden>
29
+ All <span data-slot="table-total-count">0</span> {noun} are selected.
30
+ </span>
31
+ <button type="button" data-banner-all hidden data-table-action="clear-selection" class={linkClass}>
32
+ Clear selection
33
+ </button>
34
+ </div>