@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.
- package/@types/index.d.ts +179 -15
- package/README.md +41 -4
- package/package.json +3 -8
- package/src/core/buttons.css +1 -0
- package/src/core/core.css +86 -4
- package/src/core/dialog.css +3 -1
- package/src/core/dialog.mjs +101 -16
- package/src/core/input.css +202 -0
- package/src/core/inputs.mjs +702 -23
- package/src/core/layout.mjs +1 -2
- package/src/core/media.css +36 -1
- package/src/core/media.mjs +13 -13
- package/src/core/menu.css +10 -29
- package/src/core/menu.mjs +159 -70
- package/src/core/outline.mjs +100 -0
- package/src/core/sidebar.mjs +189 -68
- package/src/core/sliderUtils.mjs +51 -0
- package/src/core/table.css +23 -0
- package/src/core/table.mjs +35 -20
- package/src/core/textareaUtils.mjs +31 -0
- package/src/core/utils.mjs +105 -0
- package/src/font-face/Framework7Icons-Regular.woff2 +0 -0
- package/src/index.mjs +3 -1
- package/src/core/core-mobile.css +0 -69
- package/src/icons/add-circle.svg +0 -1
- package/src/icons/add.svg +0 -1
- package/src/icons/alt.svg +0 -1
- package/src/icons/archive.svg +0 -1
- package/src/icons/arrow-down.svg +0 -1
- package/src/icons/arrow-left.svg +0 -1
- package/src/icons/arrow-right.svg +0 -1
- package/src/icons/arrow-up.svg +0 -1
- package/src/icons/at.svg +0 -1
- package/src/icons/attestation.svg +0 -1
- package/src/icons/battery-25.svg +0 -1
- package/src/icons/bell.svg +0 -3
- package/src/icons/bookmark.svg +0 -1
- package/src/icons/bot.svg +0 -1
- package/src/icons/bubble.svg +0 -1
- package/src/icons/building.svg +0 -3
- package/src/icons/button.svg +0 -1
- package/src/icons/calculate.svg +0 -1
- package/src/icons/calendar.svg +0 -1
- package/src/icons/captions-bubble.svg +0 -1
- package/src/icons/cart.svg +0 -1
- package/src/icons/chart.svg +0 -1
- package/src/icons/check.svg +0 -1
- package/src/icons/chevron-down.svg +0 -1
- package/src/icons/chevron-left.svg +0 -1
- package/src/icons/chevron-right.svg +0 -1
- package/src/icons/clip.svg +0 -1
- package/src/icons/clock.svg +0 -3
- package/src/icons/close-circle.svg +0 -3
- package/src/icons/close.svg +0 -1
- package/src/icons/cloud-download.svg +0 -1
- package/src/icons/cloud-upload.svg +0 -1
- package/src/icons/cloud.svg +0 -1
- package/src/icons/columns-layout.svg +0 -1
- package/src/icons/command.svg +0 -1
- package/src/icons/cube.svg +0 -1
- package/src/icons/delete.svg +0 -3
- package/src/icons/dollar.svg +0 -3
- package/src/icons/download.svg +0 -1
- package/src/icons/draw.svg +0 -1
- package/src/icons/duplicate.svg +0 -3
- package/src/icons/ear.svg +0 -1
- package/src/icons/edit.svg +0 -1
- package/src/icons/exclamation-mark.svg +0 -1
- package/src/icons/eye-open.svg +0 -1
- package/src/icons/eye.svg +0 -1
- package/src/icons/file-html.svg +0 -1
- package/src/icons/file.svg +0 -3
- package/src/icons/finger.svg +0 -1
- package/src/icons/flag.svg +0 -1
- package/src/icons/folder.svg +0 -1
- package/src/icons/function.svg +0 -1
- package/src/icons/gear.svg +0 -1
- package/src/icons/gift.svg +0 -1
- package/src/icons/globe.svg +0 -3
- package/src/icons/grid.svg +0 -1
- package/src/icons/hammer.svg +0 -1
- package/src/icons/hand.svg +0 -1
- package/src/icons/hare.svg +0 -1
- package/src/icons/heart.svg +0 -3
- package/src/icons/home.svg +0 -3
- package/src/icons/image.svg +0 -1
- package/src/icons/inbox.svg +0 -3
- package/src/icons/info.svg +0 -1
- package/src/icons/key.svg +0 -1
- package/src/icons/lamp.svg +0 -1
- package/src/icons/link.svg +0 -1
- package/src/icons/location.svg +0 -1
- package/src/icons/locker.svg +0 -1
- package/src/icons/login.svg +0 -1
- package/src/icons/logout.svg +0 -3
- package/src/icons/mail.svg +0 -3
- package/src/icons/map.svg +0 -3
- package/src/icons/markup.svg +0 -1
- package/src/icons/merge.svg +0 -1
- package/src/icons/more-horizontal.svg +0 -5
- package/src/icons/more-vertical.svg +0 -5
- package/src/icons/mouse.svg +0 -1
- package/src/icons/music-mic.svg +0 -1
- package/src/icons/paintbrush.svg +0 -1
- package/src/icons/palette.svg +0 -1
- package/src/icons/password.svg +0 -1
- package/src/icons/pencil.svg +0 -1
- package/src/icons/people.svg +0 -3
- package/src/icons/percent.svg +0 -1
- package/src/icons/person-add.svg +0 -1
- package/src/icons/person-remove.svg +0 -1
- package/src/icons/person.svg +0 -4
- package/src/icons/phone.svg +0 -1
- package/src/icons/pin.svg +0 -1
- package/src/icons/question-circle.svg +0 -3
- package/src/icons/remove-circle.svg +0 -1
- package/src/icons/return-arrow.svg +0 -1
- package/src/icons/save.svg +0 -1
- package/src/icons/search.svg +0 -1
- package/src/icons/sections.svg +0 -1
- package/src/icons/send.svg +0 -1
- package/src/icons/share.svg +0 -1
- package/src/icons/shine.svg +0 -1
- package/src/icons/sliders.svg +0 -1
- package/src/icons/star.svg +0 -3
- package/src/icons/staroflife.svg +0 -1
- package/src/icons/storage.svg +0 -1
- package/src/icons/success-circle.svg +0 -3
- package/src/icons/swap.svg +0 -1
- package/src/icons/switch.svg +0 -1
- package/src/icons/sync.svg +0 -3
- package/src/icons/table.svg +0 -3
- package/src/icons/tag.svg +0 -3
- package/src/icons/terminal.svg +0 -1
- package/src/icons/text.svg +0 -1
- package/src/icons/thumb-down.svg +0 -1
- package/src/icons/thumb-up.svg +0 -1
- package/src/icons/timer.svg +0 -3
- package/src/icons/toggle.svg +0 -1
- package/src/icons/trash.svg +0 -1
- package/src/icons/tv-music.svg +0 -1
- package/src/icons/update-page.svg +0 -1
- package/src/icons/upload.svg +0 -1
- package/src/icons/video.svg +0 -1
- package/src/icons/wallet.svg +0 -1
- package/src/icons/wand-stars.svg +0 -1
- package/src/icons/waveform.svg +0 -1
- package/src/icons/window.svg +0 -1
- package/src/utils/iconRegistry.generated.mjs +0 -187
- package/src/utils/iconRegistry.mjs +0 -34
package/src/core/sidebar.mjs
CHANGED
|
@@ -1,88 +1,209 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sidebar
|
|
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
|
|
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
|
-
* -
|
|
7
|
+
* - Recursive nested items with internal expand/collapse state
|
|
8
|
+
* - Parent items stay selectable while toggling their child groups
|
|
14
9
|
*/
|
|
15
|
-
import
|
|
16
|
-
import { withNormalizedArgs, withExtractedStyles,
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
: useState(props.items ?? []);
|
|
17
|
+
const resolveSelectionValue = (selection) => {
|
|
18
|
+
if (selection?.get) return selection.get();
|
|
19
|
+
return selection ?? "";
|
|
20
|
+
};
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
? props.selected
|
|
31
|
-
: useState(props.selected ?? "");
|
|
22
|
+
const buildExpansionState = (items = [], currentState = {}, selectedKey = "") => {
|
|
23
|
+
const nextState = { ...currentState };
|
|
32
24
|
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
}
|
package/src/core/table.css
CHANGED
|
@@ -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
|
+
}
|
package/src/core/table.mjs
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
}
|
package/src/core/utils.mjs
CHANGED
|
@@ -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
|
};
|
|
Binary file
|
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";
|