@bunnix/components 0.9.0 → 0.9.2

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 (42) hide show
  1. package/@types/index.d.ts +134 -30
  2. package/README.md +2 -2
  3. package/package.json +1 -1
  4. package/src/components/AccordionGroup.mjs +2 -1
  5. package/src/components/Badge.mjs +18 -4
  6. package/src/components/Button.mjs +7 -9
  7. package/src/components/Card.mjs +37 -0
  8. package/src/components/Checkbox.mjs +5 -7
  9. package/src/components/CodeBlock.mjs +31 -0
  10. package/src/components/ComboBox.mjs +22 -14
  11. package/src/components/Container.mjs +8 -10
  12. package/src/components/DatePicker.mjs +13 -15
  13. package/src/components/Dialog.mjs +35 -4
  14. package/src/components/DropdownMenu.mjs +16 -14
  15. package/src/components/HStack.mjs +11 -3
  16. package/src/components/Icon.mjs +9 -5
  17. package/src/components/InputField.mjs +12 -4
  18. package/src/components/NavigationBar.mjs +55 -25
  19. package/src/components/PageHeader.mjs +11 -8
  20. package/src/components/PageSection.mjs +20 -10
  21. package/src/components/PopoverMenu.mjs +94 -50
  22. package/src/components/RadioCheckbox.mjs +5 -7
  23. package/src/components/SearchBox.mjs +12 -21
  24. package/src/components/Sidebar.mjs +142 -67
  25. package/src/components/Table.mjs +145 -96
  26. package/src/components/Text.mjs +52 -21
  27. package/src/components/TimePicker.mjs +13 -15
  28. package/src/components/ToastNotification.mjs +16 -13
  29. package/src/components/ToggleSwitch.mjs +5 -7
  30. package/src/components/VStack.mjs +7 -6
  31. package/src/index.mjs +2 -0
  32. package/src/styles/buttons.css +8 -0
  33. package/src/styles/colors.css +8 -0
  34. package/src/styles/controls.css +61 -0
  35. package/src/styles/layout.css +64 -5
  36. package/src/styles/media.css +11 -0
  37. package/src/styles/menu.css +39 -21
  38. package/src/styles/table.css +2 -2
  39. package/src/styles/typography.css +25 -0
  40. package/src/styles/variables.css +3 -0
  41. package/src/utils/iconUtils.mjs +10 -0
  42. package/src/utils/sizeUtils.mjs +87 -0
@@ -1,4 +1,5 @@
1
1
  import Bunnix from "@bunnix/core";
2
+ import { clampSize, toSizeToken } from "../utils/sizeUtils.mjs";
2
3
  const { label, input, span } = Bunnix;
3
4
 
4
5
  export default function RadioCheckbox({
@@ -10,14 +11,11 @@ export default function RadioCheckbox({
10
11
  class: className = "",
11
12
  ...inputProps
12
13
  }) {
13
- const normalizeSize = (value) => {
14
- if (!value || value === "default" || value === "regular" || value === "md") return "md";
15
- if (value === "sm") return "sm";
16
- if (value === "lg" || value === "xl") return value;
17
- return value;
18
- };
14
+ // RadioCheckbox supports all sizes
15
+ const normalizeSize = (value) => clampSize(value, ["xsmall", "small", "regular", "large", "xlarge"], "regular");
19
16
  const normalizedSize = normalizeSize(size);
20
- const sizeClass = normalizedSize === "lg" ? "checkbox-lg" : normalizedSize === "xl" ? "checkbox-xl" : "";
17
+ const sizeToken = toSizeToken(normalizedSize);
18
+ const sizeClass = sizeToken === "xl" ? "checkbox-xl" : sizeToken === "lg" ? "checkbox-lg" : "";
21
19
  const nativeChange = onChange ?? inputProps.change;
22
20
  const checkHandler = onCheck ?? check;
23
21
 
@@ -1,14 +1,8 @@
1
1
  import Bunnix, { ForEach, useEffect, useMemo, useRef, useState } from "@bunnix/core";
2
+ import { clampSize, toSizeToken } from "../utils/sizeUtils.mjs";
2
3
  import InputField from "./InputField.mjs";
3
4
  import Icon from "./Icon.mjs";
4
5
 
5
- const sizeClassMap = {
6
- sm: "",
7
- md: "",
8
- lg: "input-lg",
9
- xl: "input-xl"
10
- };
11
-
12
6
  const { div, button, span } = Bunnix;
13
7
 
14
8
  export default function SearchBox({
@@ -24,14 +18,11 @@ export default function SearchBox({
24
18
  select,
25
19
  ...rest
26
20
  } = {}) {
27
- const normalizeSize = (value) => {
28
- if (!value || value === "default" || value === "regular" || value === "md") return "md";
29
- if (value === "sm") return "sm";
30
- if (value === "lg" || value === "xl") return value;
31
- return value;
32
- };
21
+ // SearchBox supports all sizes
22
+ const normalizeSize = (value) => clampSize(value, ["xsmall", "small", "regular", "large", "xlarge"], "regular");
33
23
  const normalizedSize = normalizeSize(size);
34
- const sizeClass = sizeClassMap[normalizedSize] || "";
24
+ const sizeToken = toSizeToken(normalizedSize);
25
+ const sizeClass = sizeToken === "xl" ? "input-xl" : sizeToken === "lg" ? "input-lg" : "";
35
26
  const variantClass = variant === "rounded" ? "rounded-full" : "";
36
27
  const combinedClass = `${sizeClass} ${variantClass} ${className}`.trim();
37
28
 
@@ -122,13 +113,13 @@ export default function SearchBox({
122
113
  }
123
114
  };
124
115
 
125
- const itemSizeClass = normalizedSize === "lg" ? "btn-lg" : normalizedSize === "xl" ? "btn-xl" : "";
126
- const iconSizeValue = normalizedSize === "sm"
127
- ? "sm"
128
- : normalizedSize === "lg"
129
- ? "lg"
130
- : normalizedSize === "xl"
131
- ? "xl"
116
+ const itemSizeClass = sizeToken === "lg" ? "btn-lg" : sizeToken === "xl" ? "btn-xl" : "";
117
+ const iconSizeValue = normalizedSize === "small"
118
+ ? "small"
119
+ : normalizedSize === "large"
120
+ ? "large"
121
+ : normalizedSize === "xlarge"
122
+ ? "xlarge"
132
123
  : undefined;
133
124
  const hasResults = indexedData.map((list) => (list || []).length > 0);
134
125
 
@@ -1,21 +1,37 @@
1
1
  import Bunnix, { useMemo, useState, ForEach, Show } from "@bunnix/core";
2
2
  import SearchBox from "./SearchBox.mjs";
3
3
  import Badge from "./Badge.mjs";
4
+ import { resolveIconClass } from "../utils/iconUtils.mjs";
4
5
  const { div, a, span, h4, h6, hr } = Bunnix;
5
6
 
6
7
  export default function Sidebar({
7
- items = [],
8
+ items,
8
9
  selection,
9
- onSelect,
10
10
  onItemSelect,
11
11
  searchable = false,
12
- searchProps = {}
12
+ searchProps = {},
13
+ leading,
14
+ trailing,
15
+ class: className = "",
13
16
  } = {}) {
14
- const selected = useState(selection ?? 'home');
17
+ const selectionState =
18
+ selection &&
19
+ typeof selection.map === "function" &&
20
+ typeof selection.get === "function" &&
21
+ typeof selection.set === "function"
22
+ ? selection
23
+ : null;
24
+ const selected = selectionState ?? useState(selection ?? "home");
15
25
  const searchValue = useState("");
16
26
 
27
+ const resolveItems = (value) => {
28
+ const resolved =
29
+ value && typeof value.get === "function" ? value.get() : value;
30
+ return Array.isArray(resolved) ? resolved : [];
31
+ };
32
+
17
33
  // Initialize expanded state from items' isExpanded property
18
- const initialExpanded = items.reduce((acc, item) => {
34
+ const initialExpanded = resolveItems(items).reduce((acc, item) => {
19
35
  if (item.children && item.isExpanded) {
20
36
  acc[item.id] = true;
21
37
  }
@@ -32,7 +48,6 @@ export default function Sidebar({
32
48
  window.location.hash = target;
33
49
  }
34
50
  }
35
- if (onSelect) onSelect(id);
36
51
  if (onItemSelect) onItemSelect(id);
37
52
  };
38
53
 
@@ -48,16 +63,22 @@ export default function Sidebar({
48
63
  }
49
64
 
50
65
  if (item.isHeader) {
51
- return div({ class: "row-container px-base py-md select-none sticky-top" },
52
- h6({ class: "no-margin text-tertiary font-bold" }, item.label)
66
+ return div(
67
+ { class: "row-container px-base py-sm pt-md select-none sticky-top" },
68
+ span(
69
+ {
70
+ class:
71
+ "no-margin text-tertiary text-sm bold text-uppercase no-selectable",
72
+ },
73
+ item.label,
74
+ ),
53
75
  );
54
76
  }
55
77
 
56
78
  const hasChildren = item.children && item.children.length > 0;
57
- const isSelected = selected.map(v => v === item.id);
58
- const isExpanded = useMemo(
59
- [expanded, searchValue],
60
- (ex, query) => (String(query ?? "").trim() ? true : !!ex[item.id])
79
+ const isSelected = selected.map((v) => v === item.id);
80
+ const isExpanded = useMemo([expanded, searchValue], (ex, query) =>
81
+ String(query ?? "").trim() ? true : !!ex[item.id],
61
82
  );
62
83
 
63
84
  const handleItemClick = (e) => {
@@ -69,53 +90,95 @@ export default function Sidebar({
69
90
  };
70
91
 
71
92
  return div({ class: "column-container" }, [
72
- div({ class: `box-sm ${isChild ? "pl-md" : ""}` },
73
- div({
74
- class: isSelected.map(s => `box-control hoverable ${s ? 'selected' : ''}`),
75
- click: handleItemClick
76
- }, [
77
- div({ class: "row-container items-center gap-sm no-margin w-full" }, [
78
- span({ class: isSelected.map(s => `icon ${item.icon} ${s ? 'bg-white' : 'icon-base'}`) }),
79
- h4({ class: "no-margin text-base font-inherit" }, item.label),
80
- (item.badge || hasChildren) ? div({ class: "spacer-h" }) : null,
81
- (() => {
82
- if (!item.badge) return null;
83
- if (typeof item.badge === "string" || typeof item.badge === "number") {
84
- return Badge({ tone: "accent", size: "xs", shape: "capsule" }, String(item.badge));
85
- }
86
- const value = item.badge.value;
87
- if (value === undefined || value === null || value === "") return null;
88
- return Badge({
89
- tone: item.badge.tone || "accent",
90
- variant: item.badge.variant || "solid",
91
- size: item.badge.size || "xs",
92
- shape: "capsule"
93
- }, String(value));
94
- })(),
95
- hasChildren && span({
96
- class: isExpanded.map(ex => `icon icon-chevron-down ml-auto transition-transform ${ex ? 'rotate-180' : 'icon-base'}`)
97
- })
98
- ])
99
- ])
93
+ div(
94
+ { class: `box-sm no-selectable ${isChild ? "pl-md" : ""}` },
95
+ div(
96
+ {
97
+ class: isSelected.map(
98
+ (s) => `box-control hoverable ${s ? "selected" : ""}`,
99
+ ),
100
+ click: handleItemClick,
101
+ },
102
+ [
103
+ div(
104
+ { class: "row-container items-center gap-sm no-margin w-full" },
105
+ [
106
+ item.icon
107
+ ? (() => {
108
+ const resolvedIconClass = resolveIconClass(item.icon);
109
+ return span({
110
+ class: isSelected.map(
111
+ (s) =>
112
+ `icon ${resolvedIconClass} ${s ? "bg-white" : "icon-base"}`,
113
+ ),
114
+ });
115
+ })()
116
+ : null,
117
+ h4({ class: "no-margin text-base font-inherit" }, item.label),
118
+ item.badge || hasChildren ? div({ class: "spacer-h" }) : null,
119
+ (() => {
120
+ if (!item.badge) return null;
121
+ if (
122
+ typeof item.badge === "string" ||
123
+ typeof item.badge === "number"
124
+ ) {
125
+ return Badge(
126
+ { tone: "accent", size: "xsmall", shape: "capsule" },
127
+ String(item.badge),
128
+ );
129
+ }
130
+ const value = item.badge.value;
131
+ if (value === undefined || value === null || value === "")
132
+ return null;
133
+ return Badge(
134
+ {
135
+ tone: item.badge.tone || "accent",
136
+ variant: item.badge.variant || "solid",
137
+ size: item.badge.size || "xsmall",
138
+ shape: "capsule",
139
+ },
140
+ String(value),
141
+ );
142
+ })(),
143
+ hasChildren &&
144
+ span({
145
+ class: isExpanded.map(
146
+ (ex) =>
147
+ `icon icon-chevron-down icon-base ml-auto transition-transform ${ex ? "rotate-180" : ""}`,
148
+ ),
149
+ }),
150
+ ],
151
+ ),
152
+ ],
153
+ ),
100
154
  ),
101
- hasChildren && Show(isExpanded, div({ class: "column-container py-xs" },
102
- item.children.map(child => renderItem(child, true))
103
- ))
155
+ hasChildren &&
156
+ Show(
157
+ isExpanded,
158
+ div(
159
+ { class: "column-container py-xs" },
160
+ item.children.map((child) => renderItem(child, true)),
161
+ ),
162
+ ),
104
163
  ]);
105
164
  };
106
165
 
107
166
  const filterSidebarItems = (rawItems, query) => {
108
- if (!query) return rawItems;
167
+ const list = Array.isArray(rawItems) ? rawItems : [];
168
+ if (!query) return list;
109
169
  const normalized = query.trim().toLowerCase();
110
- if (!normalized) return rawItems;
170
+ if (!normalized) return list;
111
171
 
112
172
  const filterItem = (item) => {
113
173
  if (item.isHeader || item.isSeparator) return item;
114
174
  const label = (item.label ?? "").toLowerCase();
115
175
  const hasChildren = item.children && item.children.length > 0;
116
176
  if (hasChildren) {
117
- const filteredChildren = item.children.map(child => filterItem(child)).filter(Boolean);
118
- const matched = label.includes(normalized) || filteredChildren.length > 0;
177
+ const filteredChildren = item.children
178
+ .map((child) => filterItem(child))
179
+ .filter(Boolean);
180
+ const matched =
181
+ label.includes(normalized) || filteredChildren.length > 0;
119
182
  if (!matched) return null;
120
183
  return { ...item, children: filteredChildren };
121
184
  }
@@ -137,7 +200,7 @@ export default function Sidebar({
137
200
  currentGroup = [];
138
201
  };
139
202
 
140
- for (const item of rawItems) {
203
+ for (const item of list) {
141
204
  if (item.isHeader) {
142
205
  flush();
143
206
  currentHeader = item;
@@ -158,30 +221,42 @@ export default function Sidebar({
158
221
  return result;
159
222
  };
160
223
 
161
- const filteredItems = useMemo(
162
- [items, searchValue],
163
- (list, query) => filterSidebarItems(list, (query ?? "").trim())
164
- );
224
+ const filteredItems = useMemo([items, searchValue], (list, query) => {
225
+ return filterSidebarItems(resolveItems(list), (query ?? "").trim());
226
+ });
227
+
228
+ const leadingContent = typeof leading === "function" ? leading() : leading;
229
+ const trailingContent =
230
+ typeof trailing === "function" ? trailing() : trailing;
165
231
 
166
232
  const content = [];
167
233
  if (searchable) {
168
- content.push(div({ class: "px-base py-xs" },
169
- SearchBox({
170
- placeholder: "Search",
171
- variant: "rounded",
172
- class: "w-full",
173
- value: searchValue.get(),
174
- onInput: (event) => {
175
- const value = event?.target?.value ?? "";
176
- searchValue.set(value);
177
- },
178
- ...searchProps
179
- })
180
- ));
234
+ content.push(
235
+ div(
236
+ { class: "px-base py-xs" },
237
+ SearchBox({
238
+ placeholder: "Search",
239
+ variant: "rounded",
240
+ class: "w-full",
241
+ value: searchValue.get(),
242
+ onInput: (event) => {
243
+ const value = event?.target?.value ?? "";
244
+ searchValue.set(value);
245
+ },
246
+ ...searchProps,
247
+ }),
248
+ ),
249
+ );
250
+ }
251
+ if (leadingContent) {
252
+ content.push(div({ class: "px-base py-xs" }, leadingContent));
181
253
  }
182
254
  content.push(ForEach(filteredItems, "id", (item) => renderItem(item)));
255
+ if (trailingContent) {
256
+ content.push(div({ class: "px-base py-xs" }, trailingContent));
257
+ }
183
258
 
184
- return div({ class: "sidebar" }, [
185
- div({ class: "column-container py-xs" }, content),
259
+ return div({ class: `sidebar ${className}` }, [
260
+ div({ class: "column-container py-xs w-full h-full" }, content),
186
261
  ]);
187
262
  }
@@ -51,7 +51,9 @@ const compareValues = (aValue, bValue, sortType) => {
51
51
  return aDate - bDate;
52
52
  }
53
53
 
54
- return String(aValue).localeCompare(String(bValue), undefined, { sensitivity: "base" });
54
+ return String(aValue).localeCompare(String(bValue), undefined, {
55
+ sensitivity: "base",
56
+ });
55
57
  };
56
58
 
57
59
  export default function Table({
@@ -66,47 +68,59 @@ export default function Table({
66
68
  sort,
67
69
  variant = "regular",
68
70
  interactive = false,
69
- class: className = ""
71
+ hideHeaders = false,
72
+ class: className = "",
70
73
  } = {}) {
71
74
  const renderer = renderCell || cell;
72
75
  const searchField = searchable?.field;
73
76
  const searchText = searchable?.searchText;
74
- const searchTextState = searchText && typeof searchText.map === "function" ? searchText : null;
77
+ const searchTextState =
78
+ searchText && typeof searchText.map === "function" ? searchText : null;
75
79
  const sortableConfig = Array.isArray(sortable) ? sortable : [];
76
80
  const initialSort = sortableConfig.find((entry) => entry.sorted);
77
81
  const sortState = useState(
78
82
  initialSort
79
83
  ? {
80
84
  field: initialSort.field,
81
- direction: initialSort.direction === "desc" ? "desc" : "asc"
85
+ direction: initialSort.direction === "desc" ? "desc" : "asc",
82
86
  }
83
- : null
87
+ : null,
84
88
  );
85
89
  const selectionEnabled = typeof selection === "function";
86
90
  const selectedKeys = useState([]);
87
91
 
88
- const variantClass = variant === "background"
89
- ? "table-bg"
90
- : variant === "bordered"
91
- ? "table-bordered"
92
- : "";
93
- const interactiveClass = interactive ? "table-hover-rows table-interactive" : "";
92
+ const variantClass =
93
+ variant === "background"
94
+ ? "table-bg"
95
+ : variant === "bordered"
96
+ ? "table-bordered"
97
+ : "";
98
+ const interactiveClass = interactive
99
+ ? "table-hover-rows table-interactive"
100
+ : "";
94
101
 
95
102
  const filterRows = (rows, textValue) => {
96
103
  if (!searchField || textValue == null || textValue === "") return rows;
97
104
  const needle = String(textValue).toLowerCase();
98
105
  return (rows || []).filter((row) => {
99
106
  const value = row && typeof row === "object" ? row[searchField] : "";
100
- return String(value ?? "").toLowerCase().includes(needle);
107
+ return String(value ?? "")
108
+ .toLowerCase()
109
+ .includes(needle);
101
110
  });
102
111
  };
103
112
 
104
- const resolvedSearchText = searchTextState ? searchTextState.get() : searchText;
105
- const isDataState = data && typeof data.get === "function" && typeof data.map === "function";
113
+ const resolvedSearchText = searchTextState
114
+ ? searchTextState.get()
115
+ : searchText;
116
+ const isDataState =
117
+ data && typeof data.get === "function" && typeof data.map === "function";
106
118
 
107
119
  const applySort = (rows, sortValue) => {
108
120
  if (!sortValue || !sortValue.field) return rows;
109
- const sortableEntry = sortableConfig.find((entry) => entry.field === sortValue.field);
121
+ const sortableEntry = sortableConfig.find(
122
+ (entry) => entry.field === sortValue.field,
123
+ );
110
124
  if (!sortableEntry) return rows;
111
125
 
112
126
  const direction = sortValue.direction === "desc" ? -1 : 1;
@@ -127,22 +141,28 @@ export default function Table({
127
141
 
128
142
  const buildRows = (rows, textValue, sortValue) =>
129
143
  applySort(filterRows(rows, textValue), sortValue).map((row, index) => ({
130
- __key: (keyField && row && row[keyField] != null) ? row[keyField] : fallbackKey(row, index),
131
- __row: row
144
+ __key:
145
+ keyField && row && row[keyField] != null
146
+ ? row[keyField]
147
+ : fallbackKey(row, index),
148
+ __row: row,
132
149
  }));
133
150
 
134
151
  const normalizedRows = useMemo(
135
152
  [data, searchTextState ?? searchText, sortState],
136
- (rows, textValue, sortValue) => buildRows(rows, textValue, sortValue)
153
+ (rows, textValue, sortValue) => buildRows(rows, textValue, sortValue),
137
154
  );
138
155
 
139
- const visibleKeysState = normalizedRows && typeof normalizedRows.map === "function"
140
- ? normalizedRows.map((rows) => (rows || []).map((row) => row.__key))
141
- : null;
156
+ const visibleKeysState =
157
+ normalizedRows && typeof normalizedRows.map === "function"
158
+ ? normalizedRows.map((rows) => (rows || []).map((row) => row.__key))
159
+ : null;
142
160
 
143
161
  const isAllSelected = visibleKeysState
144
- ? useMemo([selectedKeys, visibleKeysState], (keys, visible) =>
145
- visible.length > 0 && visible.every((key) => keys.includes(key))
162
+ ? useMemo(
163
+ [selectedKeys, visibleKeysState],
164
+ (keys, visible) =>
165
+ visible.length > 0 && visible.every((key) => keys.includes(key)),
146
166
  )
147
167
  : selectedKeys.map((keys) => keys.length > 0);
148
168
 
@@ -173,82 +193,111 @@ export default function Table({
173
193
  }
174
194
  sortState.set({
175
195
  field,
176
- direction: current.direction === "asc" ? "desc" : "asc"
196
+ direction: current.direction === "asc" ? "desc" : "asc",
177
197
  });
178
198
  };
179
199
 
180
- return table({ class: `table ${variantClass} ${interactiveClass} ${className}`.trim() }, [
181
- colgroup(
182
- [
183
- selectionEnabled ? col({ style: "width: 40px;" }) : null,
184
- ...columns.map((column) => col({ style: `width: ${resolveColumnWidth(column.size)};` }))
185
- ].filter(Boolean)
186
- ),
187
- thead([
188
- tr(
189
- [
190
- selectionEnabled ? th({ class: "table-checkbox-cell" }, [
191
- Checkbox({
192
- class: "table-checkbox",
193
- checked: isAllSelected,
194
- change: handleToggleAll
195
- })
196
- ]) : null,
197
- ...columns.map((column) => {
198
- const sortableEntry = sortableConfig.find((entry) => entry.field === column.field);
199
- if (!sortableEntry) {
200
- return th(column.label ?? column.field ?? "");
201
- }
202
- const iconClass = sortState.map((sortValue) => {
203
- const isSorted = sortValue && sortValue.field === column.field;
204
- const isAsc = isSorted && sortValue.direction === "asc";
205
- return `icon icon-chevron-down table-sort-icon ${isSorted ? "icon-base" : "icon-quaternary"} ${isAsc ? "rotate-180" : ""}`.trim();
206
- });
207
-
208
- return th({
209
- class: sortState.map((sortValue) => {
210
- const isSorted = sortValue && sortValue.field === column.field;
211
- return `table-sortable hoverable ${isSorted ? "is-sorted" : ""}`.trim();
212
- }),
213
- click: () => handleSort(column.field)
214
- }, [
215
- span({ class: "row-container items-center gap-xs w-full" }, [
216
- span(column.label ?? column.field ?? ""),
217
- span({ class: iconClass.map(cls => `${cls} ml-auto`.trim()) })
218
- ])
219
- ]);
220
- })
221
- ].filter(Boolean)
222
- )
223
- ]),
224
- tbody([
225
- ForEach(normalizedRows, "__key", (item, rowIndex) => {
226
- const row = item.__row;
227
- return tr(
200
+ const header = hideHeaders
201
+ ? null
202
+ : thead([
203
+ tr(
228
204
  [
229
- selectionEnabled ? td({ class: "table-checkbox-cell" }, [
230
- Checkbox({
231
- class: "table-checkbox",
232
- checked: selectedKeys.map((keys) => keys.includes(item.__key)),
233
- change: () => handleToggleRow(item.__key)
234
- })
235
- ]) : null,
236
- ...columns.map((column, columnIndex) => {
237
- if (renderer) {
238
- const rendered = renderer(columnIndex, column.field, row, column);
239
- if (rendered !== undefined && rendered !== null) {
240
- return td(rendered);
241
- }
242
- }
243
- const value = row && typeof row === "object" ? row[column.field] : "";
244
- if (value && typeof value.map === "function") {
245
- return td(value.map((val) => span(val)));
205
+ selectionEnabled
206
+ ? th({ class: "table-checkbox-cell" }, [
207
+ Checkbox({
208
+ class: "table-checkbox",
209
+ checked: isAllSelected,
210
+ change: handleToggleAll,
211
+ }),
212
+ ])
213
+ : null,
214
+ ...columns.map((column) => {
215
+ const sortableEntry = sortableConfig.find(
216
+ (entry) => entry.field === column.field,
217
+ );
218
+ if (!sortableEntry) {
219
+ return th(column.label ?? column.field ?? "");
246
220
  }
247
- return td(String(value ?? ""));
248
- })
249
- ].filter(Boolean)
250
- );
251
- })
252
- ])
253
- ]);
221
+ const iconClass = sortState.map((sortValue) => {
222
+ const isSorted = sortValue && sortValue.field === column.field;
223
+ const isAsc = isSorted && sortValue.direction === "asc";
224
+ return `icon icon-chevron-down table-sort-icon ${isSorted ? "icon-base" : "icon-quaternary"} ${isAsc ? "rotate-180" : ""}`.trim();
225
+ });
226
+
227
+ return th(
228
+ {
229
+ class: sortState.map((sortValue) => {
230
+ const isSorted =
231
+ sortValue && sortValue.field === column.field;
232
+ return `table-sortable hoverable ${isSorted ? "is-sorted" : ""}`.trim();
233
+ }),
234
+ click: () => handleSort(column.field),
235
+ },
236
+ [
237
+ span({ class: "row-container items-center gap-xs w-full" }, [
238
+ span(column.label ?? column.field ?? ""),
239
+ span({
240
+ class: iconClass.map((cls) => `${cls} ml-auto`.trim()),
241
+ }),
242
+ ]),
243
+ ],
244
+ );
245
+ }),
246
+ ].filter(Boolean),
247
+ ),
248
+ ]);
249
+
250
+ return table(
251
+ { class: `table ${variantClass} ${interactiveClass} ${className}`.trim() },
252
+ [
253
+ colgroup(
254
+ [
255
+ selectionEnabled ? col({ style: "width: 40px;" }) : null,
256
+ ...columns.map((column) =>
257
+ col({ style: `width: ${resolveColumnWidth(column.size)};` }),
258
+ ),
259
+ ].filter(Boolean),
260
+ ),
261
+ header,
262
+ tbody([
263
+ ForEach(normalizedRows, "__key", (item, rowIndex) => {
264
+ const row = item.__row;
265
+ return tr(
266
+ [
267
+ selectionEnabled
268
+ ? td({ class: "table-checkbox-cell" }, [
269
+ Checkbox({
270
+ class: "table-checkbox",
271
+ checked: selectedKeys.map((keys) =>
272
+ keys.includes(item.__key),
273
+ ),
274
+ change: () => handleToggleRow(item.__key),
275
+ }),
276
+ ])
277
+ : null,
278
+ ...columns.map((column, columnIndex) => {
279
+ if (renderer) {
280
+ const rendered = renderer(
281
+ columnIndex,
282
+ column.field,
283
+ row,
284
+ column,
285
+ );
286
+ if (rendered !== undefined && rendered !== null) {
287
+ return td(rendered);
288
+ }
289
+ }
290
+ const value =
291
+ row && typeof row === "object" ? row[column.field] : "";
292
+ if (value && typeof value.map === "function") {
293
+ return td(value.map((val) => span(val)));
294
+ }
295
+ return td(String(value ?? ""));
296
+ }),
297
+ ].filter(Boolean),
298
+ );
299
+ }),
300
+ ]),
301
+ ],
302
+ );
254
303
  }