@bunnix/components 0.9.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 (152) hide show
  1. package/@types/index.d.ts +269 -0
  2. package/LICENSE +7 -0
  3. package/README.md +184 -0
  4. package/package.json +53 -0
  5. package/src/components/AccordionGroup.mjs +37 -0
  6. package/src/components/Badge.mjs +49 -0
  7. package/src/components/Button.mjs +76 -0
  8. package/src/components/Checkbox.mjs +36 -0
  9. package/src/components/ComboBox.mjs +44 -0
  10. package/src/components/Container.mjs +27 -0
  11. package/src/components/DatePicker.mjs +251 -0
  12. package/src/components/Dialog.mjs +166 -0
  13. package/src/components/DropdownMenu.mjs +110 -0
  14. package/src/components/Grid.mjs +40 -0
  15. package/src/components/HStack.mjs +34 -0
  16. package/src/components/Icon.mjs +32 -0
  17. package/src/components/InputField.mjs +78 -0
  18. package/src/components/NavigationBar.mjs +47 -0
  19. package/src/components/PageHeader.mjs +13 -0
  20. package/src/components/PageSection.mjs +20 -0
  21. package/src/components/PopoverMenu.mjs +87 -0
  22. package/src/components/RadioCheckbox.mjs +36 -0
  23. package/src/components/SearchBox.mjs +207 -0
  24. package/src/components/Sidebar.mjs +187 -0
  25. package/src/components/Table.mjs +254 -0
  26. package/src/components/Text.mjs +38 -0
  27. package/src/components/TimePicker.mjs +172 -0
  28. package/src/components/ToastNotification.mjs +105 -0
  29. package/src/components/ToggleSwitch.mjs +26 -0
  30. package/src/components/VStack.mjs +35 -0
  31. package/src/icons/add-circle.svg +1 -0
  32. package/src/icons/add.svg +1 -0
  33. package/src/icons/alt.svg +1 -0
  34. package/src/icons/archive.svg +1 -0
  35. package/src/icons/at.svg +1 -0
  36. package/src/icons/attestation.svg +1 -0
  37. package/src/icons/bell.svg +4 -0
  38. package/src/icons/bookmark.svg +1 -0
  39. package/src/icons/bot.svg +1 -0
  40. package/src/icons/button.svg +1 -0
  41. package/src/icons/calculate.svg +1 -0
  42. package/src/icons/calendar.svg +1 -0
  43. package/src/icons/chart.svg +1 -0
  44. package/src/icons/check.svg +1 -0
  45. package/src/icons/chevron-down.svg +1 -0
  46. package/src/icons/chevron-left.svg +1 -0
  47. package/src/icons/chevron-right.svg +1 -0
  48. package/src/icons/clip.svg +1 -0
  49. package/src/icons/clock.svg +4 -0
  50. package/src/icons/close-circle.svg +4 -0
  51. package/src/icons/close.svg +1 -0
  52. package/src/icons/cloud-download.svg +1 -0
  53. package/src/icons/cloud-upload.svg +1 -0
  54. package/src/icons/cloud.svg +1 -0
  55. package/src/icons/columns-layout.svg +1 -0
  56. package/src/icons/command.svg +1 -0
  57. package/src/icons/cube.svg +1 -0
  58. package/src/icons/delete.svg +4 -0
  59. package/src/icons/dollar.svg +4 -0
  60. package/src/icons/download.svg +1 -0
  61. package/src/icons/draw.svg +1 -0
  62. package/src/icons/duplicate.svg +4 -0
  63. package/src/icons/edit.svg +1 -0
  64. package/src/icons/exclamation-mark.svg +1 -0
  65. package/src/icons/eye-open.svg +1 -0
  66. package/src/icons/eye.svg +1 -0
  67. package/src/icons/file-html.svg +1 -0
  68. package/src/icons/file.svg +4 -0
  69. package/src/icons/finger.svg +1 -0
  70. package/src/icons/flag.svg +1 -0
  71. package/src/icons/folder.svg +1 -0
  72. package/src/icons/function.svg +1 -0
  73. package/src/icons/gear.svg +1 -0
  74. package/src/icons/gift.svg +1 -0
  75. package/src/icons/globe.svg +4 -0
  76. package/src/icons/grid.svg +1 -0
  77. package/src/icons/hand.svg +1 -0
  78. package/src/icons/heart.svg +4 -0
  79. package/src/icons/home.svg +4 -0
  80. package/src/icons/image.svg +1 -0
  81. package/src/icons/inbox.svg +4 -0
  82. package/src/icons/info.svg +1 -0
  83. package/src/icons/key.svg +1 -0
  84. package/src/icons/lamp.svg +1 -0
  85. package/src/icons/link.svg +1 -0
  86. package/src/icons/location.svg +1 -0
  87. package/src/icons/locker.svg +1 -0
  88. package/src/icons/login.svg +1 -0
  89. package/src/icons/logout.svg +4 -0
  90. package/src/icons/mail.svg +4 -0
  91. package/src/icons/map.svg +4 -0
  92. package/src/icons/markup.svg +1 -0
  93. package/src/icons/merge.svg +1 -0
  94. package/src/icons/more-horizontal.svg +5 -0
  95. package/src/icons/more-vertical.svg +5 -0
  96. package/src/icons/mouse.svg +1 -0
  97. package/src/icons/palette.svg +1 -0
  98. package/src/icons/password.svg +1 -0
  99. package/src/icons/pencil.svg +1 -0
  100. package/src/icons/people.svg +4 -0
  101. package/src/icons/person-add.svg +1 -0
  102. package/src/icons/person-remove.svg +1 -0
  103. package/src/icons/person.svg +5 -0
  104. package/src/icons/pin.svg +1 -0
  105. package/src/icons/question-circle.svg +4 -0
  106. package/src/icons/remove-circle.svg +1 -0
  107. package/src/icons/return-arrow.svg +2 -0
  108. package/src/icons/save.svg +1 -0
  109. package/src/icons/search.svg +1 -0
  110. package/src/icons/sections.svg +1 -0
  111. package/src/icons/send.svg +1 -0
  112. package/src/icons/share.svg +1 -0
  113. package/src/icons/shine.svg +1 -0
  114. package/src/icons/sliders.svg +1 -0
  115. package/src/icons/star.svg +4 -0
  116. package/src/icons/storage.svg +1 -0
  117. package/src/icons/success-circle.svg +4 -0
  118. package/src/icons/swap.svg +1 -0
  119. package/src/icons/switch.svg +1 -0
  120. package/src/icons/sync.svg +4 -0
  121. package/src/icons/table.svg +4 -0
  122. package/src/icons/tag.svg +4 -0
  123. package/src/icons/terminal.svg +1 -0
  124. package/src/icons/text.svg +1 -0
  125. package/src/icons/thumb-down.svg +1 -0
  126. package/src/icons/thumb-up.svg +1 -0
  127. package/src/icons/timer.svg +4 -0
  128. package/src/icons/toggle.svg +1 -0
  129. package/src/icons/trash.svg +1 -0
  130. package/src/icons/update-page.svg +1 -0
  131. package/src/icons/upload.svg +1 -0
  132. package/src/icons/video.svg +1 -0
  133. package/src/icons/wallet.svg +1 -0
  134. package/src/icons/window.svg +1 -0
  135. package/src/index.mjs +29 -0
  136. package/src/styles/accordion.css +70 -0
  137. package/src/styles/buttons.css +118 -0
  138. package/src/styles/colors.css +131 -0
  139. package/src/styles/controls.css +504 -0
  140. package/src/styles/datepicker.css +140 -0
  141. package/src/styles/interactable.css +16 -0
  142. package/src/styles/layout.css +444 -0
  143. package/src/styles/links.css +38 -0
  144. package/src/styles/main.css +16 -0
  145. package/src/styles/media.css +155 -0
  146. package/src/styles/menu.css +168 -0
  147. package/src/styles/motion.css +66 -0
  148. package/src/styles/table.css +78 -0
  149. package/src/styles/timepicker.css +87 -0
  150. package/src/styles/typography.css +94 -0
  151. package/src/styles/variables.css +218 -0
  152. package/src/styles.css +1 -0
@@ -0,0 +1,187 @@
1
+ import Bunnix, { useMemo, useState, ForEach, Show } from "@bunnix/core";
2
+ import SearchBox from "./SearchBox.mjs";
3
+ import Badge from "./Badge.mjs";
4
+ const { div, a, span, h4, h6, hr } = Bunnix;
5
+
6
+ export default function Sidebar({
7
+ items = [],
8
+ selection,
9
+ onSelect,
10
+ onItemSelect,
11
+ searchable = false,
12
+ searchProps = {}
13
+ } = {}) {
14
+ const selected = useState(selection ?? 'home');
15
+ const searchValue = useState("");
16
+
17
+ // Initialize expanded state from items' isExpanded property
18
+ const initialExpanded = items.reduce((acc, item) => {
19
+ if (item.children && item.isExpanded) {
20
+ acc[item.id] = true;
21
+ }
22
+ return acc;
23
+ }, {});
24
+
25
+ const expanded = useState(initialExpanded);
26
+
27
+ const handleClick = (id, href) => {
28
+ selected.set(id);
29
+ if (href !== null) {
30
+ const target = href ?? `#${id}`;
31
+ if (target && window?.location) {
32
+ window.location.hash = target;
33
+ }
34
+ }
35
+ if (onSelect) onSelect(id);
36
+ if (onItemSelect) onItemSelect(id);
37
+ };
38
+
39
+ const toggleExpand = (id) => {
40
+ const current = expanded.get();
41
+ expanded.set({ ...current, [id]: !current[id] });
42
+ };
43
+
44
+ const renderItem = (item, isChild = false) => {
45
+ const isSearching = (searchValue.get() ?? "").trim() !== "";
46
+ if (item.isSeparator) {
47
+ return div({ class: "py-xs px-base" }, hr({ class: "no-margin" }));
48
+ }
49
+
50
+ 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)
53
+ );
54
+ }
55
+
56
+ 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])
61
+ );
62
+
63
+ const handleItemClick = (e) => {
64
+ if (hasChildren && !isSearching) {
65
+ toggleExpand(item.id);
66
+ } else {
67
+ handleClick(item.id, item.href);
68
+ }
69
+ };
70
+
71
+ 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
+ ])
100
+ ),
101
+ hasChildren && Show(isExpanded, div({ class: "column-container py-xs" },
102
+ item.children.map(child => renderItem(child, true))
103
+ ))
104
+ ]);
105
+ };
106
+
107
+ const filterSidebarItems = (rawItems, query) => {
108
+ if (!query) return rawItems;
109
+ const normalized = query.trim().toLowerCase();
110
+ if (!normalized) return rawItems;
111
+
112
+ const filterItem = (item) => {
113
+ if (item.isHeader || item.isSeparator) return item;
114
+ const label = (item.label ?? "").toLowerCase();
115
+ const hasChildren = item.children && item.children.length > 0;
116
+ if (hasChildren) {
117
+ const filteredChildren = item.children.map(child => filterItem(child)).filter(Boolean);
118
+ const matched = label.includes(normalized) || filteredChildren.length > 0;
119
+ if (!matched) return null;
120
+ return { ...item, children: filteredChildren };
121
+ }
122
+ return label.includes(normalized) ? item : null;
123
+ };
124
+
125
+ const result = [];
126
+ let currentHeader = null;
127
+ let currentGroup = [];
128
+ const flush = () => {
129
+ if (currentHeader) {
130
+ if (currentGroup.length > 0) {
131
+ result.push(currentHeader, ...currentGroup);
132
+ }
133
+ } else if (currentGroup.length > 0) {
134
+ result.push(...currentGroup);
135
+ }
136
+ currentHeader = null;
137
+ currentGroup = [];
138
+ };
139
+
140
+ for (const item of rawItems) {
141
+ if (item.isHeader) {
142
+ flush();
143
+ currentHeader = item;
144
+ continue;
145
+ }
146
+ if (item.isSeparator) {
147
+ if (currentGroup.length > 0) {
148
+ currentGroup.push(item);
149
+ }
150
+ continue;
151
+ }
152
+ const filtered = filterItem(item);
153
+ if (filtered) {
154
+ currentGroup.push(filtered);
155
+ }
156
+ }
157
+ flush();
158
+ return result;
159
+ };
160
+
161
+ const filteredItems = useMemo(
162
+ [items, searchValue],
163
+ (list, query) => filterSidebarItems(list, (query ?? "").trim())
164
+ );
165
+
166
+ const content = [];
167
+ 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
+ ));
181
+ }
182
+ content.push(ForEach(filteredItems, "id", (item) => renderItem(item)));
183
+
184
+ return div({ class: "sidebar" }, [
185
+ div({ class: "column-container py-xs" }, content),
186
+ ]);
187
+ }
@@ -0,0 +1,254 @@
1
+ import Bunnix, { ForEach, useMemo, useState } from "@bunnix/core";
2
+ import Checkbox from "./Checkbox.mjs";
3
+ const { table, thead, tbody, tr, th, td, colgroup, col, span } = Bunnix;
4
+
5
+ const normalizeKey = (value) =>
6
+ String(value ?? "")
7
+ .trim()
8
+ .toLowerCase()
9
+ .replace(/[^a-z0-9]+/g, "_")
10
+ .replace(/^_+|_+$/g, "");
11
+
12
+ const fallbackKey = (row, index) => {
13
+ if (row && typeof row === "object") {
14
+ const values = Object.values(row).map((val) => normalizeKey(val));
15
+ return `${values.filter(Boolean).join("_")}_${index}`;
16
+ }
17
+ return `${normalizeKey(row)}_${index}`;
18
+ };
19
+
20
+ const resolveColumnWidth = (size) => {
21
+ if (!size || size === "auto") return "auto";
22
+ if (typeof size === "number") return `${size}px`;
23
+ return size;
24
+ };
25
+
26
+ const stableSort = (rows, compare) =>
27
+ rows
28
+ .map((item, index) => ({ item, index }))
29
+ .sort((a, b) => {
30
+ const result = compare(a.item, b.item);
31
+ return result === 0 ? a.index - b.index : result;
32
+ })
33
+ .map((entry) => entry.item);
34
+
35
+ const compareValues = (aValue, bValue, sortType) => {
36
+ const aEmpty = aValue == null || aValue === "";
37
+ const bEmpty = bValue == null || bValue === "";
38
+ if (aEmpty && bEmpty) return 0;
39
+ if (aEmpty) return 1;
40
+ if (bEmpty) return -1;
41
+
42
+ if (sortType === "number") {
43
+ const aNum = Number(aValue);
44
+ const bNum = Number(bValue);
45
+ return aNum - bNum;
46
+ }
47
+
48
+ if (sortType === "date") {
49
+ const aDate = new Date(aValue).getTime();
50
+ const bDate = new Date(bValue).getTime();
51
+ return aDate - bDate;
52
+ }
53
+
54
+ return String(aValue).localeCompare(String(bValue), undefined, { sensitivity: "base" });
55
+ };
56
+
57
+ export default function Table({
58
+ columns = [],
59
+ data = [],
60
+ key: keyField,
61
+ renderCell,
62
+ cell,
63
+ searchable,
64
+ sortable = [],
65
+ selection,
66
+ sort,
67
+ variant = "regular",
68
+ interactive = false,
69
+ class: className = ""
70
+ } = {}) {
71
+ const renderer = renderCell || cell;
72
+ const searchField = searchable?.field;
73
+ const searchText = searchable?.searchText;
74
+ const searchTextState = searchText && typeof searchText.map === "function" ? searchText : null;
75
+ const sortableConfig = Array.isArray(sortable) ? sortable : [];
76
+ const initialSort = sortableConfig.find((entry) => entry.sorted);
77
+ const sortState = useState(
78
+ initialSort
79
+ ? {
80
+ field: initialSort.field,
81
+ direction: initialSort.direction === "desc" ? "desc" : "asc"
82
+ }
83
+ : null
84
+ );
85
+ const selectionEnabled = typeof selection === "function";
86
+ const selectedKeys = useState([]);
87
+
88
+ const variantClass = variant === "background"
89
+ ? "table-bg"
90
+ : variant === "bordered"
91
+ ? "table-bordered"
92
+ : "";
93
+ const interactiveClass = interactive ? "table-hover-rows table-interactive" : "";
94
+
95
+ const filterRows = (rows, textValue) => {
96
+ if (!searchField || textValue == null || textValue === "") return rows;
97
+ const needle = String(textValue).toLowerCase();
98
+ return (rows || []).filter((row) => {
99
+ const value = row && typeof row === "object" ? row[searchField] : "";
100
+ return String(value ?? "").toLowerCase().includes(needle);
101
+ });
102
+ };
103
+
104
+ const resolvedSearchText = searchTextState ? searchTextState.get() : searchText;
105
+ const isDataState = data && typeof data.get === "function" && typeof data.map === "function";
106
+
107
+ const applySort = (rows, sortValue) => {
108
+ if (!sortValue || !sortValue.field) return rows;
109
+ const sortableEntry = sortableConfig.find((entry) => entry.field === sortValue.field);
110
+ if (!sortableEntry) return rows;
111
+
112
+ const direction = sortValue.direction === "desc" ? -1 : 1;
113
+
114
+ if (sort) {
115
+ const comparator = sort(sortValue.field);
116
+ if (typeof comparator === "function") {
117
+ return stableSort(rows, (a, b) => comparator(a, b) * direction);
118
+ }
119
+ }
120
+
121
+ return stableSort(rows, (a, b) => {
122
+ const aValue = a && typeof a === "object" ? a[sortValue.field] : "";
123
+ const bValue = b && typeof b === "object" ? b[sortValue.field] : "";
124
+ return compareValues(aValue, bValue, sortableEntry?.sortType) * direction;
125
+ });
126
+ };
127
+
128
+ const buildRows = (rows, textValue, sortValue) =>
129
+ applySort(filterRows(rows, textValue), sortValue).map((row, index) => ({
130
+ __key: (keyField && row && row[keyField] != null) ? row[keyField] : fallbackKey(row, index),
131
+ __row: row
132
+ }));
133
+
134
+ const normalizedRows = useMemo(
135
+ [data, searchTextState ?? searchText, sortState],
136
+ (rows, textValue, sortValue) => buildRows(rows, textValue, sortValue)
137
+ );
138
+
139
+ const visibleKeysState = normalizedRows && typeof normalizedRows.map === "function"
140
+ ? normalizedRows.map((rows) => (rows || []).map((row) => row.__key))
141
+ : null;
142
+
143
+ const isAllSelected = visibleKeysState
144
+ ? useMemo([selectedKeys, visibleKeysState], (keys, visible) =>
145
+ visible.length > 0 && visible.every((key) => keys.includes(key))
146
+ )
147
+ : selectedKeys.map((keys) => keys.length > 0);
148
+
149
+ const handleSelectionChange = (next) => {
150
+ selectedKeys.set(next);
151
+ if (selectionEnabled) selection(next);
152
+ };
153
+
154
+ const handleToggleAll = () => {
155
+ const visibleKeys = visibleKeysState ? visibleKeysState.get() : [];
156
+ const allSelected = isAllSelected.get();
157
+ handleSelectionChange(allSelected ? [] : visibleKeys);
158
+ };
159
+
160
+ const handleToggleRow = (rowKey) => {
161
+ const current = selectedKeys.get();
162
+ const next = current.includes(rowKey)
163
+ ? current.filter((key) => key !== rowKey)
164
+ : [...current, rowKey];
165
+ handleSelectionChange(next);
166
+ };
167
+
168
+ const handleSort = (field) => {
169
+ const current = sortState.get();
170
+ if (!current || current.field !== field) {
171
+ sortState.set({ field, direction: "asc" });
172
+ return;
173
+ }
174
+ sortState.set({
175
+ field,
176
+ direction: current.direction === "asc" ? "desc" : "asc"
177
+ });
178
+ };
179
+
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(
228
+ [
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)));
246
+ }
247
+ return td(String(value ?? ""));
248
+ })
249
+ ].filter(Boolean)
250
+ );
251
+ })
252
+ ])
253
+ ]);
254
+ }
@@ -0,0 +1,38 @@
1
+ import Bunnix from "@bunnix/core";
2
+ const { span, p, h1, h2, h3, h4 } = Bunnix;
3
+
4
+ export default function Text({
5
+ type = "text",
6
+ color = "primary",
7
+ design = "regular",
8
+ class: className = "",
9
+ ...rest
10
+ } = {}, children) {
11
+ const tagMap = {
12
+ text: span,
13
+ paragraph: p,
14
+ heading1: h1,
15
+ heading2: h2,
16
+ heading3: h3,
17
+ heading4: h4
18
+ };
19
+
20
+ const tag = tagMap[type] || span;
21
+
22
+ // Color mapping: primary -> text-primary, secondary -> text-secondary, etc.
23
+ const colorClass = color ? `text-${color}` : "";
24
+
25
+ // Design mapping: mono -> text-mono, regular -> ""
26
+ const designClass = design === "mono" ? "text-mono" : "";
27
+
28
+ const isState = className && typeof className.map === "function";
29
+
30
+ const combinedClass = isState
31
+ ? className.map((value) => `${colorClass} ${designClass} ${value}`.trim())
32
+ : `${colorClass} ${designClass} ${className}`.trim();
33
+
34
+ return tag({
35
+ class: combinedClass,
36
+ ...rest
37
+ }, children);
38
+ }
@@ -0,0 +1,172 @@
1
+ import Bunnix, { useRef, useState, useMemo } from "@bunnix/core";
2
+ import Icon from "./Icon.mjs";
3
+ const { div, button, span, hr, input } = Bunnix;
4
+
5
+ const formatSegment = (val) => val.toString().padStart(2, '0');
6
+
7
+ export default function TimePicker({
8
+ id,
9
+ placeholder,
10
+ variant = "regular",
11
+ size = "md",
12
+ class: className = ""
13
+ } = {}) {
14
+ const popoverRef = useRef(null);
15
+ const hourInputRef = useRef(null);
16
+ const minuteInputRef = useRef(null);
17
+ const pickerId = id || `timepicker-${Math.random().toString(36).slice(2, 8)}`;
18
+ const anchorName = `--${pickerId}`;
19
+
20
+ // State as strings for better input handling
21
+ const now = new Date();
22
+ const hour = useState(formatSegment(now.getHours()));
23
+ const minute = useState(formatSegment(now.getMinutes()));
24
+ const isModified = useState(false);
25
+
26
+ const openPopover = () => {
27
+ const popover = popoverRef.current;
28
+ if (popover && !popover.matches(":popover-open")) {
29
+ popover.showPopover();
30
+ }
31
+ };
32
+
33
+ const closePopover = () => {
34
+ const popover = popoverRef.current;
35
+ if (popover && popover.matches(":popover-open")) {
36
+ popover.hidePopover();
37
+ }
38
+ };
39
+
40
+ const handleNow = () => {
41
+ const d = new Date();
42
+ hour.set(formatSegment(d.getHours()));
43
+ minute.set(formatSegment(d.getMinutes()));
44
+ isModified.set(true);
45
+ };
46
+
47
+ const handleHourInput = (e) => {
48
+ let raw = e.target.value.replace(/\D/g, '').slice(0, 2);
49
+ if (raw !== "") {
50
+ const val = parseInt(raw, 10);
51
+ if (val > 23) raw = "23";
52
+ }
53
+ hour.set(raw);
54
+ isModified.set(true);
55
+
56
+ // Auto-focus minutes if we have 2 digits or a digit that can't be leading (3-9)
57
+ if (raw.length === 2 || (raw.length === 1 && parseInt(raw, 10) > 2)) {
58
+ minuteInputRef.current?.focus();
59
+ minuteInputRef.current?.select();
60
+ }
61
+ };
62
+
63
+ const handleMinuteInput = (e) => {
64
+ let raw = e.target.value.replace(/\D/g, '').slice(0, 2);
65
+ if (raw !== "") {
66
+ const val = parseInt(raw, 10);
67
+ if (val > 59) raw = "59";
68
+ }
69
+ minute.set(raw);
70
+ isModified.set(true);
71
+ };
72
+
73
+ const handleBlur = (type) => {
74
+ const state = type === 'hour' ? hour : minute;
75
+ let val = state.get();
76
+ if (val === "") val = "00";
77
+ state.set(val.padStart(2, '0'));
78
+ };
79
+
80
+ // Reactive display label for the trigger
81
+ const displayLabel = useMemo([hour, minute, isModified], (h, m, mod) => {
82
+ if (!mod && placeholder) return placeholder;
83
+ // Ensure we show padded values in the trigger even if input is mid-edit
84
+ const hh = h === "" ? "00" : h.padStart(2, '0');
85
+ const mm = m === "" ? "00" : m.padStart(2, '0');
86
+ return `${hh}:${hh === h ? '' : ''}${mm}`; // Trigger re-render correctly
87
+ });
88
+
89
+ // Refined display label using state directly for consistency
90
+ const finalLabel = useMemo([hour, minute, isModified], (h, m, mod) => {
91
+ if (!mod && placeholder) return placeholder;
92
+ const hh = h.padStart(2, '0');
93
+ const mm = m.padStart(2, '0');
94
+ return `${hh}:${mm}`;
95
+ });
96
+
97
+ const hasValue = isModified.map(m => !!m);
98
+
99
+ const normalizeSize = (value) => {
100
+ if (!value || value === "default" || value === "regular" || value === "md") return "md";
101
+ if (value === "sm") return "md";
102
+ if (value === "lg" || value === "xl") return value;
103
+ return value;
104
+ };
105
+ const normalizedSize = normalizeSize(size);
106
+ const variantClass = variant === "rounded" ? "rounded-full" : "";
107
+ const triggerSizeClass = normalizedSize === "xl"
108
+ ? "dropdown-xl"
109
+ : normalizedSize === "lg"
110
+ ? "dropdown-lg"
111
+ : "";
112
+ const iconSizeValue = normalizedSize === "sm"
113
+ ? "sm"
114
+ : normalizedSize === "lg"
115
+ ? "lg"
116
+ : normalizedSize === "xl"
117
+ ? "xl"
118
+ : undefined;
119
+
120
+ return div({ class: `timepicker-wrapper ${className}`.trim() }, [
121
+ button({
122
+ id: pickerId,
123
+ class: `dropdown-trigger timepicker-trigger justify-start ${variantClass} ${triggerSizeClass} no-chevron`.trim(),
124
+ style: `anchor-name: ${anchorName}`,
125
+ click: openPopover
126
+ }, [
127
+ span({ class: hasValue.map(h => h ? "" : "text-tertiary") }, finalLabel),
128
+ Icon({ name: "clock", fill: "quaternary", size: iconSizeValue, class: "ml-auto" })
129
+ ]),
130
+
131
+ div({
132
+ ref: popoverRef,
133
+ popover: "auto",
134
+ class: "timepicker-popover popover-base",
135
+ style: `--anchor-id: ${anchorName}`
136
+ }, [
137
+ div({ class: "card column-container shadow gap-0 p-0 bg-base timepicker-card" }, [
138
+ div({ class: "timepicker-display" }, [
139
+ input({
140
+ ref: hourInputRef,
141
+ type: "text",
142
+ class: "time-segment",
143
+ value: hour,
144
+ placeholder: "00",
145
+ input: handleHourInput,
146
+ blur: () => handleBlur('hour'),
147
+ focus: (e) => e.target.select()
148
+ }),
149
+ span({ class: "time-separator" }, ":"),
150
+ input({
151
+ ref: minuteInputRef,
152
+ type: "text",
153
+ class: "time-segment",
154
+ value: minute,
155
+ placeholder: "00",
156
+ input: handleMinuteInput,
157
+ blur: () => handleBlur('minute'),
158
+ focus: (e) => e.target.select()
159
+ })
160
+ ]),
161
+
162
+ hr({ class: "no-margin" }),
163
+
164
+ div({ class: "row-container justify-center items-center gap-md p-base shrink-0" }, [
165
+ button({ class: "btn btn-flat", click: () => { isModified.set(false); closePopover(); } }, "Clear"),
166
+ button({ class: "btn btn-flat", click: handleNow }, "Now"),
167
+ button({ class: "btn", click: closePopover }, "OK")
168
+ ])
169
+ ])
170
+ ])
171
+ ]);
172
+ }