@bunnix/components 0.10.2 → 0.11.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 (150) hide show
  1. package/@types/index.d.ts +179 -15
  2. package/README.md +41 -4
  3. package/package.json +3 -8
  4. package/src/core/buttons.css +1 -0
  5. package/src/core/core.css +86 -4
  6. package/src/core/dialog.css +3 -1
  7. package/src/core/dialog.mjs +101 -16
  8. package/src/core/input.css +202 -0
  9. package/src/core/inputs.mjs +702 -23
  10. package/src/core/layout.mjs +1 -2
  11. package/src/core/media.css +36 -1
  12. package/src/core/media.mjs +13 -13
  13. package/src/core/menu.css +10 -29
  14. package/src/core/menu.mjs +159 -70
  15. package/src/core/outline.mjs +100 -0
  16. package/src/core/sidebar.mjs +189 -68
  17. package/src/core/sliderUtils.mjs +51 -0
  18. package/src/core/table.css +23 -0
  19. package/src/core/table.mjs +35 -20
  20. package/src/core/textareaUtils.mjs +31 -0
  21. package/src/core/utils.mjs +105 -0
  22. package/src/font-face/Framework7Icons-Regular.woff2 +0 -0
  23. package/src/index.mjs +3 -1
  24. package/src/core/core-mobile.css +0 -69
  25. package/src/icons/add-circle.svg +0 -1
  26. package/src/icons/add.svg +0 -1
  27. package/src/icons/alt.svg +0 -1
  28. package/src/icons/archive.svg +0 -1
  29. package/src/icons/arrow-down.svg +0 -1
  30. package/src/icons/arrow-left.svg +0 -1
  31. package/src/icons/arrow-right.svg +0 -1
  32. package/src/icons/arrow-up.svg +0 -1
  33. package/src/icons/at.svg +0 -1
  34. package/src/icons/attestation.svg +0 -1
  35. package/src/icons/battery-25.svg +0 -1
  36. package/src/icons/bell.svg +0 -3
  37. package/src/icons/bookmark.svg +0 -1
  38. package/src/icons/bot.svg +0 -1
  39. package/src/icons/bubble.svg +0 -1
  40. package/src/icons/building.svg +0 -3
  41. package/src/icons/button.svg +0 -1
  42. package/src/icons/calculate.svg +0 -1
  43. package/src/icons/calendar.svg +0 -1
  44. package/src/icons/captions-bubble.svg +0 -1
  45. package/src/icons/cart.svg +0 -1
  46. package/src/icons/chart.svg +0 -1
  47. package/src/icons/check.svg +0 -1
  48. package/src/icons/chevron-down.svg +0 -1
  49. package/src/icons/chevron-left.svg +0 -1
  50. package/src/icons/chevron-right.svg +0 -1
  51. package/src/icons/clip.svg +0 -1
  52. package/src/icons/clock.svg +0 -3
  53. package/src/icons/close-circle.svg +0 -3
  54. package/src/icons/close.svg +0 -1
  55. package/src/icons/cloud-download.svg +0 -1
  56. package/src/icons/cloud-upload.svg +0 -1
  57. package/src/icons/cloud.svg +0 -1
  58. package/src/icons/columns-layout.svg +0 -1
  59. package/src/icons/command.svg +0 -1
  60. package/src/icons/cube.svg +0 -1
  61. package/src/icons/delete.svg +0 -3
  62. package/src/icons/dollar.svg +0 -3
  63. package/src/icons/download.svg +0 -1
  64. package/src/icons/draw.svg +0 -1
  65. package/src/icons/duplicate.svg +0 -3
  66. package/src/icons/ear.svg +0 -1
  67. package/src/icons/edit.svg +0 -1
  68. package/src/icons/exclamation-mark.svg +0 -1
  69. package/src/icons/eye-open.svg +0 -1
  70. package/src/icons/eye.svg +0 -1
  71. package/src/icons/file-html.svg +0 -1
  72. package/src/icons/file.svg +0 -3
  73. package/src/icons/finger.svg +0 -1
  74. package/src/icons/flag.svg +0 -1
  75. package/src/icons/folder.svg +0 -1
  76. package/src/icons/function.svg +0 -1
  77. package/src/icons/gear.svg +0 -1
  78. package/src/icons/gift.svg +0 -1
  79. package/src/icons/globe.svg +0 -3
  80. package/src/icons/grid.svg +0 -1
  81. package/src/icons/hammer.svg +0 -1
  82. package/src/icons/hand.svg +0 -1
  83. package/src/icons/hare.svg +0 -1
  84. package/src/icons/heart.svg +0 -3
  85. package/src/icons/home.svg +0 -3
  86. package/src/icons/image.svg +0 -1
  87. package/src/icons/inbox.svg +0 -3
  88. package/src/icons/info.svg +0 -1
  89. package/src/icons/key.svg +0 -1
  90. package/src/icons/lamp.svg +0 -1
  91. package/src/icons/link.svg +0 -1
  92. package/src/icons/location.svg +0 -1
  93. package/src/icons/locker.svg +0 -1
  94. package/src/icons/login.svg +0 -1
  95. package/src/icons/logout.svg +0 -3
  96. package/src/icons/mail.svg +0 -3
  97. package/src/icons/map.svg +0 -3
  98. package/src/icons/markup.svg +0 -1
  99. package/src/icons/merge.svg +0 -1
  100. package/src/icons/more-horizontal.svg +0 -5
  101. package/src/icons/more-vertical.svg +0 -5
  102. package/src/icons/mouse.svg +0 -1
  103. package/src/icons/music-mic.svg +0 -1
  104. package/src/icons/paintbrush.svg +0 -1
  105. package/src/icons/palette.svg +0 -1
  106. package/src/icons/password.svg +0 -1
  107. package/src/icons/pencil.svg +0 -1
  108. package/src/icons/people.svg +0 -3
  109. package/src/icons/percent.svg +0 -1
  110. package/src/icons/person-add.svg +0 -1
  111. package/src/icons/person-remove.svg +0 -1
  112. package/src/icons/person.svg +0 -4
  113. package/src/icons/phone.svg +0 -1
  114. package/src/icons/pin.svg +0 -1
  115. package/src/icons/question-circle.svg +0 -3
  116. package/src/icons/remove-circle.svg +0 -1
  117. package/src/icons/return-arrow.svg +0 -1
  118. package/src/icons/save.svg +0 -1
  119. package/src/icons/search.svg +0 -1
  120. package/src/icons/sections.svg +0 -1
  121. package/src/icons/send.svg +0 -1
  122. package/src/icons/share.svg +0 -1
  123. package/src/icons/shine.svg +0 -1
  124. package/src/icons/sliders.svg +0 -1
  125. package/src/icons/star.svg +0 -3
  126. package/src/icons/staroflife.svg +0 -1
  127. package/src/icons/storage.svg +0 -1
  128. package/src/icons/success-circle.svg +0 -3
  129. package/src/icons/swap.svg +0 -1
  130. package/src/icons/switch.svg +0 -1
  131. package/src/icons/sync.svg +0 -3
  132. package/src/icons/table.svg +0 -3
  133. package/src/icons/tag.svg +0 -3
  134. package/src/icons/terminal.svg +0 -1
  135. package/src/icons/text.svg +0 -1
  136. package/src/icons/thumb-down.svg +0 -1
  137. package/src/icons/thumb-up.svg +0 -1
  138. package/src/icons/timer.svg +0 -3
  139. package/src/icons/toggle.svg +0 -1
  140. package/src/icons/trash.svg +0 -1
  141. package/src/icons/tv-music.svg +0 -1
  142. package/src/icons/update-page.svg +0 -1
  143. package/src/icons/upload.svg +0 -1
  144. package/src/icons/video.svg +0 -1
  145. package/src/icons/wallet.svg +0 -1
  146. package/src/icons/wand-stars.svg +0 -1
  147. package/src/icons/waveform.svg +0 -1
  148. package/src/icons/window.svg +0 -1
  149. package/src/utils/iconRegistry.generated.mjs +0 -187
  150. package/src/utils/iconRegistry.mjs +0 -34
@@ -1,88 +1,209 @@
1
1
  /**
2
- * Sidebar Components (Next-Gen Core)
3
- *
4
- * Sidebar navigation component with items and selection state.
5
- *
6
- * Components:
7
- * - Sidebar: Sidebar navigation with headers and clickable items
2
+ * Sidebar navigation with selectable items and optional nested groups.
8
3
  *
9
4
  * Features:
10
- * - State binding for items and selected key
11
- * - Automatic state resolution (useState object or raw value)
5
+ * - State binding for items and selection key
12
6
  * - Headers support for grouping items
13
- * - Icon support for items
7
+ * - Recursive nested items with internal expand/collapse state
8
+ * - Parent items stay selectable while toggling their child groups
14
9
  */
15
- import Bunnix, { ForEach, useState, Show } from "@bunnix/core";
16
- import { withNormalizedArgs, withExtractedStyles, isStateLike } from "./utils.mjs";
17
- import { Column, Row } from "./layout.mjs";
10
+ import { Compute, Show, useEffect, useState } from "@bunnix/core";
11
+ import { withNormalizedArgs, withExtractedStyles, resolveCollectionState } from "./utils.mjs";
12
+ import { Column, Row, Spacer } from "./layout.mjs";
18
13
  import { Button } from "./buttons.mjs";
19
14
  import { Icon } from "./media.mjs";
20
- import { Heading } from "./typography.mjs";
15
+ import { Heading, Text } from "./typography.mjs";
21
16
 
22
- const SidebarCore = (props, ...children) => {
23
- // Resolve items (state or raw value)
24
- let itemsValue = props.items?.get && props.items?.set
25
- ? props.items
26
- : useState(props.items ?? []);
17
+ const resolveSelectionValue = (selection) => {
18
+ if (selection?.get) return selection.get();
19
+ return selection ?? "";
20
+ };
27
21
 
28
- // Resolve selected (state or raw value)
29
- let selectedValue = props.selected?.get && props.selected?.set
30
- ? props.selected
31
- : useState(props.selected ?? "");
22
+ const buildExpansionState = (items = [], currentState = {}, selectedKey = "") => {
23
+ const nextState = { ...currentState };
32
24
 
33
- delete props.items;
34
- delete props.selected;
25
+ for (const item of items) {
26
+ if (Array.isArray(item.children) && item.children.length > 0) {
27
+ if (!(item.key in nextState)) {
28
+ nextState[item.key] = item.key === selectedKey || (item.expanded ?? false);
29
+ }
30
+
31
+ Object.assign(nextState, buildExpansionState(item.children, nextState, selectedKey));
32
+ }
33
+ }
34
+
35
+ return nextState;
36
+ };
37
+
38
+ const hasSidebarItem = (items = [], key) => {
39
+ for (const item of items) {
40
+ if (item.key === key) return true;
41
+ if (Array.isArray(item.children) && hasSidebarItem(item.children, key)) return true;
42
+ }
43
+
44
+ return false;
45
+ };
46
+
47
+ const renderSidebarItem = ({
48
+ item,
49
+ index,
50
+ level,
51
+ selected,
52
+ expandedItems,
53
+ setSelection,
54
+ toggleExpanded,
55
+ }) => {
56
+ if (item.isHeader) {
57
+ return Heading(
58
+ {
59
+ h4: true,
60
+ color: "tertiary",
61
+ textSize: "1rem",
62
+ marginTop: index > 0 ? "regular" : 0,
63
+ ...(level > 0 ? { paddingLeft: level * 20 } : {}),
64
+ },
65
+ item.text,
66
+ );
67
+ }
68
+
69
+ const hasChildren = Array.isArray(item.children) && item.children.length > 0;
70
+ const isExpanded = !!expandedItems[item.key];
71
+ const isSelected = selected === item.key;
72
+
73
+ const button = Button(
74
+ {
75
+ variant: isSelected ? "primary" : "tertiary",
76
+ click: () => {
77
+ setSelection(item.key);
78
+ if (hasChildren) toggleExpanded(item.key);
79
+ },
80
+ },
81
+ Row(
82
+ {
83
+ fillWidth: true,
84
+ alignItems: "center",
85
+ gap: "small",
86
+ ...(level > 0 ? { paddingLeft: level * 20 } : {}),
87
+ },
88
+ ...(level === 0 && item.icon
89
+ ? [
90
+ Icon({
91
+ size: 18,
92
+ name: item.icon,
93
+ ...(isSelected ? {} : { color: "secondary" }),
94
+ }),
95
+ ]
96
+ : []),
97
+ Text({ weight: "heavy" }, item.text),
98
+ Spacer(),
99
+ ...(hasChildren
100
+ ? [
101
+ Icon({
102
+ name: isExpanded ? "chevron_down" : "chevron_right",
103
+ size: 16,
104
+ ...(isSelected ? {} : { color: "secondary" }),
105
+ }),
106
+ ]
107
+ : []),
108
+ ),
109
+ );
110
+
111
+ if (!hasChildren) return button;
35
112
 
36
113
  return Column(
37
- { ...props },
38
- ForEach(itemsValue, "key", (item) =>
39
- item.isHeader
40
- ? Heading(
41
- { h4: true, color: "tertiary", textSize: "1rem" },
42
- item.text,
43
- )
44
- : Show(selectedValue, (selected) =>
45
- Button(
46
- {
47
- variant: selected === item.key ? "primary" : "tertiary",
48
- click: () => selectedValue.set(item.key),
49
- },
50
- Row(
51
- { fillWidth: true, alignItems: "center" },
52
- item.icon && Icon({
53
- size: 20,
54
- name: item.icon,
55
- ...(selected !== item.key && { color: "secondary" })
56
- }),
57
- Row(item.text),
58
- ),
59
- )
114
+ { gap: 4 },
115
+ button,
116
+ ...(isExpanded
117
+ ? [
118
+ Column(
119
+ { gap: 4 },
120
+ ...item.children.map((child, childIndex) =>
121
+ renderSidebarItem({
122
+ item: child,
123
+ index: childIndex,
124
+ level: level + 1,
125
+ selected,
126
+ expandedItems,
127
+ setSelection,
128
+ toggleExpanded,
129
+ }),
130
+ ),
60
131
  ),
132
+ ]
133
+ : []),
134
+ );
135
+ };
136
+
137
+ const SidebarCore = (props, ...children) => {
138
+ const initialSelection = resolveSelectionValue(props.selection);
139
+ let itemsValue = resolveCollectionState(props.items, []);
140
+ let selectionValue = props.selection?.get && props.selection?.set
141
+ ? props.selection
142
+ : useState(props.selection ?? null);
143
+ let expandedItemsValue = useState(
144
+ buildExpansionState(itemsValue.get?.() ?? props.items ?? [], {}, initialSelection),
145
+ );
146
+ const sidebarState = Compute(
147
+ [itemsValue, expandedItemsValue, selectionValue],
148
+ (resolvedItems, expandedItems, selected) => ({
149
+ resolvedItems: resolvedItems ?? [],
150
+ expandedItems: expandedItems ?? {},
151
+ selected,
152
+ }),
153
+ );
154
+
155
+ useEffect((nextItems) => {
156
+ const currentState = expandedItemsValue.get() ?? {};
157
+ const resolvedSelection = resolveSelectionValue(selectionValue);
158
+
159
+ if (resolvedSelection && !hasSidebarItem(nextItems ?? [], resolvedSelection)) {
160
+ selectionValue.set && selectionValue.set(null);
161
+ }
162
+
163
+ const nextState = buildExpansionState(
164
+ nextItems ?? [],
165
+ currentState,
166
+ resolveSelectionValue(selectionValue),
167
+ );
168
+
169
+ if (JSON.stringify(currentState) !== JSON.stringify(nextState)) {
170
+ expandedItemsValue.set(nextState);
171
+ }
172
+ }, itemsValue);
173
+
174
+ delete props.items;
175
+ delete props.selection;
176
+
177
+ const setSelection = (key) => {
178
+ selectionValue.set && selectionValue.set(key);
179
+ };
180
+
181
+ const toggleExpanded = (key) => {
182
+ const currentState = expandedItemsValue.get() ?? {};
183
+ expandedItemsValue.set({
184
+ ...currentState,
185
+ [key]: !currentState[key],
186
+ });
187
+ };
188
+
189
+ return Show(sidebarState, ({ resolvedItems, expandedItems, selected }) =>
190
+ Column(
191
+ { ...props, gap: 4 },
192
+ ...resolvedItems.map((item, index) =>
193
+ renderSidebarItem({
194
+ item,
195
+ index,
196
+ level: 0,
197
+ selected,
198
+ expandedItems,
199
+ setSelection,
200
+ toggleExpanded,
201
+ }),
202
+ ),
61
203
  ),
62
204
  );
63
205
  };
64
206
 
65
- /**
66
- * Sidebar navigation component with item selection state.
67
- *
68
- * @param {Object} props - Component props
69
- * @param {Array<{key: string, text: string, icon?: string, isHeader?: boolean}>|*} props.items - Navigation items array or state object containing items
70
- * @param {string|*} props.selected - Currently selected item key or state object for selection
71
- * @param {string} [props.padding="regular"] - Padding size: "small" | "regular" | "large"
72
- * @param {string} [props.class] - Additional CSS classes
73
- * @param {...*} children - Child elements
74
- * @returns {*} Sidebar component
75
- *
76
- * @example
77
- * const selected = useState("home");
78
- * Sidebar({
79
- * items: [
80
- * { key: "home", text: "Home", icon: "home" },
81
- * { key: "settings", text: "Settings", icon: "settings", isHeader: true }
82
- * ],
83
- * selected: selected
84
- * })
85
- */
86
207
  export const Sidebar = withNormalizedArgs((props, ...children) =>
87
208
  withExtractedStyles((finalProps, ...children) =>
88
209
  SidebarCore(finalProps, ...children),
@@ -0,0 +1,51 @@
1
+ export function toSliderNumber(value, fallback = 0) {
2
+ const nextValue = Number(value);
3
+ return Number.isFinite(nextValue) ? nextValue : fallback;
4
+ }
5
+
6
+ export function isValidSliderSteps(steps) {
7
+ if (!Array.isArray(steps) || steps.length < 2) return false;
8
+
9
+ let previousValue = -Infinity;
10
+
11
+ for (const step of steps) {
12
+ const nextValue = toSliderNumber(step?.value, NaN);
13
+ if (!Number.isFinite(nextValue) || nextValue <= previousValue) return false;
14
+ previousValue = nextValue;
15
+ }
16
+
17
+ return true;
18
+ }
19
+
20
+ export function findNearestSliderStepIndex(steps, value) {
21
+ if (!isValidSliderSteps(steps)) return 0;
22
+
23
+ const targetValue = toSliderNumber(value, Number(steps[0].value));
24
+ let nearestIndex = 0;
25
+ let nearestDistance = Math.abs(Number(steps[0].value) - targetValue);
26
+
27
+ for (let index = 1; index < steps.length; index += 1) {
28
+ const distance = Math.abs(Number(steps[index].value) - targetValue);
29
+ if (distance < nearestDistance) {
30
+ nearestIndex = index;
31
+ nearestDistance = distance;
32
+ }
33
+ }
34
+
35
+ return nearestIndex;
36
+ }
37
+
38
+ export function getSliderStepValue(steps, index) {
39
+ if (!isValidSliderSteps(steps)) return 0;
40
+
41
+ const safeIndex = Math.min(
42
+ steps.length - 1,
43
+ Math.max(0, Math.round(toSliderNumber(index, 0))),
44
+ );
45
+
46
+ return Number(steps[safeIndex].value);
47
+ }
48
+
49
+ export function hasSliderStepLabels(steps) {
50
+ return Array.isArray(steps) && steps.some((step) => !!step?.label);
51
+ }
@@ -10,11 +10,13 @@
10
10
  .table thead td,
11
11
  .table th {
12
12
  font-weight: var(--font-weight-heavier);
13
+ height: 40px;
13
14
  padding: calc(var(--padding-md) * 0.75) var(--padding-md);
14
15
  border-bottom: 1px solid var(--color-border-primary);
15
16
  }
16
17
 
17
18
  .table td {
19
+ height: 40px;
18
20
  padding: calc(var(--padding-md) * 0.5) var(--padding-md);
19
21
  border-bottom: 1px solid var(--color-border-primary);
20
22
  }
@@ -22,3 +24,24 @@
22
24
  .table tr:last-child td {
23
25
  border-bottom: none;
24
26
  }
27
+
28
+ .table.table-alternate-rows thead td,
29
+ .table.table-alternate-rows tbody td,
30
+ .table.table-alternate-rows th {
31
+ border-bottom: none;
32
+ }
33
+
34
+ .table.table-alternate-rows thead td,
35
+ .table.table-alternate-rows thead th {
36
+ background-color: var(--color-bg-primary-dimmed);
37
+ }
38
+
39
+ .table.table-alternate-rows thead + tbody tr:nth-child(even) td,
40
+ .table.table-alternate-rows thead + tbody tr:nth-child(even) th {
41
+ background-color: var(--color-bg-primary-dimmed);
42
+ }
43
+
44
+ .table.table-alternate-rows.table-hide-headers tbody tr:nth-child(odd) td,
45
+ .table.table-alternate-rows.table-hide-headers tbody tr:nth-child(odd) th {
46
+ background-color: var(--color-bg-primary-dimmed);
47
+ }
@@ -12,39 +12,54 @@
12
12
  * - Column-based data mapping with headers array (content, key, size)
13
13
  * - Row rendering from data objects mapped to header keys
14
14
  */
15
- import Bunnix from "@bunnix/core";
16
- import { withNormalizedArgs, withExtractedStyles } from "./utils.mjs";
15
+ import Bunnix, { ForEach } from "@bunnix/core";
16
+ import { withNormalizedArgs, withExtractedStyles, resolveCollectionState } from "./utils.mjs";
17
17
 
18
18
  const { table, colgroup, col, thead, tbody, tr, td } = Bunnix;
19
19
 
20
20
  const TableCore = withNormalizedArgs((props, ...children) => {
21
21
  return withExtractedStyles((finalProps, ...children) => {
22
- let headers = finalProps.headers ?? [];
23
- let rows = finalProps.rows ?? [];
22
+ const headersValue = resolveCollectionState(finalProps.headers, []);
23
+ const rowsValue = resolveCollectionState(finalProps.rows, []);
24
+ let type = finalProps.type ?? "regular";
25
+ let border = finalProps.border;
26
+ let hideHeaders = finalProps.hideHeaders ?? false;
27
+ let renderCell = finalProps.renderCell;
24
28
 
25
29
  delete finalProps.headers;
26
30
  delete finalProps.rows;
27
-
28
- let tcols = headers.map((h) => col({ width: h.size ?? 0 }));
29
- let theaders = headers.map((h) => td(h.content ?? ""));
30
- let trows = rows.map((r) =>
31
- tr(
32
- headers.map((h) => {
33
- if (!h.key) return td("");
34
- if (!(h.key in r)) return td("");
35
- return td({ "data-label": h.content }, r[h.key]);
36
- }),
37
- ),
38
- );
31
+ delete finalProps.type;
32
+ delete finalProps.border;
33
+ delete finalProps.hideHeaders;
34
+ delete finalProps.renderCell;
39
35
 
40
36
  return table(
41
37
  {
42
38
  ...finalProps,
43
- class: `table ${finalProps.class || ""}`,
39
+ class: `table ${type !== "regular" ? `table-${type} ` : ""}${hideHeaders ? "table-hide-headers " : ""}${border ? `border-${border} ` : ""}${finalProps.class || ""}`.trim(),
44
40
  },
45
- colgroup(tcols),
46
- thead(theaders),
47
- tbody(trows),
41
+ colgroup(ForEach(headersValue, "key", (h) => col({ width: h.size ?? 0 }))),
42
+ ...(!hideHeaders
43
+ ? [thead(ForEach(headersValue, "key", (h) => td(h.content ?? "")))]
44
+ : []),
45
+ tbody(
46
+ ForEach(rowsValue, {}, (r, rowIndex) =>
47
+ tr(
48
+ ForEach(headersValue, "key", (h) => {
49
+ if (!h.key) return td("");
50
+ let renderedCell = renderCell?.(r, rowIndex, h.key);
51
+ if (renderedCell !== undefined) {
52
+ return td({ "data-label": h.content }, renderedCell);
53
+ }
54
+ if (!(h.key in r)) return td("");
55
+ return td(
56
+ { "data-label": h.content },
57
+ r[h.key],
58
+ );
59
+ }),
60
+ ),
61
+ ),
62
+ ),
48
63
  );
49
64
  })(props, ...children);
50
65
  });
@@ -0,0 +1,31 @@
1
+ export function resolveTextAreaLines(value, fallback = 3) {
2
+ const lines = Number(value);
3
+
4
+ if (!Number.isFinite(lines)) return fallback;
5
+
6
+ return Math.max(1, Math.floor(lines));
7
+ }
8
+
9
+ export function getTextAreaHeightMetrics({
10
+ lineHeight,
11
+ scrollHeight,
12
+ minLines,
13
+ maxLines,
14
+ verticalInset = 0,
15
+ }) {
16
+ const normalizedMinLines = resolveTextAreaLines(minLines, 3);
17
+ const normalizedMaxLines = Math.max(
18
+ normalizedMinLines,
19
+ resolveTextAreaLines(maxLines, 3),
20
+ );
21
+ const minHeight = (lineHeight * normalizedMinLines) + verticalInset;
22
+ const maxHeight = (lineHeight * normalizedMaxLines) + verticalInset;
23
+ const nextHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);
24
+
25
+ return {
26
+ minHeight,
27
+ maxHeight,
28
+ nextHeight,
29
+ shouldScroll: scrollHeight > maxHeight,
30
+ };
31
+ }
@@ -1,8 +1,32 @@
1
+ import { useState } from "@bunnix/core";
2
+
1
3
  export const isStateLike = (value) =>
2
4
  value &&
3
5
  typeof value.get === "function" &&
4
6
  typeof value.subscribe === "function";
5
7
 
8
+ export const resolveCollectionState = (value, fallback = []) =>
9
+ isStateLike(value) ? value : useState(value ?? fallback);
10
+
11
+ const resolvePadding = (value) => {
12
+ if (typeof value === "number") return `${value}px`;
13
+ if (value === "none") return "0";
14
+ if (value === "sm" || value === "small") return "var(--padding-sm)";
15
+ if (value === "md" || value === "regular") return "var(--padding-md)";
16
+ if (value === "lg" || value === "large") return "var(--padding-lg)";
17
+ return value; // pass-through for custom values (e.g. "8px", "1rem")
18
+ };
19
+
20
+ const resolveBorderRadius = (value) => {
21
+ if (typeof value === "number") return `${value}px`;
22
+ if (value === "none") return "0";
23
+ if (value === "md" || value === "regular") return "var(--radius-md)";
24
+ if (value === "lg" || value === "large") return "var(--radius-lg)";
25
+ if (value === "pill") return "9999px";
26
+ if (value === "circle") return "9999%";
27
+ return value;
28
+ };
29
+
6
30
  export function withNormalizedArgs(fn) {
7
31
  return (props = {}, ...children) => {
8
32
  const isProps =
@@ -51,6 +75,14 @@ export function withExtractedStyles(fn) {
51
75
  delete finalProps.textSize;
52
76
  }
53
77
 
78
+ if ("fontSize" in props) {
79
+ style.fontSize =
80
+ typeof props.fontSize === "number"
81
+ ? `${props.fontSize}px`
82
+ : props.fontSize;
83
+ delete finalProps.fontSize;
84
+ }
85
+
54
86
  if ("weight" in props) {
55
87
  if (typeof props.weight === "number") {
56
88
  style.fontWeight = props.weight;
@@ -70,8 +102,26 @@ export function withExtractedStyles(fn) {
70
102
  delete finalProps.overflow;
71
103
  }
72
104
 
105
+ if ("overflowX" in props) {
106
+ style.overflowX = props.overflowX;
107
+ delete finalProps.overflowX;
108
+ }
109
+
110
+ if ("overflowY" in props) {
111
+ style.overflowY = props.overflowY;
112
+ delete finalProps.overflowY;
113
+ }
114
+
73
115
  if ("bgColor" in props) {
74
116
  style.backgroundColor = props.bgColor;
117
+ if (props.bgColor === "primary") style.backgroundColor = "var(--color-bg-primary)";
118
+ if (props.bgColor === "primary-dimmed") style.backgroundColor = "var(--color-bg-primary-dimmed)";
119
+ if (props.bgColor === "secondary") style.backgroundColor = "var(--color-bg-secondary)";
120
+ if (props.bgColor === "success") style.backgroundColor = "var(--color-success)";
121
+ if (props.bgColor === "success-dimmed") style.backgroundColor = "var(--color-success-dimmed)";
122
+ if (props.bgColor === "warning") style.backgroundColor = "var(--color-warning)";
123
+ if (props.bgColor === "warning-dimmed") style.backgroundColor = "var(--color-warning-dimmed)";
124
+ if (props.bgColor === "danger") style.backgroundColor = "var(--color-danger)";
75
125
  delete finalProps.bgColor;
76
126
  }
77
127
 
@@ -141,6 +191,12 @@ export function withExtractedStyles(fn) {
141
191
  delete finalProps.size;
142
192
  }
143
193
 
194
+ if ("margin" in props) {
195
+ style.margin =
196
+ typeof props.margin === "number" ? `${props.margin}px` : props.margin;
197
+ delete finalProps.margin;
198
+ }
199
+
144
200
  if ("marginX" in props) {
145
201
  const marginXValue =
146
202
  typeof props.marginX === "number"
@@ -193,6 +249,50 @@ export function withExtractedStyles(fn) {
193
249
  delete finalProps.marginBottom;
194
250
  }
195
251
 
252
+ if ("padding" in props) {
253
+ style.padding = resolvePadding(props.padding);
254
+ delete finalProps.padding;
255
+ }
256
+
257
+ if ("paddingX" in props) {
258
+ const v = resolvePadding(props.paddingX);
259
+ style.paddingLeft = v;
260
+ style.paddingRight = v;
261
+ delete finalProps.paddingX;
262
+ }
263
+
264
+ if ("paddingY" in props) {
265
+ const v = resolvePadding(props.paddingY);
266
+ style.paddingTop = v;
267
+ style.paddingBottom = v;
268
+ delete finalProps.paddingY;
269
+ }
270
+
271
+ if ("paddingTop" in props) {
272
+ style.paddingTop = resolvePadding(props.paddingTop);
273
+ delete finalProps.paddingTop;
274
+ }
275
+
276
+ if ("paddingBottom" in props) {
277
+ style.paddingBottom = resolvePadding(props.paddingBottom);
278
+ delete finalProps.paddingBottom;
279
+ }
280
+
281
+ if ("paddingLeft" in props) {
282
+ style.paddingLeft = resolvePadding(props.paddingLeft);
283
+ delete finalProps.paddingLeft;
284
+ }
285
+
286
+ if ("paddingRight" in props) {
287
+ style.paddingRight = resolvePadding(props.paddingRight);
288
+ delete finalProps.paddingRight;
289
+ }
290
+
291
+ if ("borderRadius" in props) {
292
+ style.borderRadius = resolveBorderRadius(props.borderRadius);
293
+ delete finalProps.borderRadius;
294
+ }
295
+
196
296
  if ("top" in props) {
197
297
  style.top =
198
298
  typeof props.top === "number"
@@ -235,6 +335,11 @@ export function withExtractedStyles(fn) {
235
335
  delete finalProps.flexShrink;
236
336
  }
237
337
 
338
+ if ("zIndex" in props) {
339
+ style.zIndex = props.zIndex;
340
+ delete finalProps.zIndex;
341
+ }
342
+
238
343
  finalProps.style = style;
239
344
  return fn(finalProps, ...children);
240
345
  };
package/src/index.mjs CHANGED
@@ -19,7 +19,7 @@ export { Media, Icon, Spinner, Avatar } from "./core/media.mjs";
19
19
  export { Button, LinkButton } from "./core/buttons.mjs";
20
20
 
21
21
  // Inputs
22
- export { TextInput, Select, CheckBox } from "./core/inputs.mjs";
22
+ export { TextInput, TextArea, Select, CheckBox, Switch, SegmentedPicker, Slider } from "./core/inputs.mjs";
23
23
 
24
24
  // Data Display
25
25
  export { Table } from "./core/table.mjs";
@@ -27,7 +27,9 @@ export { Code } from "./core/code.mjs";
27
27
 
28
28
  // Navigation
29
29
  export { Sidebar } from "./core/sidebar.mjs";
30
+ export { Picker } from "./core/inputs.mjs";
30
31
  export { Menu } from "./core/menu.mjs";
32
+ export { Outline } from "./core/outline.mjs";
31
33
 
32
34
  // Feedback
33
35
  export { useDialog } from "./core/dialog.mjs";