@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.
- package/@types/index.d.ts +134 -30
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/components/AccordionGroup.mjs +2 -1
- package/src/components/Badge.mjs +18 -4
- package/src/components/Button.mjs +7 -9
- package/src/components/Card.mjs +37 -0
- package/src/components/Checkbox.mjs +5 -7
- package/src/components/CodeBlock.mjs +31 -0
- package/src/components/ComboBox.mjs +22 -14
- package/src/components/Container.mjs +8 -10
- package/src/components/DatePicker.mjs +13 -15
- package/src/components/Dialog.mjs +35 -4
- package/src/components/DropdownMenu.mjs +16 -14
- package/src/components/HStack.mjs +11 -3
- package/src/components/Icon.mjs +9 -5
- package/src/components/InputField.mjs +12 -4
- package/src/components/NavigationBar.mjs +55 -25
- package/src/components/PageHeader.mjs +11 -8
- package/src/components/PageSection.mjs +20 -10
- package/src/components/PopoverMenu.mjs +94 -50
- package/src/components/RadioCheckbox.mjs +5 -7
- package/src/components/SearchBox.mjs +12 -21
- package/src/components/Sidebar.mjs +142 -67
- package/src/components/Table.mjs +145 -96
- package/src/components/Text.mjs +52 -21
- package/src/components/TimePicker.mjs +13 -15
- package/src/components/ToastNotification.mjs +16 -13
- package/src/components/ToggleSwitch.mjs +5 -7
- package/src/components/VStack.mjs +7 -6
- package/src/index.mjs +2 -0
- package/src/styles/buttons.css +8 -0
- package/src/styles/colors.css +8 -0
- package/src/styles/controls.css +61 -0
- package/src/styles/layout.css +64 -5
- package/src/styles/media.css +11 -0
- package/src/styles/menu.css +39 -21
- package/src/styles/table.css +2 -2
- package/src/styles/typography.css +25 -0
- package/src/styles/variables.css +3 -0
- package/src/utils/iconUtils.mjs +10 -0
- package/src/utils/sizeUtils.mjs +87 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Bunnix, { ForEach, useMemo, useRef, useState } from "@bunnix/core";
|
|
2
|
+
import { clampSize, toSizeToken } from "../utils/sizeUtils.mjs";
|
|
2
3
|
import Icon from "./Icon.mjs";
|
|
3
4
|
const { div, button, span, hr } = Bunnix;
|
|
4
5
|
|
|
@@ -51,7 +52,7 @@ export default function DatePicker({
|
|
|
51
52
|
placeholder,
|
|
52
53
|
range = false,
|
|
53
54
|
variant = "regular",
|
|
54
|
-
size = "
|
|
55
|
+
size = "regular",
|
|
55
56
|
class: className = ""
|
|
56
57
|
} = {}) {
|
|
57
58
|
const popoverRef = useRef(null);
|
|
@@ -161,25 +162,22 @@ export default function DatePicker({
|
|
|
161
162
|
|
|
162
163
|
const hasValue = inputValue.map(v => !!v);
|
|
163
164
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (value === "sm") return "md";
|
|
167
|
-
if (value === "lg" || value === "xl") return value;
|
|
168
|
-
return value;
|
|
169
|
-
};
|
|
165
|
+
// DatePicker does not support small size (clamps to regular)
|
|
166
|
+
const normalizeSize = (value) => clampSize(value, ["xsmall", "regular", "large", "xlarge"], "regular");
|
|
170
167
|
const normalizedSize = normalizeSize(size);
|
|
168
|
+
const sizeToken = toSizeToken(normalizedSize);
|
|
171
169
|
const variantClass = variant === "rounded" ? "rounded-full" : "";
|
|
172
|
-
const triggerSizeClass =
|
|
170
|
+
const triggerSizeClass = sizeToken === "xl"
|
|
173
171
|
? "dropdown-xl"
|
|
174
|
-
:
|
|
172
|
+
: sizeToken === "lg"
|
|
175
173
|
? "dropdown-lg"
|
|
176
174
|
: "";
|
|
177
|
-
const iconSizeValue = normalizedSize === "
|
|
178
|
-
? "
|
|
179
|
-
: normalizedSize === "
|
|
180
|
-
? "
|
|
181
|
-
: normalizedSize === "
|
|
182
|
-
? "
|
|
175
|
+
const iconSizeValue = normalizedSize === "small"
|
|
176
|
+
? "small"
|
|
177
|
+
: normalizedSize === "large"
|
|
178
|
+
? "large"
|
|
179
|
+
: normalizedSize === "xlarge"
|
|
180
|
+
? "xlarge"
|
|
183
181
|
: undefined;
|
|
184
182
|
|
|
185
183
|
return div({ class: `datepicker-wrapper ${className}`.trim() }, [
|
|
@@ -11,6 +11,8 @@ const defaultDialog = {
|
|
|
11
11
|
open: false,
|
|
12
12
|
title: "",
|
|
13
13
|
message: "",
|
|
14
|
+
minWidth: 400,
|
|
15
|
+
minHeight: null,
|
|
14
16
|
confirmation: {
|
|
15
17
|
text: "",
|
|
16
18
|
action: null,
|
|
@@ -26,11 +28,17 @@ const defaultDialog = {
|
|
|
26
28
|
|
|
27
29
|
export const dialogState = useState(defaultDialog);
|
|
28
30
|
|
|
29
|
-
export const showDialog = (
|
|
31
|
+
export const showDialog = (options = {}) => {
|
|
32
|
+
const { title, message, confirmation, content, minWidth, minHeight } = options;
|
|
33
|
+
const hasMinWidth = Object.prototype.hasOwnProperty.call(options, "minWidth");
|
|
34
|
+
const hasMinHeight = Object.prototype.hasOwnProperty.call(options, "minHeight");
|
|
35
|
+
|
|
30
36
|
dialogState.set({
|
|
31
37
|
open: true,
|
|
32
38
|
title: title ?? "",
|
|
33
39
|
message: message ?? "",
|
|
40
|
+
minWidth: hasMinWidth ? minWidth : defaultDialog.minWidth,
|
|
41
|
+
minHeight: hasMinHeight ? minHeight : defaultDialog.minHeight,
|
|
34
42
|
confirmation: {
|
|
35
43
|
text: confirmation?.text ?? defaultDialog.confirmation.text,
|
|
36
44
|
action: confirmation?.action ?? null,
|
|
@@ -51,6 +59,7 @@ export const hideDialog = () => {
|
|
|
51
59
|
|
|
52
60
|
export default function Dialog() {
|
|
53
61
|
const dialogRef = useRef(null);
|
|
62
|
+
const panelRef = useRef(null);
|
|
54
63
|
const setConfirmDisabled = (disabled) => {
|
|
55
64
|
const current = dialogState.get();
|
|
56
65
|
dialogState.set({
|
|
@@ -62,6 +71,13 @@ export default function Dialog() {
|
|
|
62
71
|
});
|
|
63
72
|
};
|
|
64
73
|
|
|
74
|
+
const resolveSizeValue = (size) => {
|
|
75
|
+
if (size == null || size === "") return "";
|
|
76
|
+
if (size === "auto") return "auto";
|
|
77
|
+
if (typeof size === "number") return `${size}px`;
|
|
78
|
+
return String(size);
|
|
79
|
+
};
|
|
80
|
+
|
|
65
81
|
useEffect((value) => {
|
|
66
82
|
const element = dialogRef.current;
|
|
67
83
|
if (!element) return;
|
|
@@ -73,6 +89,14 @@ export default function Dialog() {
|
|
|
73
89
|
} else if (element.open) {
|
|
74
90
|
element.close();
|
|
75
91
|
}
|
|
92
|
+
|
|
93
|
+
const panel = panelRef.current;
|
|
94
|
+
if (panel) {
|
|
95
|
+
const minWidth = resolveSizeValue(value?.minWidth);
|
|
96
|
+
const minHeight = resolveSizeValue(value?.minHeight);
|
|
97
|
+
panel.style.minWidth = minWidth || "";
|
|
98
|
+
panel.style.minHeight = minHeight || "";
|
|
99
|
+
}
|
|
76
100
|
}, [dialogState]);
|
|
77
101
|
|
|
78
102
|
const confirmationText = dialogState.map((value) => value.confirmation?.text ?? defaultDialog.confirmation.text);
|
|
@@ -109,7 +133,11 @@ export default function Dialog() {
|
|
|
109
133
|
hideDialog();
|
|
110
134
|
}
|
|
111
135
|
}, [
|
|
112
|
-
VStack({
|
|
136
|
+
VStack({
|
|
137
|
+
ref: panelRef,
|
|
138
|
+
gap: "regular",
|
|
139
|
+
class: "box-capsule dialog-panel shadow bg-base p-lg items-stretch dialog-appear"
|
|
140
|
+
}, [
|
|
113
141
|
HStack({ alignment: "leading", gap: "small", class: "items-center w-full" }, [
|
|
114
142
|
Text({ type: "heading4", class: "no-margin" }, dialogState.map((value) => value.title)),
|
|
115
143
|
div({ class: "spacer-h" }),
|
|
@@ -127,7 +155,10 @@ export default function Dialog() {
|
|
|
127
155
|
Show(showContent, () => {
|
|
128
156
|
const current = dialogState.get();
|
|
129
157
|
if (typeof current.content !== "function") return null;
|
|
130
|
-
return div(
|
|
158
|
+
return div(
|
|
159
|
+
{ class: "column-container gap-sm w-full h-full flex-1" },
|
|
160
|
+
current.content({ setConfirmDisabled })
|
|
161
|
+
);
|
|
131
162
|
}),
|
|
132
163
|
HStack({ alignment: "trailing", gap: "regular", class: "w-full" }, [
|
|
133
164
|
Show(showExtra, () => Button({
|
|
@@ -156,7 +187,7 @@ export default function Dialog() {
|
|
|
156
187
|
}, [
|
|
157
188
|
confirmationText,
|
|
158
189
|
kbd({ class: "text-white text-sm whitespace-nowrap" }, [
|
|
159
|
-
Icon({ name: "return-arrow", fill: "white", size: "
|
|
190
|
+
Icon({ name: "return-arrow", fill: "white", size: "xsmall" }),
|
|
160
191
|
"Enter"
|
|
161
192
|
])
|
|
162
193
|
]))
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import Bunnix, { useRef, useState, useMemo } from "@bunnix/core";
|
|
2
|
+
import { clampSize, toSizeToken } from "../utils/sizeUtils.mjs";
|
|
3
|
+
import { resolveIconClass } from "../utils/iconUtils.mjs";
|
|
2
4
|
const { div, button, hr, span } = Bunnix;
|
|
3
5
|
|
|
4
6
|
let dropdownCounter = 0;
|
|
@@ -12,13 +14,10 @@ export default function DropdownMenu({
|
|
|
12
14
|
onSelect,
|
|
13
15
|
class: className = ""
|
|
14
16
|
}) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (value === "sm") return "sm";
|
|
18
|
-
if (value === "lg" || value === "xl") return value;
|
|
19
|
-
return value;
|
|
20
|
-
};
|
|
17
|
+
// DropdownMenu supports all sizes
|
|
18
|
+
const normalizeSize = (value) => clampSize(value, ["xsmall", "small", "regular", "large", "xlarge"], "regular");
|
|
21
19
|
const normalizedSize = normalizeSize(size);
|
|
20
|
+
const sizeToken = toSizeToken(normalizedSize);
|
|
22
21
|
const popoverRef = useRef(null);
|
|
23
22
|
const dropdownId = id || `dropdown-instance-${++dropdownCounter}`;
|
|
24
23
|
const anchorName = `--${dropdownId}`;
|
|
@@ -48,13 +47,13 @@ export default function DropdownMenu({
|
|
|
48
47
|
}
|
|
49
48
|
};
|
|
50
49
|
|
|
51
|
-
const sizeClass =
|
|
52
|
-
const itemSizeClass =
|
|
53
|
-
const iconSizeClass =
|
|
50
|
+
const sizeClass = sizeToken === "lg" ? "dropdown-lg" : sizeToken === "xl" ? "dropdown-xl" : "";
|
|
51
|
+
const itemSizeClass = sizeToken === "lg" ? "btn-lg" : sizeToken === "xl" ? "btn-xl" : "";
|
|
52
|
+
const iconSizeClass = sizeToken === "sm"
|
|
54
53
|
? "icon-sm"
|
|
55
|
-
:
|
|
54
|
+
: sizeToken === "lg"
|
|
56
55
|
? "icon-lg"
|
|
57
|
-
:
|
|
56
|
+
: sizeToken === "xl"
|
|
58
57
|
? "icon-xl"
|
|
59
58
|
: "";
|
|
60
59
|
|
|
@@ -68,9 +67,10 @@ export default function DropdownMenu({
|
|
|
68
67
|
// Reactive Icon: stable element, reactive class
|
|
69
68
|
span({
|
|
70
69
|
class: selectedItem.map(s => {
|
|
71
|
-
|
|
70
|
+
const resolvedIcon = resolveIconClass(s?.icon);
|
|
71
|
+
if (!resolvedIcon) return "hidden";
|
|
72
72
|
const tint = s.destructive ? "bg-destructive" : "bg-primary";
|
|
73
|
-
return `icon ${iconSizeClass} ${
|
|
73
|
+
return `icon ${iconSizeClass} ${resolvedIcon} ${tint}`.trim();
|
|
74
74
|
})
|
|
75
75
|
}),
|
|
76
76
|
// Reactive Title: text with dimmed style when empty
|
|
@@ -97,8 +97,10 @@ export default function DropdownMenu({
|
|
|
97
97
|
}, [
|
|
98
98
|
span({
|
|
99
99
|
class: isCurrent.map(active => {
|
|
100
|
+
const resolvedIcon = resolveIconClass(item.icon);
|
|
101
|
+
if (!resolvedIcon) return "hidden";
|
|
100
102
|
const tint = active ? "bg-white" : item.destructive ? "bg-destructive" : "bg-primary";
|
|
101
|
-
return `icon ${iconSizeClass} ${
|
|
103
|
+
return `icon ${iconSizeClass} ${resolvedIcon} ${tint}`.trim();
|
|
102
104
|
})
|
|
103
105
|
}),
|
|
104
106
|
item.title
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Bunnix from "@bunnix/core";
|
|
2
2
|
const { div } = Bunnix;
|
|
3
3
|
|
|
4
|
-
export default function HStack(props = {}, children) {
|
|
4
|
+
export default function HStack(props = {}, ...children) {
|
|
5
5
|
if (props === null || props === undefined || Array.isArray(props) || typeof props !== "object") {
|
|
6
6
|
children = props;
|
|
7
7
|
props = {};
|
|
@@ -9,6 +9,7 @@ export default function HStack(props = {}, children) {
|
|
|
9
9
|
|
|
10
10
|
const {
|
|
11
11
|
alignment = "leading",
|
|
12
|
+
verticalAlignment = "center",
|
|
12
13
|
gap = "regular",
|
|
13
14
|
class: className = "",
|
|
14
15
|
...rest
|
|
@@ -19,16 +20,23 @@ export default function HStack(props = {}, children) {
|
|
|
19
20
|
trailing: "justify-end"
|
|
20
21
|
};
|
|
21
22
|
|
|
23
|
+
const verticalAlignmentMap = {
|
|
24
|
+
top: "items-start",
|
|
25
|
+
center: "items-center",
|
|
26
|
+
bottom: "items-end"
|
|
27
|
+
};
|
|
28
|
+
|
|
22
29
|
const gapMap = {
|
|
30
|
+
xsmall: "gap-xs",
|
|
23
31
|
small: "gap-sm",
|
|
24
32
|
regular: "gap-md",
|
|
25
33
|
large: "gap-lg"
|
|
26
34
|
};
|
|
27
35
|
|
|
28
|
-
const combinedClass = `row-container ${alignmentMap[alignment]} ${gapMap[gap]} ${className}`.trim();
|
|
36
|
+
const combinedClass = `row-container ${alignmentMap[alignment]} ${verticalAlignmentMap[verticalAlignment]} ${gapMap[gap]} ${className}`.trim();
|
|
29
37
|
|
|
30
38
|
return div({
|
|
31
39
|
class: combinedClass,
|
|
32
40
|
...rest
|
|
33
|
-
}, children);
|
|
41
|
+
}, ...children);
|
|
34
42
|
}
|
package/src/components/Icon.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import Bunnix from "@bunnix/core";
|
|
2
|
+
import { normalizeSize, toSizeToken } from "../utils/sizeUtils.mjs";
|
|
2
3
|
const { span } = Bunnix;
|
|
3
4
|
|
|
4
5
|
export default function Icon({
|
|
5
6
|
name,
|
|
6
|
-
fill = "
|
|
7
|
+
fill = "default",
|
|
7
8
|
size,
|
|
8
9
|
class: className = "",
|
|
9
10
|
...rest
|
|
@@ -16,12 +17,15 @@ export default function Icon({
|
|
|
16
17
|
// fill mapping: "base" -> "icon-base", "white" -> "icon-white", etc.
|
|
17
18
|
const fillClass = fill.startsWith("icon-") ? fill : `icon-${fill}`;
|
|
18
19
|
|
|
19
|
-
const
|
|
20
|
-
if (!value
|
|
20
|
+
const normalizeSizeClass = (value) => {
|
|
21
|
+
if (!value) return "";
|
|
21
22
|
if (typeof value === "string" && value.startsWith("icon-")) return value;
|
|
22
|
-
|
|
23
|
+
const normalized = normalizeSize(value);
|
|
24
|
+
if (normalized === "regular") return "";
|
|
25
|
+
const sizeToken = toSizeToken(normalized);
|
|
26
|
+
return sizeToken ? `icon-${sizeToken}` : "";
|
|
23
27
|
};
|
|
24
|
-
const sizeClass =
|
|
28
|
+
const sizeClass = normalizeSizeClass(size);
|
|
25
29
|
|
|
26
30
|
const combinedClass = `icon ${iconName} ${fillClass} ${sizeClass} ${className}`.trim();
|
|
27
31
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import Bunnix, { useRef, useEffect } from "@bunnix/core";
|
|
2
|
+
import { clampSize, toSizeToken } from "../utils/sizeUtils.mjs";
|
|
2
3
|
const { div, label, input: inputEl, datalist, option, span } = Bunnix;
|
|
3
4
|
|
|
4
5
|
export default function InputField({
|
|
5
6
|
class: className = "",
|
|
6
7
|
type = "text",
|
|
7
8
|
variant = "regular",
|
|
9
|
+
size,
|
|
8
10
|
value,
|
|
9
11
|
placeholder,
|
|
10
12
|
label: labelText,
|
|
@@ -25,6 +27,12 @@ export default function InputField({
|
|
|
25
27
|
const inputRef = useRef(null);
|
|
26
28
|
const listId = suggestions.length > 0 ? `list-${Math.random().toString(36).slice(2, 8)}` : null;
|
|
27
29
|
|
|
30
|
+
// InputField supports regular, large, xlarge (no xsmall, small)
|
|
31
|
+
const normalizeSize = (value) => clampSize(value, ["regular", "large", "xlarge"], "regular");
|
|
32
|
+
const normalizedSize = normalizeSize(size);
|
|
33
|
+
const sizeToken = toSizeToken(normalizedSize);
|
|
34
|
+
const sizeClass = sizeToken === "xl" ? "input-xl" : sizeToken === "lg" ? "input-lg" : "";
|
|
35
|
+
|
|
28
36
|
useEffect((el) => {
|
|
29
37
|
if (el && listId) {
|
|
30
38
|
el.setAttribute('list', listId);
|
|
@@ -32,7 +40,7 @@ export default function InputField({
|
|
|
32
40
|
}, inputRef);
|
|
33
41
|
|
|
34
42
|
const variantClass = variant === "rounded" ? "rounded-full" : "";
|
|
35
|
-
const combinedClass = `${className} ${variantClass}`.trim();
|
|
43
|
+
const combinedClass = `${className} ${sizeClass} ${variantClass}`.trim();
|
|
36
44
|
|
|
37
45
|
const handleInput = onInput ?? input;
|
|
38
46
|
const handleChange = onChange ?? change;
|
|
@@ -55,15 +63,15 @@ export default function InputField({
|
|
|
55
63
|
...rest
|
|
56
64
|
});
|
|
57
65
|
|
|
58
|
-
const
|
|
66
|
+
const iconSizeClass = sizeToken === "xl"
|
|
59
67
|
? "icon-xl"
|
|
60
|
-
:
|
|
68
|
+
: sizeToken === "lg"
|
|
61
69
|
? "icon-lg"
|
|
62
70
|
: "";
|
|
63
71
|
|
|
64
72
|
const inputBlock = type === "search"
|
|
65
73
|
? div({ class: "input-search w-full" }, [
|
|
66
|
-
span({ class: `icon icon-search icon-quaternary ${
|
|
74
|
+
span({ class: `icon icon-search icon-quaternary ${iconSizeClass}`.trim() }),
|
|
67
75
|
inputElement
|
|
68
76
|
])
|
|
69
77
|
: inputElement;
|
|
@@ -3,7 +3,20 @@ import SearchBox from "./SearchBox.mjs";
|
|
|
3
3
|
|
|
4
4
|
const { nav, div, h2 } = Bunnix;
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const resolveNode = (node) => {
|
|
7
|
+
if (node && typeof node.map === "function") {
|
|
8
|
+
return node.map((value) => resolveNode(value));
|
|
9
|
+
}
|
|
10
|
+
if (node && typeof node.get === "function") {
|
|
11
|
+
return resolveNode(node.get());
|
|
12
|
+
}
|
|
13
|
+
return typeof node === "function" ? node() : node;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const isState = (value) =>
|
|
17
|
+
value &&
|
|
18
|
+
typeof value.get === "function" &&
|
|
19
|
+
typeof value.subscribe === "function";
|
|
7
20
|
|
|
8
21
|
export default function NavigationBar({
|
|
9
22
|
title,
|
|
@@ -18,30 +31,47 @@ export default function NavigationBar({
|
|
|
18
31
|
class: className = "",
|
|
19
32
|
...rest
|
|
20
33
|
} = {}) {
|
|
21
|
-
const
|
|
22
|
-
?
|
|
23
|
-
|
|
34
|
+
const titleContent = isState(title)
|
|
35
|
+
? title.map((value) => {
|
|
36
|
+
if (value === null || value === undefined) return "";
|
|
37
|
+
return typeof value === "string" || typeof value === "number"
|
|
38
|
+
? String(value)
|
|
39
|
+
: value;
|
|
40
|
+
})
|
|
41
|
+
: title === null || title === undefined
|
|
42
|
+
? ""
|
|
43
|
+
: typeof title === "string" || typeof title === "number"
|
|
44
|
+
? String(title)
|
|
45
|
+
: title;
|
|
24
46
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
47
|
+
const titleNode = h2({ class: "whitespace-nowrap" }, titleContent);
|
|
48
|
+
const leadingNode = resolveNode(leading);
|
|
49
|
+
const trailingNode = resolveNode(trailing);
|
|
27
50
|
|
|
28
|
-
return nav(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
return nav(
|
|
52
|
+
{
|
|
53
|
+
class:
|
|
54
|
+
`navigation-bar row-container items-center gap-md w-full sticky-top bg-base ${className}`.trim(),
|
|
55
|
+
...rest,
|
|
56
|
+
},
|
|
57
|
+
[
|
|
58
|
+
title ? titleNode : null,
|
|
59
|
+
leadingNode &&
|
|
60
|
+
div({ class: "shrink-0 row-container items-center" }, leadingNode),
|
|
61
|
+
div({ class: "w-full" }),
|
|
62
|
+
trailingNode &&
|
|
63
|
+
div({ class: "shrink-0 row-container items-center" }, trailingNode),
|
|
64
|
+
searchable &&
|
|
65
|
+
SearchBox({
|
|
66
|
+
data: searchData,
|
|
67
|
+
value: searchValue,
|
|
68
|
+
onInput: onSearchInput,
|
|
69
|
+
onSelect: onSearchSelect,
|
|
70
|
+
placeholder: "Search components",
|
|
71
|
+
variant: "rounded",
|
|
72
|
+
class: "w-300",
|
|
73
|
+
...searchProps,
|
|
74
|
+
}),
|
|
75
|
+
],
|
|
76
|
+
);
|
|
47
77
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import Bunnix from "@bunnix/core";
|
|
2
|
-
const { div,
|
|
2
|
+
const { div, h2, p } = Bunnix;
|
|
3
|
+
|
|
4
|
+
export default function PageHeader({ title, description, trailing } = {}) {
|
|
5
|
+
const trailingContent = typeof trailing === "function" ? trailing() : trailing;
|
|
3
6
|
|
|
4
|
-
export default function PageHeader({ title, description } = {}) {
|
|
5
7
|
return div({
|
|
6
|
-
class: "row-container no-margin"
|
|
8
|
+
class: "row-container items-center gap-md no-margin"
|
|
7
9
|
}, [
|
|
8
|
-
div({ class: "column-container gap-xs flex-1" }, [
|
|
9
|
-
|
|
10
|
-
p({ class: "text-secondary no-margin" }, description)
|
|
11
|
-
])
|
|
12
|
-
|
|
10
|
+
div({ class: "column-container gap-xs flex-1 w-full" }, [
|
|
11
|
+
h2({ class: "no-margin" }, title),
|
|
12
|
+
description ? p({ class: "text-secondary no-margin" }, description) : null
|
|
13
|
+
]),
|
|
14
|
+
trailingContent ? div({ class: "shrink-0" }, trailingContent) : null
|
|
15
|
+
].filter(Boolean));
|
|
13
16
|
}
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import Bunnix from "@bunnix/core";
|
|
2
2
|
const { div, h5 } = Bunnix;
|
|
3
3
|
|
|
4
|
-
export default function PageSection(
|
|
4
|
+
export default function PageSection(
|
|
5
|
+
{ title, stickyOffset = 0, gap = "regular", trailing } = {},
|
|
6
|
+
children,
|
|
7
|
+
) {
|
|
5
8
|
const gapMap = {
|
|
6
9
|
small: "gap-sm",
|
|
7
10
|
regular: "gap-md",
|
|
8
|
-
large: "gap-lg"
|
|
11
|
+
large: "gap-lg",
|
|
9
12
|
};
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const trailingContent =
|
|
15
|
+
typeof trailing === "function" ? trailing() : trailing;
|
|
16
|
+
|
|
17
|
+
return div({ class: "column-container no-margin w-full" }, [
|
|
18
|
+
div(
|
|
19
|
+
{
|
|
20
|
+
class: "row-container items-center gap-md pt-sm pb-sm",
|
|
21
|
+
style: ` --sticky-offset: ${stickyOffset}`,
|
|
22
|
+
},
|
|
23
|
+
[
|
|
24
|
+
h5({ class: "no-margin select-none flex-1 w-full" }, title),
|
|
25
|
+
trailingContent ? div({ class: "shrink-0" }, trailingContent) : null,
|
|
26
|
+
].filter(Boolean),
|
|
27
|
+
),
|
|
28
|
+
div({ class: `column-container py-xs ${gapMap[gap]}`.trim() }, children),
|
|
19
29
|
]);
|
|
20
30
|
}
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import Bunnix, { useRef } from "@bunnix/core";
|
|
2
|
+
import { resolveIconClass } from "../utils/iconUtils.mjs";
|
|
3
|
+
import { clampSize, toSizeToken } from "../utils/sizeUtils.mjs";
|
|
2
4
|
const { div, button, hr, span } = Bunnix;
|
|
3
5
|
|
|
4
6
|
let menuCounter = 0;
|
|
5
7
|
|
|
6
|
-
export default function PopoverMenu(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
};
|
|
8
|
+
export default function PopoverMenu(
|
|
9
|
+
{
|
|
10
|
+
trigger,
|
|
11
|
+
menuItems = [],
|
|
12
|
+
id,
|
|
13
|
+
align = "left",
|
|
14
|
+
size,
|
|
15
|
+
onSelect,
|
|
16
|
+
class: className = "",
|
|
17
|
+
} = {},
|
|
18
|
+
children,
|
|
19
|
+
) {
|
|
20
|
+
const normalizeSize = (value) =>
|
|
21
|
+
clampSize(value, ["xsmall", "small", "regular", "large", "xlarge"], "regular");
|
|
21
22
|
const normalizedSize = normalizeSize(size);
|
|
23
|
+
const sizeToken = toSizeToken(normalizedSize);
|
|
22
24
|
const popoverRef = useRef(null);
|
|
23
25
|
|
|
24
26
|
const menuId = id || `menu-instance-${++menuCounter}`;
|
|
@@ -44,44 +46,86 @@ export default function PopoverMenu({
|
|
|
44
46
|
}
|
|
45
47
|
};
|
|
46
48
|
|
|
47
|
-
const sizeClass =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
: normalizedSize === "xl"
|
|
53
|
-
? "icon-xl"
|
|
49
|
+
const sizeClass =
|
|
50
|
+
sizeToken === "lg"
|
|
51
|
+
? "btn-lg"
|
|
52
|
+
: sizeToken === "xl"
|
|
53
|
+
? "btn-xl"
|
|
54
54
|
: "";
|
|
55
|
+
const iconSizeClass =
|
|
56
|
+
sizeToken === "sm"
|
|
57
|
+
? "icon-sm"
|
|
58
|
+
: sizeToken === "lg"
|
|
59
|
+
? "icon-lg"
|
|
60
|
+
: sizeToken === "xl"
|
|
61
|
+
? "icon-xl"
|
|
62
|
+
: "";
|
|
63
|
+
|
|
64
|
+
const triggerProps = {
|
|
65
|
+
id: menuId,
|
|
66
|
+
type: 'button',
|
|
67
|
+
style: `anchor-name: ${anchorName}`,
|
|
68
|
+
class: `btn btn-flat ${sizeClass} ${className}`.trim(),
|
|
69
|
+
click: handleToggle,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Determine what to use as trigger:
|
|
73
|
+
// 1. If children are provided, use children as trigger content
|
|
74
|
+
// 2. Otherwise use trigger prop
|
|
75
|
+
const hasTriggerContent =
|
|
76
|
+
children !== undefined &&
|
|
77
|
+
children !== null &&
|
|
78
|
+
(Array.isArray(children) ? children.length > 0 : true);
|
|
79
|
+
|
|
80
|
+
const resolvedTrigger = hasTriggerContent ? children : trigger;
|
|
81
|
+
|
|
82
|
+
// If trigger is a function, call it to get the content, then wrap in button
|
|
83
|
+
// Otherwise use the trigger/children directly as content
|
|
84
|
+
const triggerContent =
|
|
85
|
+
typeof resolvedTrigger === "function" ? resolvedTrigger() : resolvedTrigger;
|
|
86
|
+
|
|
87
|
+
const triggerElement = button(triggerProps, triggerContent);
|
|
55
88
|
|
|
56
89
|
return div({ class: "menu-wrapper" }, [
|
|
57
|
-
|
|
58
|
-
id: menuId,
|
|
59
|
-
style: `anchor-name: ${anchorName}`,
|
|
60
|
-
class: `btn btn-flat ${sizeClass} ${className}`.trim(),
|
|
61
|
-
click: handleToggle
|
|
62
|
-
}, trigger),
|
|
90
|
+
triggerElement,
|
|
63
91
|
|
|
64
|
-
div(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
div(
|
|
93
|
+
{
|
|
94
|
+
ref: popoverRef,
|
|
95
|
+
popover: "auto",
|
|
96
|
+
class: `menu-popover popover-base menu-anchor-${align}`,
|
|
97
|
+
style: `--anchor-id: ${anchorName}`,
|
|
98
|
+
},
|
|
99
|
+
[
|
|
100
|
+
/* All design system styles go here to avoid overriding popover visibility */
|
|
101
|
+
div(
|
|
102
|
+
{
|
|
103
|
+
class: "card column-container shadow gap-sm w-min-150 p-sm bg-base",
|
|
104
|
+
},
|
|
105
|
+
menuItems.map((item) => {
|
|
106
|
+
if (item.isSeparator) {
|
|
107
|
+
return hr({ class: "no-margin" });
|
|
108
|
+
}
|
|
109
|
+
return button(
|
|
110
|
+
{
|
|
111
|
+
class: `btn btn-flat justify-start w-full ${sizeClass}`.trim(),
|
|
112
|
+
click: () => handleItemClick(item),
|
|
113
|
+
},
|
|
114
|
+
[
|
|
115
|
+
item.icon
|
|
116
|
+
? span({
|
|
117
|
+
class: (() => {
|
|
118
|
+
const resolvedIcon = resolveIconClass(item.icon);
|
|
119
|
+
return `icon ${iconSizeClass} ${resolvedIcon} ${item.destructive ? "bg-destructive" : "bg-primary"}`.trim();
|
|
120
|
+
})(),
|
|
121
|
+
})
|
|
122
|
+
: null,
|
|
123
|
+
item.title,
|
|
124
|
+
],
|
|
125
|
+
);
|
|
126
|
+
}),
|
|
127
|
+
),
|
|
128
|
+
],
|
|
129
|
+
),
|
|
86
130
|
]);
|
|
87
131
|
}
|