@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.
- package/@types/index.d.ts +269 -0
- package/LICENSE +7 -0
- package/README.md +184 -0
- package/package.json +53 -0
- package/src/components/AccordionGroup.mjs +37 -0
- package/src/components/Badge.mjs +49 -0
- package/src/components/Button.mjs +76 -0
- package/src/components/Checkbox.mjs +36 -0
- package/src/components/ComboBox.mjs +44 -0
- package/src/components/Container.mjs +27 -0
- package/src/components/DatePicker.mjs +251 -0
- package/src/components/Dialog.mjs +166 -0
- package/src/components/DropdownMenu.mjs +110 -0
- package/src/components/Grid.mjs +40 -0
- package/src/components/HStack.mjs +34 -0
- package/src/components/Icon.mjs +32 -0
- package/src/components/InputField.mjs +78 -0
- package/src/components/NavigationBar.mjs +47 -0
- package/src/components/PageHeader.mjs +13 -0
- package/src/components/PageSection.mjs +20 -0
- package/src/components/PopoverMenu.mjs +87 -0
- package/src/components/RadioCheckbox.mjs +36 -0
- package/src/components/SearchBox.mjs +207 -0
- package/src/components/Sidebar.mjs +187 -0
- package/src/components/Table.mjs +254 -0
- package/src/components/Text.mjs +38 -0
- package/src/components/TimePicker.mjs +172 -0
- package/src/components/ToastNotification.mjs +105 -0
- package/src/components/ToggleSwitch.mjs +26 -0
- package/src/components/VStack.mjs +35 -0
- package/src/icons/add-circle.svg +1 -0
- package/src/icons/add.svg +1 -0
- package/src/icons/alt.svg +1 -0
- package/src/icons/archive.svg +1 -0
- package/src/icons/at.svg +1 -0
- package/src/icons/attestation.svg +1 -0
- package/src/icons/bell.svg +4 -0
- package/src/icons/bookmark.svg +1 -0
- package/src/icons/bot.svg +1 -0
- package/src/icons/button.svg +1 -0
- package/src/icons/calculate.svg +1 -0
- package/src/icons/calendar.svg +1 -0
- package/src/icons/chart.svg +1 -0
- package/src/icons/check.svg +1 -0
- package/src/icons/chevron-down.svg +1 -0
- package/src/icons/chevron-left.svg +1 -0
- package/src/icons/chevron-right.svg +1 -0
- package/src/icons/clip.svg +1 -0
- package/src/icons/clock.svg +4 -0
- package/src/icons/close-circle.svg +4 -0
- package/src/icons/close.svg +1 -0
- package/src/icons/cloud-download.svg +1 -0
- package/src/icons/cloud-upload.svg +1 -0
- package/src/icons/cloud.svg +1 -0
- package/src/icons/columns-layout.svg +1 -0
- package/src/icons/command.svg +1 -0
- package/src/icons/cube.svg +1 -0
- package/src/icons/delete.svg +4 -0
- package/src/icons/dollar.svg +4 -0
- package/src/icons/download.svg +1 -0
- package/src/icons/draw.svg +1 -0
- package/src/icons/duplicate.svg +4 -0
- package/src/icons/edit.svg +1 -0
- package/src/icons/exclamation-mark.svg +1 -0
- package/src/icons/eye-open.svg +1 -0
- package/src/icons/eye.svg +1 -0
- package/src/icons/file-html.svg +1 -0
- package/src/icons/file.svg +4 -0
- package/src/icons/finger.svg +1 -0
- package/src/icons/flag.svg +1 -0
- package/src/icons/folder.svg +1 -0
- package/src/icons/function.svg +1 -0
- package/src/icons/gear.svg +1 -0
- package/src/icons/gift.svg +1 -0
- package/src/icons/globe.svg +4 -0
- package/src/icons/grid.svg +1 -0
- package/src/icons/hand.svg +1 -0
- package/src/icons/heart.svg +4 -0
- package/src/icons/home.svg +4 -0
- package/src/icons/image.svg +1 -0
- package/src/icons/inbox.svg +4 -0
- package/src/icons/info.svg +1 -0
- package/src/icons/key.svg +1 -0
- package/src/icons/lamp.svg +1 -0
- package/src/icons/link.svg +1 -0
- package/src/icons/location.svg +1 -0
- package/src/icons/locker.svg +1 -0
- package/src/icons/login.svg +1 -0
- package/src/icons/logout.svg +4 -0
- package/src/icons/mail.svg +4 -0
- package/src/icons/map.svg +4 -0
- package/src/icons/markup.svg +1 -0
- package/src/icons/merge.svg +1 -0
- package/src/icons/more-horizontal.svg +5 -0
- package/src/icons/more-vertical.svg +5 -0
- package/src/icons/mouse.svg +1 -0
- package/src/icons/palette.svg +1 -0
- package/src/icons/password.svg +1 -0
- package/src/icons/pencil.svg +1 -0
- package/src/icons/people.svg +4 -0
- package/src/icons/person-add.svg +1 -0
- package/src/icons/person-remove.svg +1 -0
- package/src/icons/person.svg +5 -0
- package/src/icons/pin.svg +1 -0
- package/src/icons/question-circle.svg +4 -0
- package/src/icons/remove-circle.svg +1 -0
- package/src/icons/return-arrow.svg +2 -0
- package/src/icons/save.svg +1 -0
- package/src/icons/search.svg +1 -0
- package/src/icons/sections.svg +1 -0
- package/src/icons/send.svg +1 -0
- package/src/icons/share.svg +1 -0
- package/src/icons/shine.svg +1 -0
- package/src/icons/sliders.svg +1 -0
- package/src/icons/star.svg +4 -0
- package/src/icons/storage.svg +1 -0
- package/src/icons/success-circle.svg +4 -0
- package/src/icons/swap.svg +1 -0
- package/src/icons/switch.svg +1 -0
- package/src/icons/sync.svg +4 -0
- package/src/icons/table.svg +4 -0
- package/src/icons/tag.svg +4 -0
- package/src/icons/terminal.svg +1 -0
- package/src/icons/text.svg +1 -0
- package/src/icons/thumb-down.svg +1 -0
- package/src/icons/thumb-up.svg +1 -0
- package/src/icons/timer.svg +4 -0
- package/src/icons/toggle.svg +1 -0
- package/src/icons/trash.svg +1 -0
- package/src/icons/update-page.svg +1 -0
- package/src/icons/upload.svg +1 -0
- package/src/icons/video.svg +1 -0
- package/src/icons/wallet.svg +1 -0
- package/src/icons/window.svg +1 -0
- package/src/index.mjs +29 -0
- package/src/styles/accordion.css +70 -0
- package/src/styles/buttons.css +118 -0
- package/src/styles/colors.css +131 -0
- package/src/styles/controls.css +504 -0
- package/src/styles/datepicker.css +140 -0
- package/src/styles/interactable.css +16 -0
- package/src/styles/layout.css +444 -0
- package/src/styles/links.css +38 -0
- package/src/styles/main.css +16 -0
- package/src/styles/media.css +155 -0
- package/src/styles/menu.css +168 -0
- package/src/styles/motion.css +66 -0
- package/src/styles/table.css +78 -0
- package/src/styles/timepicker.css +87 -0
- package/src/styles/typography.css +94 -0
- package/src/styles/variables.css +218 -0
- package/src/styles.css +1 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Bunnix from "@bunnix/core";
|
|
2
|
+
const { label, input, span } = Bunnix;
|
|
3
|
+
|
|
4
|
+
export default function Checkbox({
|
|
5
|
+
labelText,
|
|
6
|
+
size,
|
|
7
|
+
onCheck,
|
|
8
|
+
check,
|
|
9
|
+
onChange,
|
|
10
|
+
class: className = "",
|
|
11
|
+
...inputProps
|
|
12
|
+
}) {
|
|
13
|
+
const normalizeSize = (value) => {
|
|
14
|
+
if (!value || value === "default" || value === "regular" || value === "md") return "md";
|
|
15
|
+
if (value === "sm") return "sm";
|
|
16
|
+
if (value === "lg" || value === "xl") return value;
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
const normalizedSize = normalizeSize(size);
|
|
20
|
+
const sizeClass = normalizedSize === "lg" ? "checkbox-lg" : normalizedSize === "xl" ? "checkbox-xl" : "";
|
|
21
|
+
const nativeChange = onChange ?? inputProps.change;
|
|
22
|
+
const checkHandler = onCheck ?? check;
|
|
23
|
+
|
|
24
|
+
return label({ class: `selection-control ${className}`.trim() }, [
|
|
25
|
+
input({
|
|
26
|
+
type: "checkbox",
|
|
27
|
+
class: sizeClass,
|
|
28
|
+
...inputProps,
|
|
29
|
+
change: (e) => {
|
|
30
|
+
if (nativeChange) nativeChange(e);
|
|
31
|
+
if (checkHandler) checkHandler(e.target.checked);
|
|
32
|
+
}
|
|
33
|
+
}),
|
|
34
|
+
labelText ? span({ class: "text-base" }, labelText) : null
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import Bunnix from "@bunnix/core";
|
|
2
|
+
const { select, option } = Bunnix;
|
|
3
|
+
|
|
4
|
+
export default function ComboBox({
|
|
5
|
+
options = [],
|
|
6
|
+
selection,
|
|
7
|
+
size,
|
|
8
|
+
class: className = "",
|
|
9
|
+
onChange,
|
|
10
|
+
change,
|
|
11
|
+
...rest
|
|
12
|
+
} = {}, children) {
|
|
13
|
+
const normalizeSize = (value) => {
|
|
14
|
+
if (!value || value === "default" || value === "regular" || value === "md") return "md";
|
|
15
|
+
if (value === "sm") return "sm";
|
|
16
|
+
if (value === "lg" || value === "xl") return value;
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
const normalizedSize = normalizeSize(size);
|
|
20
|
+
const sizeClass = normalizedSize === "lg" ? "input-lg" : normalizedSize === "xl" ? "input-xl" : "";
|
|
21
|
+
const selectionState = selection && typeof selection.map === "function" ? selection : null;
|
|
22
|
+
const handleChangeExternal = onChange ?? change;
|
|
23
|
+
|
|
24
|
+
const handleChange = (event) => {
|
|
25
|
+
if (selectionState) {
|
|
26
|
+
selectionState.set(event.target.value);
|
|
27
|
+
}
|
|
28
|
+
if (handleChangeExternal) handleChangeExternal(event);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const resolvedChildren = children ?? options.map((opt) => {
|
|
32
|
+
if (typeof opt === "string") {
|
|
33
|
+
return option({ value: opt }, opt);
|
|
34
|
+
}
|
|
35
|
+
return option({ value: opt.value }, opt.label ?? opt.value);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return select({
|
|
39
|
+
class: `${sizeClass} ${className}`.trim(),
|
|
40
|
+
value: selection ?? "",
|
|
41
|
+
change: handleChange,
|
|
42
|
+
...rest
|
|
43
|
+
}, resolvedChildren);
|
|
44
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Bunnix from "@bunnix/core";
|
|
2
|
+
|
|
3
|
+
const { div } = Bunnix;
|
|
4
|
+
|
|
5
|
+
const typeClassMap = {
|
|
6
|
+
main: "main-container",
|
|
7
|
+
content: "main-content",
|
|
8
|
+
page: "page-layout"
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const directionClassMap = {
|
|
12
|
+
horizontal: "row-container",
|
|
13
|
+
vertical: "column-container"
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function Container({
|
|
17
|
+
type,
|
|
18
|
+
direction,
|
|
19
|
+
class: className = "",
|
|
20
|
+
...rest
|
|
21
|
+
} = {}, children) {
|
|
22
|
+
const typeClass = typeClassMap[type] || "";
|
|
23
|
+
const directionClass = directionClassMap[direction] || "";
|
|
24
|
+
const combinedClass = `${typeClass} ${directionClass} ${className}`.trim();
|
|
25
|
+
|
|
26
|
+
return div({ class: combinedClass, ...rest }, children);
|
|
27
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import Bunnix, { ForEach, useMemo, useRef, useState } from "@bunnix/core";
|
|
2
|
+
import Icon from "./Icon.mjs";
|
|
3
|
+
const { div, button, span, hr } = Bunnix;
|
|
4
|
+
|
|
5
|
+
const WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
6
|
+
|
|
7
|
+
const isSameDay = (a, b) =>
|
|
8
|
+
a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
9
|
+
|
|
10
|
+
const toMidnight = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
11
|
+
|
|
12
|
+
const formatDate = (date, formatter) => (date ? formatter.format(date) : "");
|
|
13
|
+
|
|
14
|
+
const buildCalendar = (viewDate) => {
|
|
15
|
+
if (!viewDate) return [];
|
|
16
|
+
const year = viewDate.getFullYear();
|
|
17
|
+
const month = viewDate.getMonth();
|
|
18
|
+
const startOfMonth = new Date(year, month, 1);
|
|
19
|
+
const startDay = startOfMonth.getDay();
|
|
20
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
21
|
+
const daysInPrev = new Date(year, month, 0).getDate();
|
|
22
|
+
|
|
23
|
+
const cells = [];
|
|
24
|
+
for (let i = 0; i < 42; i += 1) {
|
|
25
|
+
let day;
|
|
26
|
+
let cellMonth = month;
|
|
27
|
+
let outside = false;
|
|
28
|
+
|
|
29
|
+
if (i < startDay) {
|
|
30
|
+
day = daysInPrev - startDay + i + 1;
|
|
31
|
+
cellMonth = month - 1;
|
|
32
|
+
outside = true;
|
|
33
|
+
} else if (i >= startDay + daysInMonth) {
|
|
34
|
+
day = i - (startDay + daysInMonth) + 1;
|
|
35
|
+
cellMonth = month + 1;
|
|
36
|
+
outside = true;
|
|
37
|
+
} else {
|
|
38
|
+
day = i - startDay + 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cellDate = new Date(year, cellMonth, day);
|
|
42
|
+
const key = `${cellDate.getFullYear()}-${cellDate.getMonth()}-${cellDate.getDate()}-${outside ? "o" : "i"}`;
|
|
43
|
+
cells.push({ key, date: cellDate, outside });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return cells;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default function DatePicker({
|
|
50
|
+
id,
|
|
51
|
+
placeholder,
|
|
52
|
+
range = false,
|
|
53
|
+
variant = "regular",
|
|
54
|
+
size = "md",
|
|
55
|
+
class: className = ""
|
|
56
|
+
} = {}) {
|
|
57
|
+
const popoverRef = useRef(null);
|
|
58
|
+
const pickerId = id || `datepicker-${Math.random().toString(36).slice(2, 8)}`;
|
|
59
|
+
const anchorName = `--${pickerId}`;
|
|
60
|
+
|
|
61
|
+
const formatter = useMemo([], () => {
|
|
62
|
+
return new Intl.DateTimeFormat(undefined, { day: "2-digit", month: "2-digit", year: "numeric" });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const selectedStart = useState(null);
|
|
66
|
+
const selectedEnd = useState(null);
|
|
67
|
+
const inputValue = useState("");
|
|
68
|
+
|
|
69
|
+
const viewDate = useState(new Date());
|
|
70
|
+
|
|
71
|
+
const calendar = useMemo([viewDate], (value) => buildCalendar(value));
|
|
72
|
+
const monthLabel = useMemo([viewDate], (value) => {
|
|
73
|
+
try {
|
|
74
|
+
return new Intl.DateTimeFormat(undefined, { month: "long", year: "numeric" }).format(value);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return new Intl.DateTimeFormat(undefined, { month: "short", year: "numeric" }).format(value);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const openPopover = () => {
|
|
81
|
+
const popover = popoverRef.current;
|
|
82
|
+
if (popover && !popover.matches(":popover-open")) {
|
|
83
|
+
popover.showPopover();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const closePopover = () => {
|
|
88
|
+
const popover = popoverRef.current;
|
|
89
|
+
if (popover && popover.matches(":popover-open")) {
|
|
90
|
+
popover.hidePopover();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const setSelection = (start, end = null) => {
|
|
95
|
+
selectedStart.set(start);
|
|
96
|
+
selectedEnd.set(end);
|
|
97
|
+
if (range) {
|
|
98
|
+
const startText = formatDate(start, formatter.get());
|
|
99
|
+
const endText = end ? formatDate(end, formatter.get()) : "";
|
|
100
|
+
inputValue.set(endText ? `${startText} - ${endText}` : startText);
|
|
101
|
+
} else {
|
|
102
|
+
inputValue.set(formatDate(start, formatter.get()));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleDayClick = (date) => {
|
|
107
|
+
if (range) {
|
|
108
|
+
const start = selectedStart.get();
|
|
109
|
+
const end = selectedEnd.get();
|
|
110
|
+
if (!start || end) {
|
|
111
|
+
setSelection(date, null);
|
|
112
|
+
} else if (date < start) {
|
|
113
|
+
setSelection(date, start);
|
|
114
|
+
} else {
|
|
115
|
+
setSelection(start, date);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
setSelection(date, null);
|
|
119
|
+
closePopover();
|
|
120
|
+
}
|
|
121
|
+
viewDate.set(new Date(date.getFullYear(), date.getMonth(), 1));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handlePrevMonth = () => {
|
|
125
|
+
const current = viewDate.get();
|
|
126
|
+
viewDate.set(new Date(current.getFullYear(), current.getMonth() - 1, 1));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const handleNextMonth = () => {
|
|
130
|
+
const current = viewDate.get();
|
|
131
|
+
viewDate.set(new Date(current.getFullYear(), current.getMonth() + 1, 1));
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const handleToday = () => {
|
|
135
|
+
const today = toMidnight(new Date());
|
|
136
|
+
setSelection(today, null);
|
|
137
|
+
viewDate.set(new Date(today.getFullYear(), today.getMonth(), 1));
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleClear = () => {
|
|
141
|
+
selectedStart.set(null);
|
|
142
|
+
selectedEnd.set(null);
|
|
143
|
+
inputValue.set("");
|
|
144
|
+
closePopover();
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Single reactive source for the label text
|
|
148
|
+
const displayLabel = inputValue.map(v => {
|
|
149
|
+
if (v) return v;
|
|
150
|
+
if (placeholder) return placeholder;
|
|
151
|
+
|
|
152
|
+
const today = new Date();
|
|
153
|
+
const fmt = formatter.get();
|
|
154
|
+
if (range) {
|
|
155
|
+
const tomorrow = new Date(today);
|
|
156
|
+
tomorrow.setDate(today.getDate() + 1);
|
|
157
|
+
return `${formatDate(today, fmt)} - ${formatDate(tomorrow, fmt)}`;
|
|
158
|
+
}
|
|
159
|
+
return formatDate(today, fmt);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const hasValue = inputValue.map(v => !!v);
|
|
163
|
+
|
|
164
|
+
const normalizeSize = (value) => {
|
|
165
|
+
if (!value || value === "default" || value === "regular" || value === "md") return "md";
|
|
166
|
+
if (value === "sm") return "md";
|
|
167
|
+
if (value === "lg" || value === "xl") return value;
|
|
168
|
+
return value;
|
|
169
|
+
};
|
|
170
|
+
const normalizedSize = normalizeSize(size);
|
|
171
|
+
const variantClass = variant === "rounded" ? "rounded-full" : "";
|
|
172
|
+
const triggerSizeClass = normalizedSize === "xl"
|
|
173
|
+
? "dropdown-xl"
|
|
174
|
+
: normalizedSize === "lg"
|
|
175
|
+
? "dropdown-lg"
|
|
176
|
+
: "";
|
|
177
|
+
const iconSizeValue = normalizedSize === "sm"
|
|
178
|
+
? "sm"
|
|
179
|
+
: normalizedSize === "lg"
|
|
180
|
+
? "lg"
|
|
181
|
+
: normalizedSize === "xl"
|
|
182
|
+
? "xl"
|
|
183
|
+
: undefined;
|
|
184
|
+
|
|
185
|
+
return div({ class: `datepicker-wrapper ${className}`.trim() }, [
|
|
186
|
+
button({
|
|
187
|
+
id: pickerId,
|
|
188
|
+
class: `dropdown-trigger datepicker-trigger justify-start ${variantClass} ${triggerSizeClass} no-chevron`.trim(),
|
|
189
|
+
style: `anchor-name: ${anchorName}`,
|
|
190
|
+
click: openPopover
|
|
191
|
+
}, [
|
|
192
|
+
span({ class: hasValue.map(h => h ? "" : "text-tertiary") }, displayLabel),
|
|
193
|
+
Icon({ name: "calendar", fill: "quaternary", size: iconSizeValue, class: "ml-auto" })
|
|
194
|
+
]),
|
|
195
|
+
div({
|
|
196
|
+
ref: popoverRef,
|
|
197
|
+
popover: "auto",
|
|
198
|
+
class: "datepicker-popover popover-base",
|
|
199
|
+
style: `--anchor-id: ${anchorName}`
|
|
200
|
+
}, [
|
|
201
|
+
div({ class: "card column-container shadow gap-0 p-0 bg-base datepicker-card" }, [
|
|
202
|
+
div({ class: "row-container items-center justify-between datepicker-header p-sm no-margin" }, [
|
|
203
|
+
button({ class: "btn btn-flat datepicker-nav", click: handlePrevMonth }, [
|
|
204
|
+
span({ class: "icon icon-chevron-left icon-base" })
|
|
205
|
+
]),
|
|
206
|
+
span({ class: "datepicker-title" }, monthLabel),
|
|
207
|
+
button({ class: "btn btn-flat datepicker-nav", click: handleNextMonth }, [
|
|
208
|
+
span({ class: "icon icon-chevron-right icon-base" })
|
|
209
|
+
])
|
|
210
|
+
]),
|
|
211
|
+
div({ class: "datepicker-body" }, [
|
|
212
|
+
div({ class: "datepicker-weekdays" }, WEEKDAYS.map((day) =>
|
|
213
|
+
span({ class: "datepicker-weekday" }, day)
|
|
214
|
+
)),
|
|
215
|
+
div({ class: "datepicker-grid" },
|
|
216
|
+
ForEach(calendar, "key", (cell) => {
|
|
217
|
+
const start = selectedStart.get();
|
|
218
|
+
const end = selectedEnd.get();
|
|
219
|
+
const inRange = range && start && end && cell.date >= start && cell.date <= end;
|
|
220
|
+
const isStart = range && start && isSameDay(cell.date, start);
|
|
221
|
+
const isEnd = range && end && isSameDay(cell.date, end);
|
|
222
|
+
const isSelected = !range && start && isSameDay(cell.date, start);
|
|
223
|
+
const isToday = isSameDay(cell.date, toMidnight(new Date()));
|
|
224
|
+
|
|
225
|
+
const classNames = [
|
|
226
|
+
"datepicker-cell",
|
|
227
|
+
cell.outside ? "is-outside" : "",
|
|
228
|
+
inRange ? "is-in-range" : "",
|
|
229
|
+
isStart ? "is-range-start" : "",
|
|
230
|
+
isEnd ? "is-range-end" : "",
|
|
231
|
+
isSelected ? "is-selected" : "",
|
|
232
|
+
isToday ? "is-today" : ""
|
|
233
|
+
].filter(Boolean).join(" ");
|
|
234
|
+
|
|
235
|
+
return button({
|
|
236
|
+
class: classNames,
|
|
237
|
+
click: () => handleDayClick(cell.date)
|
|
238
|
+
}, cell.date.getDate().toString());
|
|
239
|
+
})
|
|
240
|
+
)
|
|
241
|
+
]),
|
|
242
|
+
hr({ class: "no-margin" }),
|
|
243
|
+
div({ class: "row-container justify-center items-center gap-md p-base shrink-0 datepicker-footer" }, [
|
|
244
|
+
button({ class: "btn btn-flat", click: handleClear }, "Clear"),
|
|
245
|
+
button({ class: "btn btn-flat", click: handleToday }, "Today"),
|
|
246
|
+
button({ class: "btn", click: closePopover }, "OK")
|
|
247
|
+
])
|
|
248
|
+
])
|
|
249
|
+
])
|
|
250
|
+
]);
|
|
251
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import Bunnix, { useEffect, useRef, useState, Show } from "@bunnix/core";
|
|
2
|
+
import VStack from "./VStack.mjs";
|
|
3
|
+
import HStack from "./HStack.mjs";
|
|
4
|
+
import Button from "./Button.mjs";
|
|
5
|
+
import Text from "./Text.mjs";
|
|
6
|
+
import Icon from "./Icon.mjs";
|
|
7
|
+
|
|
8
|
+
const { div, dialog, kbd, hr } = Bunnix;
|
|
9
|
+
|
|
10
|
+
const defaultDialog = {
|
|
11
|
+
open: false,
|
|
12
|
+
title: "",
|
|
13
|
+
message: "",
|
|
14
|
+
confirmation: {
|
|
15
|
+
text: "",
|
|
16
|
+
action: null,
|
|
17
|
+
variant: "regular",
|
|
18
|
+
disabled: false
|
|
19
|
+
},
|
|
20
|
+
extra: {
|
|
21
|
+
text: "",
|
|
22
|
+
action: null
|
|
23
|
+
},
|
|
24
|
+
content: null
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const dialogState = useState(defaultDialog);
|
|
28
|
+
|
|
29
|
+
export const showDialog = ({ title, message, confirmation, content } = {}) => {
|
|
30
|
+
dialogState.set({
|
|
31
|
+
open: true,
|
|
32
|
+
title: title ?? "",
|
|
33
|
+
message: message ?? "",
|
|
34
|
+
confirmation: {
|
|
35
|
+
text: confirmation?.text ?? defaultDialog.confirmation.text,
|
|
36
|
+
action: confirmation?.action ?? null,
|
|
37
|
+
variant: confirmation?.variant ?? defaultDialog.confirmation.variant,
|
|
38
|
+
disabled: confirmation?.disabled ?? defaultDialog.confirmation.disabled
|
|
39
|
+
},
|
|
40
|
+
extra: {
|
|
41
|
+
text: confirmation?.extra?.text ?? defaultDialog.extra.text,
|
|
42
|
+
action: confirmation?.extra?.action ?? null
|
|
43
|
+
},
|
|
44
|
+
content: content ?? null
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const hideDialog = () => {
|
|
49
|
+
dialogState.set({ ...dialogState.get(), open: false });
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default function Dialog() {
|
|
53
|
+
const dialogRef = useRef(null);
|
|
54
|
+
const setConfirmDisabled = (disabled) => {
|
|
55
|
+
const current = dialogState.get();
|
|
56
|
+
dialogState.set({
|
|
57
|
+
...current,
|
|
58
|
+
confirmation: {
|
|
59
|
+
...current.confirmation,
|
|
60
|
+
disabled: !!disabled
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
useEffect((value) => {
|
|
66
|
+
const element = dialogRef.current;
|
|
67
|
+
if (!element) return;
|
|
68
|
+
|
|
69
|
+
if (value.open) {
|
|
70
|
+
if (!element.open) {
|
|
71
|
+
element.showModal();
|
|
72
|
+
}
|
|
73
|
+
} else if (element.open) {
|
|
74
|
+
element.close();
|
|
75
|
+
}
|
|
76
|
+
}, [dialogState]);
|
|
77
|
+
|
|
78
|
+
const confirmationText = dialogState.map((value) => value.confirmation?.text ?? defaultDialog.confirmation.text);
|
|
79
|
+
const confirmationVariant = dialogState.map((value) => value.confirmation?.variant ?? defaultDialog.confirmation.variant);
|
|
80
|
+
const confirmationDisabled = dialogState.map((value) => !!value.confirmation?.disabled);
|
|
81
|
+
const showConfirmation = dialogState.map((value) => !!value.confirmation?.text);
|
|
82
|
+
const extraText = dialogState.map((value) => value.extra?.text ?? "");
|
|
83
|
+
const showExtra = dialogState.map((value) => !!value.extra?.text);
|
|
84
|
+
const showContent = dialogState.map((value) => typeof value.content === "function");
|
|
85
|
+
const showMessage = dialogState.map((value) => !!value.message && typeof value.content !== "function");
|
|
86
|
+
const showBody = dialogState.map((value) => !!value.message || typeof value.content === "function");
|
|
87
|
+
|
|
88
|
+
return dialog({
|
|
89
|
+
ref: dialogRef,
|
|
90
|
+
class: "dialog-base dialog-backdrop fixed inset-0 w-full h-full row-container items-center justify-center",
|
|
91
|
+
cancel: () => {
|
|
92
|
+
hideDialog();
|
|
93
|
+
},
|
|
94
|
+
keydown: (event) => {
|
|
95
|
+
if (event?.key !== "Enter") return;
|
|
96
|
+
const current = dialogState.get();
|
|
97
|
+
const hasConfirmation = !!current?.confirmation?.text;
|
|
98
|
+
const isDisabled = !!current?.confirmation?.disabled;
|
|
99
|
+
if (!hasConfirmation || isDisabled) {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
event.stopPropagation();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
const action = current?.confirmation?.action;
|
|
106
|
+
if (typeof action === "function") {
|
|
107
|
+
action();
|
|
108
|
+
}
|
|
109
|
+
hideDialog();
|
|
110
|
+
}
|
|
111
|
+
}, [
|
|
112
|
+
VStack({ gap: "regular", class: "box-capsule shadow bg-base w-full max-w-400 p-lg items-stretch dialog-appear" }, [
|
|
113
|
+
HStack({ alignment: "leading", gap: "small", class: "items-center w-full" }, [
|
|
114
|
+
Text({ type: "heading4", class: "no-margin" }, dialogState.map((value) => value.title)),
|
|
115
|
+
div({ class: "spacer-h" }),
|
|
116
|
+
Button({
|
|
117
|
+
variant: "flat",
|
|
118
|
+
class: "p-xs",
|
|
119
|
+
click: hideDialog
|
|
120
|
+
}, Icon({ name: "close" }))
|
|
121
|
+
]),
|
|
122
|
+
Text({
|
|
123
|
+
type: "paragraph",
|
|
124
|
+
color: "secondary",
|
|
125
|
+
class: showMessage.map((value) => `whitespace-pre-line ${value ? "" : "hidden"}`.trim())
|
|
126
|
+
}, dialogState.map((value) => value.message)),
|
|
127
|
+
Show(showContent, () => {
|
|
128
|
+
const current = dialogState.get();
|
|
129
|
+
if (typeof current.content !== "function") return null;
|
|
130
|
+
return div({ class: "column-container gap-sm" }, current.content({ setConfirmDisabled }));
|
|
131
|
+
}),
|
|
132
|
+
HStack({ alignment: "trailing", gap: "regular", class: "w-full" }, [
|
|
133
|
+
Show(showExtra, () => Button({
|
|
134
|
+
variant: "flat",
|
|
135
|
+
click: () => {
|
|
136
|
+
const current = dialogState.get();
|
|
137
|
+
const action = current.extra?.action;
|
|
138
|
+
if (typeof action === "function") {
|
|
139
|
+
action();
|
|
140
|
+
}
|
|
141
|
+
hideDialog();
|
|
142
|
+
}
|
|
143
|
+
}, extraText)),
|
|
144
|
+
Show(showConfirmation, () => Button({
|
|
145
|
+
autofocus: true,
|
|
146
|
+
variant: confirmationVariant,
|
|
147
|
+
disabled: confirmationDisabled,
|
|
148
|
+
click: () => {
|
|
149
|
+
const current = dialogState.get();
|
|
150
|
+
const action = current.confirmation?.action;
|
|
151
|
+
if (typeof action === "function") {
|
|
152
|
+
action();
|
|
153
|
+
}
|
|
154
|
+
hideDialog();
|
|
155
|
+
}
|
|
156
|
+
}, [
|
|
157
|
+
confirmationText,
|
|
158
|
+
kbd({ class: "text-white text-sm whitespace-nowrap" }, [
|
|
159
|
+
Icon({ name: "return-arrow", fill: "white", size: "xs" }),
|
|
160
|
+
"Enter"
|
|
161
|
+
])
|
|
162
|
+
]))
|
|
163
|
+
])
|
|
164
|
+
])
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import Bunnix, { useRef, useState, useMemo } from "@bunnix/core";
|
|
2
|
+
const { div, button, hr, span } = Bunnix;
|
|
3
|
+
|
|
4
|
+
let dropdownCounter = 0;
|
|
5
|
+
|
|
6
|
+
export default function DropdownMenu({
|
|
7
|
+
items = [],
|
|
8
|
+
id,
|
|
9
|
+
align = "left",
|
|
10
|
+
placeholder = "Select option...",
|
|
11
|
+
size,
|
|
12
|
+
onSelect,
|
|
13
|
+
class: className = ""
|
|
14
|
+
}) {
|
|
15
|
+
const normalizeSize = (value) => {
|
|
16
|
+
if (!value || value === "default" || value === "regular" || value === "md") return "md";
|
|
17
|
+
if (value === "sm") return "sm";
|
|
18
|
+
if (value === "lg" || value === "xl") return value;
|
|
19
|
+
return value;
|
|
20
|
+
};
|
|
21
|
+
const normalizedSize = normalizeSize(size);
|
|
22
|
+
const popoverRef = useRef(null);
|
|
23
|
+
const dropdownId = id || `dropdown-instance-${++dropdownCounter}`;
|
|
24
|
+
const anchorName = `--${dropdownId}`;
|
|
25
|
+
|
|
26
|
+
const initialItem = items.find(item => item.selected) || null;
|
|
27
|
+
const selectedItem = useState(initialItem);
|
|
28
|
+
const hasSelection = selectedItem.map(s => !!s);
|
|
29
|
+
const currentTitle = selectedItem.map(s => s ? s.title : placeholder);
|
|
30
|
+
|
|
31
|
+
const handleToggle = () => {
|
|
32
|
+
const popover = popoverRef.current;
|
|
33
|
+
if (!popover) return;
|
|
34
|
+
if (popover.matches(":popover-open")) {
|
|
35
|
+
popover.hidePopover();
|
|
36
|
+
} else {
|
|
37
|
+
popover.showPopover();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleItemClick = (item) => {
|
|
42
|
+
selectedItem.set(item);
|
|
43
|
+
if (item?.click) item.click();
|
|
44
|
+
if (onSelect) onSelect(item);
|
|
45
|
+
const popover = popoverRef.current;
|
|
46
|
+
if (popover) {
|
|
47
|
+
popover.hidePopover();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const sizeClass = normalizedSize === "lg" ? "dropdown-lg" : normalizedSize === "xl" ? "dropdown-xl" : "";
|
|
52
|
+
const itemSizeClass = normalizedSize === "lg" ? "btn-lg" : normalizedSize === "xl" ? "btn-xl" : "";
|
|
53
|
+
const iconSizeClass = normalizedSize === "sm"
|
|
54
|
+
? "icon-sm"
|
|
55
|
+
: normalizedSize === "lg"
|
|
56
|
+
? "icon-lg"
|
|
57
|
+
: normalizedSize === "xl"
|
|
58
|
+
? "icon-xl"
|
|
59
|
+
: "";
|
|
60
|
+
|
|
61
|
+
return div({ class: "menu-wrapper" }, [
|
|
62
|
+
button({
|
|
63
|
+
id: dropdownId,
|
|
64
|
+
style: `anchor-name: ${anchorName}`,
|
|
65
|
+
class: `dropdown-trigger justify-start ${sizeClass} ${className}`.trim(),
|
|
66
|
+
click: handleToggle
|
|
67
|
+
}, [
|
|
68
|
+
// Reactive Icon: stable element, reactive class
|
|
69
|
+
span({
|
|
70
|
+
class: selectedItem.map(s => {
|
|
71
|
+
if (!s?.icon) return "hidden";
|
|
72
|
+
const tint = s.destructive ? "bg-destructive" : "bg-primary";
|
|
73
|
+
return `icon ${iconSizeClass} ${s.icon} ${tint}`.trim();
|
|
74
|
+
})
|
|
75
|
+
}),
|
|
76
|
+
// Reactive Title: text with dimmed style when empty
|
|
77
|
+
span({ class: hasSelection.map(selected => selected ? "" : "text-secondary") }, currentTitle)
|
|
78
|
+
]),
|
|
79
|
+
|
|
80
|
+
div({
|
|
81
|
+
ref: popoverRef,
|
|
82
|
+
popover: "auto",
|
|
83
|
+
class: `menu-popover popover-base menu-anchor-${align}`,
|
|
84
|
+
style: `--anchor-id: ${anchorName}`
|
|
85
|
+
}, [
|
|
86
|
+
div({ class: "card column-container shadow gap-sm w-min-150 p-sm bg-base" },
|
|
87
|
+
items.map((item) => {
|
|
88
|
+
if (item.isSeparator) {
|
|
89
|
+
return hr({ class: "no-margin" });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const isCurrent = selectedItem.map(s => s?.title === item.title);
|
|
93
|
+
|
|
94
|
+
return button({
|
|
95
|
+
class: isCurrent.map(active => `btn btn-flat justify-start w-full ${itemSizeClass} ${active ? 'selected' : ''}`.trim()),
|
|
96
|
+
click: () => handleItemClick(item)
|
|
97
|
+
}, [
|
|
98
|
+
span({
|
|
99
|
+
class: isCurrent.map(active => {
|
|
100
|
+
const tint = active ? "bg-white" : item.destructive ? "bg-destructive" : "bg-primary";
|
|
101
|
+
return `icon ${iconSizeClass} ${item.icon} ${tint}`.trim();
|
|
102
|
+
})
|
|
103
|
+
}),
|
|
104
|
+
item.title
|
|
105
|
+
]);
|
|
106
|
+
})
|
|
107
|
+
)
|
|
108
|
+
])
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Bunnix from "@bunnix/core";
|
|
2
|
+
const { div } = Bunnix;
|
|
3
|
+
|
|
4
|
+
export default function Grid({
|
|
5
|
+
type = "flow",
|
|
6
|
+
columns = [],
|
|
7
|
+
gap = "regular",
|
|
8
|
+
class: className = "",
|
|
9
|
+
style: inlineStyle = "",
|
|
10
|
+
...rest
|
|
11
|
+
} = {}, children) {
|
|
12
|
+
const gapMap = {
|
|
13
|
+
small: "gap-sm",
|
|
14
|
+
regular: "gap-md",
|
|
15
|
+
large: "gap-lg"
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const isFixed = type === "fixed";
|
|
19
|
+
|
|
20
|
+
// Base structural class
|
|
21
|
+
// 'grid-flow' uses flex-wrap: wrap
|
|
22
|
+
// 'column-container' provides display: flex which we override with display: grid if fixed
|
|
23
|
+
const baseClass = isFixed ? "column-container" : "grid-flow";
|
|
24
|
+
|
|
25
|
+
let gridStyle = inlineStyle;
|
|
26
|
+
if (isFixed && columns.length > 0) {
|
|
27
|
+
const template = columns.map(col => {
|
|
28
|
+
if (col.size === "auto") return "1fr";
|
|
29
|
+
if (col.size === "minmax") return "minmax(min-content, 1fr)";
|
|
30
|
+
return col.size || "1fr";
|
|
31
|
+
}).join(" ");
|
|
32
|
+
gridStyle = `display: grid; grid-template-columns: ${template}; ${inlineStyle}`.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return div({
|
|
36
|
+
class: `${baseClass} ${gapMap[gap]} ${className}`.trim(),
|
|
37
|
+
style: gridStyle,
|
|
38
|
+
...rest
|
|
39
|
+
}, children);
|
|
40
|
+
}
|