@dragunio/fe-libs 0.1.0 → 0.1.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/dist/index.mjs CHANGED
@@ -8,45 +8,60 @@ var hierarchyFromVariant = {
8
8
  danger: "primary"
9
9
  };
10
10
  var getIconSize = (size) => size === "24" ? 16 : 20;
11
- var Button = forwardRef(function Button2({
12
- hierarchy,
13
- variant,
14
- size = "32",
15
- color = "normal",
16
- destructive,
17
- iconOnly = false,
18
- icon,
19
- prefix,
20
- suffix,
21
- content,
22
- loading = false,
23
- alignCenter = false,
24
- children,
25
- className,
26
- disabled,
27
- type = "button",
28
- ...props
29
- }, ref) {
30
- const resolvedHierarchy = hierarchy ?? hierarchyFromVariant[variant ?? "primary"];
31
- const resolvedDestructive = destructive || variant === "danger";
32
- const isDisabled = disabled || loading;
33
- const label = loading ? "Loading\u2026" : children ?? content;
34
- const classes = [
35
- "fe-button",
36
- `fe-button--${resolvedHierarchy}`,
37
- `fe-button--${size}`,
38
- `fe-button--${color}`,
39
- iconOnly && "fe-button--icon-only",
40
- resolvedDestructive && "fe-button--destructive",
41
- alignCenter && "fe-button--align-center",
42
- className
43
- ].filter(Boolean).join(" ");
44
- return /* @__PURE__ */ jsx("button", { ref, className: classes, type, disabled: isDisabled, "aria-busy": loading || void 0, ...props, children: iconOnly ? icon : /* @__PURE__ */ jsxs(Fragment, { children: [
45
- prefix && /* @__PURE__ */ jsx("span", { className: "fe-button__icon", children: prefix }),
46
- /* @__PURE__ */ jsx("span", { className: "fe-button__label", children: label }),
47
- suffix && /* @__PURE__ */ jsx("span", { className: "fe-button__icon", children: suffix })
48
- ] }) });
49
- });
11
+ var Button = forwardRef(
12
+ function Button2({
13
+ hierarchy,
14
+ variant,
15
+ size = "32",
16
+ color = "normal",
17
+ destructive,
18
+ iconOnly = false,
19
+ icon,
20
+ prefix,
21
+ suffix,
22
+ content,
23
+ loading = false,
24
+ alignCenter = false,
25
+ children,
26
+ className,
27
+ disabled,
28
+ type = "button",
29
+ ...props
30
+ }, ref) {
31
+ const resolvedHierarchy = hierarchy ?? hierarchyFromVariant[variant ?? "primary"];
32
+ const resolvedDestructive = destructive || variant === "danger";
33
+ const isDisabled = disabled || loading;
34
+ const label = loading ? "Loading\u2026" : children ?? content;
35
+ const hasLabel = label !== null && label !== void 0 && label !== false && label !== "";
36
+ const resolvedIconOnly = iconOnly || icon !== null && icon !== void 0 && !hasLabel;
37
+ const classes = [
38
+ "fe-button",
39
+ `fe-button--${resolvedHierarchy}`,
40
+ `fe-button--${size}`,
41
+ `fe-button--${color}`,
42
+ resolvedIconOnly && "fe-button--icon-only",
43
+ resolvedDestructive && "fe-button--destructive",
44
+ alignCenter && "fe-button--align-center",
45
+ className
46
+ ].filter(Boolean).join(" ");
47
+ return /* @__PURE__ */ jsx(
48
+ "button",
49
+ {
50
+ ref,
51
+ className: classes,
52
+ type,
53
+ disabled: isDisabled,
54
+ "aria-busy": loading || void 0,
55
+ ...props,
56
+ children: resolvedIconOnly ? icon : /* @__PURE__ */ jsxs(Fragment, { children: [
57
+ prefix && /* @__PURE__ */ jsx("span", { className: "fe-button__icon", children: prefix }),
58
+ /* @__PURE__ */ jsx("span", { className: "fe-button__label", children: label }),
59
+ suffix && /* @__PURE__ */ jsx("span", { className: "fe-button__icon", children: suffix })
60
+ ] })
61
+ }
62
+ );
63
+ }
64
+ );
50
65
  Button.displayName = "Button";
51
66
 
52
67
  // packages/icons/src/index.tsx
@@ -76,11 +91,130 @@ function SearchIcon(props) {
76
91
  }
77
92
 
78
93
  // packages/table/src/index.tsx
79
- import { createContext, useContext } from "react";
80
- import { jsx as jsx3 } from "react/jsx-runtime";
94
+ import {
95
+ createContext,
96
+ isValidElement,
97
+ useCallback,
98
+ useContext,
99
+ useEffect,
100
+ useMemo,
101
+ useRef,
102
+ useState
103
+ } from "react";
104
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
81
105
  var TableContext = createContext({ striped: false });
82
- function Table({ striped = false, children, ...props }) {
83
- return /* @__PURE__ */ jsx3(TableContext.Provider, { value: { striped }, children: /* @__PURE__ */ jsx3("table", { ...props, children }) });
106
+ var MIN_COLUMN_WIDTH = 96;
107
+ var MAX_COLUMN_WIDTH = 1200;
108
+ function getColumnCount(headers, data) {
109
+ return Math.max(headers.length, ...data.map((row) => row.length), 0);
110
+ }
111
+ function getText(value) {
112
+ if (typeof value === "string" || typeof value === "number") return String(value);
113
+ if (Array.isArray(value)) return value.map(getText).join("");
114
+ if (isValidElement(value)) return getText(value.props.children);
115
+ return "";
116
+ }
117
+ function autoColumnSize(columnIndex, headers, data) {
118
+ const values = [headers[columnIndex], ...data.map((row) => row[columnIndex])];
119
+ const longestText = values.reduce((longest, value) => Math.max(longest, getText(value).length), 0);
120
+ return Math.min(MAX_COLUMN_WIDTH, Math.max(MIN_COLUMN_WIDTH, longestText * 8 + 40));
121
+ }
122
+ function getInitialColumnSizes(headers, data, sizes, minColumnWidth = MIN_COLUMN_WIDTH) {
123
+ return Array.from({ length: getColumnCount(headers, data) }, (_, columnIndex) => {
124
+ const suppliedSize = sizes?.[columnIndex];
125
+ return typeof suppliedSize === "number" && Number.isFinite(suppliedSize) ? Math.min(MAX_COLUMN_WIDTH, Math.max(minColumnWidth, suppliedSize)) : autoColumnSize(columnIndex, headers, data);
126
+ });
127
+ }
128
+ function sameSizes(left, right) {
129
+ return left.length === right.length && left.every((size, index) => size === right[index]);
130
+ }
131
+ function DataTable({
132
+ headers,
133
+ data,
134
+ striped = true,
135
+ defaultColumnSizes,
136
+ columnSizes,
137
+ onColumnResize,
138
+ minColumnWidth = MIN_COLUMN_WIDTH,
139
+ className,
140
+ ...props
141
+ }) {
142
+ const columnCount = getColumnCount(headers, data);
143
+ const safeMinWidth = Math.max(1, minColumnWidth);
144
+ const generatedSizes = useMemo(
145
+ () => getInitialColumnSizes(headers, data, defaultColumnSizes, safeMinWidth),
146
+ [headers, data, defaultColumnSizes, safeMinWidth]
147
+ );
148
+ const [uncontrolledSizes, setUncontrolledSizes] = useState(generatedSizes);
149
+ const resizeState = useRef();
150
+ const [isResizing, setIsResizing] = useState(false);
151
+ const sizes = columnSizes ? getInitialColumnSizes(headers, data, columnSizes, safeMinWidth).map((size) => Math.max(safeMinWidth, size)) : uncontrolledSizes.map((size) => Math.max(safeMinWidth, size));
152
+ useEffect(() => {
153
+ if (columnSizes) return;
154
+ setUncontrolledSizes((currentSizes) => {
155
+ const nextSizes = generatedSizes.map((generatedSize, index) => currentSizes[index] ?? generatedSize);
156
+ return sameSizes(currentSizes, nextSizes) ? currentSizes : nextSizes;
157
+ });
158
+ }, [columnSizes, generatedSizes]);
159
+ const updateColumnSize = useCallback((columnIndex, nextSize) => {
160
+ const clampedSize = Math.min(MAX_COLUMN_WIDTH, Math.max(safeMinWidth, Math.round(nextSize)));
161
+ const nextSizes = sizes.map((size, index) => index === columnIndex ? clampedSize : size);
162
+ if (!columnSizes) setUncontrolledSizes(nextSizes);
163
+ onColumnResize?.(nextSizes, columnIndex);
164
+ }, [columnSizes, onColumnResize, safeMinWidth, sizes]);
165
+ useEffect(() => {
166
+ const onPointerMove = (event) => {
167
+ const activeResize = resizeState.current;
168
+ if (!activeResize) return;
169
+ updateColumnSize(activeResize.columnIndex, activeResize.startWidth + event.clientX - activeResize.startX);
170
+ };
171
+ const stopResizing = () => {
172
+ resizeState.current = void 0;
173
+ setIsResizing(false);
174
+ };
175
+ window.addEventListener("pointermove", onPointerMove);
176
+ window.addEventListener("pointerup", stopResizing);
177
+ window.addEventListener("pointercancel", stopResizing);
178
+ return () => {
179
+ window.removeEventListener("pointermove", onPointerMove);
180
+ window.removeEventListener("pointerup", stopResizing);
181
+ window.removeEventListener("pointercancel", stopResizing);
182
+ };
183
+ }, [updateColumnSize]);
184
+ return /* @__PURE__ */ jsx3("div", { className: "fe-table", "data-resizing": isResizing || void 0, children: /* @__PURE__ */ jsxs3("table", { className: ["fe-table__table", className].filter(Boolean).join(" "), "data-fe-table": true, "data-striped": striped || void 0, ...props, children: [
185
+ /* @__PURE__ */ jsx3("colgroup", { children: sizes.map((size, index) => /* @__PURE__ */ jsx3("col", { style: { width: `${size}px` } }, index)) }),
186
+ /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsx3("tr", { children: Array.from({ length: columnCount }, (_, columnIndex) => /* @__PURE__ */ jsxs3("th", { scope: "col", children: [
187
+ /* @__PURE__ */ jsx3("span", { className: "fe-table__header-content", children: headers[columnIndex] ?? `Column ${columnIndex + 1}` }),
188
+ /* @__PURE__ */ jsx3(
189
+ "span",
190
+ {
191
+ className: "fe-table__resize-handle",
192
+ role: "separator",
193
+ "aria-label": `Resize ${getText(headers[columnIndex]) || `column ${columnIndex + 1}`}`,
194
+ "aria-orientation": "vertical",
195
+ "aria-valuemin": safeMinWidth,
196
+ "aria-valuemax": MAX_COLUMN_WIDTH,
197
+ "aria-valuenow": sizes[columnIndex] ?? safeMinWidth,
198
+ tabIndex: 0,
199
+ onPointerDown: (event) => {
200
+ event.preventDefault();
201
+ resizeState.current = { columnIndex, startX: event.clientX, startWidth: sizes[columnIndex] ?? safeMinWidth };
202
+ setIsResizing(true);
203
+ event.currentTarget.setPointerCapture(event.pointerId);
204
+ },
205
+ onKeyDown: (event) => {
206
+ if (event.key === "ArrowLeft") updateColumnSize(columnIndex, (sizes[columnIndex] ?? safeMinWidth) - 16);
207
+ if (event.key === "ArrowRight") updateColumnSize(columnIndex, (sizes[columnIndex] ?? safeMinWidth) + 16);
208
+ }
209
+ }
210
+ )
211
+ ] }, columnIndex)) }) }),
212
+ /* @__PURE__ */ jsx3("tbody", { children: data.map((row, rowIndex) => /* @__PURE__ */ jsx3("tr", { children: Array.from({ length: columnCount }, (_, columnIndex) => /* @__PURE__ */ jsx3("td", { children: row[columnIndex] }, columnIndex)) }, rowIndex)) })
213
+ ] }) });
214
+ }
215
+ function Table({ headers, data, striped, children, ...props }) {
216
+ if (headers || data) return /* @__PURE__ */ jsx3(DataTable, { headers: headers ?? [], data: data ?? [], striped: striped ?? true, ...props });
217
+ return /* @__PURE__ */ jsx3(TableContext.Provider, { value: { striped: striped ?? false }, children: /* @__PURE__ */ jsx3("table", { "data-fe-table": true, "data-striped": striped || void 0, ...props, children }) });
84
218
  }
85
219
  function TableHead(props) {
86
220
  return /* @__PURE__ */ jsx3("thead", { ...props });
@@ -101,12 +235,387 @@ function TableHeader({ children, ...props }) {
101
235
  function TableCaption(props) {
102
236
  return /* @__PURE__ */ jsx3("caption", { ...props });
103
237
  }
238
+
239
+ // packages/toggle/src/index.tsx
240
+ import { forwardRef as forwardRef2 } from "react";
241
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
242
+ var Toggle = forwardRef2(function Toggle2({ color = "#16a34a", label, className, style, id, ...props }, ref) {
243
+ const classes = ["fe-toggle", className].filter(Boolean).join(" ");
244
+ const toggleStyle = {
245
+ "--fe-toggle-color": color,
246
+ ...style
247
+ };
248
+ return /* @__PURE__ */ jsxs4("label", { className: classes, style: toggleStyle, children: [
249
+ /* @__PURE__ */ jsx4("input", { ref, id, className: "fe-toggle__input", type: "checkbox", role: "switch", ...props }),
250
+ /* @__PURE__ */ jsx4("span", { className: "fe-toggle__control", "aria-hidden": "true" }),
251
+ label && /* @__PURE__ */ jsx4("span", { className: "fe-toggle__label", children: label })
252
+ ] });
253
+ });
254
+ Toggle.displayName = "Toggle";
255
+
256
+ // packages/drawer/src/index.tsx
257
+ import { useEffect as useEffect2 } from "react";
258
+ import { createPortal } from "react-dom";
259
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
260
+ function Drawer({ open, onClose, title, footer, side = "right", width = 400, fullScreen = false, children, closeOnBackdropClick = true, className, style, ...props }) {
261
+ useEffect2(() => {
262
+ if (!open) return;
263
+ const onKeyDown = (event) => {
264
+ if (event.key === "Escape") onClose();
265
+ };
266
+ document.addEventListener("keydown", onKeyDown);
267
+ return () => document.removeEventListener("keydown", onKeyDown);
268
+ }, [onClose, open]);
269
+ if (!open || typeof document === "undefined") return null;
270
+ const panelStyle = !fullScreen && (side === "left" || side === "right") ? { ...style, width: typeof width === "number" ? `${width}px` : width } : style;
271
+ return createPortal(/* @__PURE__ */ jsx5("div", { className: "fe-drawer-backdrop", role: "presentation", onMouseDown: (event) => {
272
+ if (event.target === event.currentTarget && closeOnBackdropClick) onClose();
273
+ }, children: /* @__PURE__ */ jsxs5("aside", { className: ["fe-drawer", `fe-drawer--${side}`, fullScreen && "fe-drawer--full-screen", className].filter(Boolean).join(" "), style: panelStyle, role: "dialog", "aria-modal": "true", "aria-label": typeof title === "string" ? title : "Drawer", ...props, children: [
274
+ /* @__PURE__ */ jsxs5("header", { className: "fe-drawer__header", children: [
275
+ title && /* @__PURE__ */ jsx5("h2", { children: title }),
276
+ /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Close drawer", onClick: onClose, children: "\xD7" })
277
+ ] }),
278
+ /* @__PURE__ */ jsx5("div", { className: "fe-drawer__body", children }),
279
+ footer && /* @__PURE__ */ jsx5("footer", { className: "fe-drawer__footer", children: footer })
280
+ ] }) }), document.body);
281
+ }
282
+
283
+ // packages/form-controls/src/index.tsx
284
+ import {
285
+ forwardRef as forwardRef3,
286
+ useEffect as useEffect3,
287
+ useId,
288
+ useRef as useRef2,
289
+ useState as useState2
290
+ } from "react";
291
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
292
+ var Selection = forwardRef3(function Selection2({ className, label, helperText, error, options, placeholder = "Select an option", size = "medium", id, value, defaultValue, onChange, multiple = false, searchable = false, tag = false, clearable = false, disabled = false, name, onKeyDown, ...props }, ref) {
293
+ const generatedId = useId();
294
+ const listboxId = id ?? generatedId;
295
+ const isControlled = value !== void 0;
296
+ const [internalValue, setInternalValue] = useState2(defaultValue ?? (multiple ? [] : ""));
297
+ const selectedValue = isControlled ? value : internalValue;
298
+ const selectedValues = Array.isArray(selectedValue) ? selectedValue : selectedValue === "" || selectedValue === null || selectedValue === void 0 ? [] : [selectedValue];
299
+ const selectedOptions = options.filter((option) => selectedValues.some((item) => String(item) === String(option.value)));
300
+ const [open, setOpen] = useState2(false);
301
+ const [query, setQuery] = useState2("");
302
+ const [activeIndex, setActiveIndex] = useState2(0);
303
+ const rootRef = useRef2(null);
304
+ useEffect3(() => {
305
+ const close = (event) => {
306
+ if (!rootRef.current?.contains(event.target)) setOpen(false);
307
+ };
308
+ document.addEventListener("mousedown", close);
309
+ return () => document.removeEventListener("mousedown", close);
310
+ }, []);
311
+ const filteredOptions = options.filter((option) => !query || (option.searchText ?? (typeof option.label === "string" ? option.label : String(option.value))).toLowerCase().includes(query.toLowerCase()));
312
+ const commit = (next) => {
313
+ if (!isControlled) setInternalValue(next);
314
+ const selected = options.filter((option) => (Array.isArray(next) ? next : [next]).some((item) => String(item) === String(option.value)));
315
+ onChange?.(next, multiple ? selected : selected[0]);
316
+ };
317
+ const choose = (option) => {
318
+ if (option.disabled) return;
319
+ const isSelected = selectedValues.some((item) => String(item) === String(option.value));
320
+ if (multiple) commit(isSelected ? selectedValues.filter((item) => String(item) !== String(option.value)) : [...selectedValues, option.value]);
321
+ else {
322
+ commit(option.value);
323
+ setOpen(false);
324
+ setQuery("");
325
+ }
326
+ };
327
+ const clear = () => {
328
+ commit(multiple ? [] : "");
329
+ setQuery("");
330
+ };
331
+ const displayLabel = selectedOptions.length ? selectedOptions.map((option) => option.label) : null;
332
+ const onTriggerKeyDown = (event) => {
333
+ onKeyDown?.(event);
334
+ if (disabled) return;
335
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
336
+ event.preventDefault();
337
+ setOpen(true);
338
+ setActiveIndex((current) => Math.max(0, Math.min(filteredOptions.length - 1, current + (event.key === "ArrowDown" ? 1 : -1))));
339
+ }
340
+ if (event.key === "Enter" || event.key === " ") {
341
+ event.preventDefault();
342
+ if (!open) setOpen(true);
343
+ else if (filteredOptions[activeIndex]) choose(filteredOptions[activeIndex]);
344
+ }
345
+ if (event.key === "Escape") setOpen(false);
346
+ };
347
+ return /* @__PURE__ */ jsxs6("div", { ref, className: ["fe-selection", `fe-selection--${size}`, open && "fe-selection--open", clearable && selectedOptions.length > 0 && "fe-selection--clearable", error && "fe-selection--error", disabled && "fe-selection--disabled", className].filter(Boolean).join(" "), ...props, children: [
348
+ label && /* @__PURE__ */ jsx6("span", { className: "fe-selection__label", children: label }),
349
+ /* @__PURE__ */ jsxs6("div", { ref: rootRef, className: "fe-selection__control", children: [
350
+ name && selectedValues.map((item) => /* @__PURE__ */ jsx6("input", { type: "hidden", name: multiple ? `${name}[]` : name, value: item }, String(item))),
351
+ /* @__PURE__ */ jsxs6("button", { type: "button", className: "fe-selection__trigger", disabled, "aria-invalid": error || void 0, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": listboxId, onClick: () => setOpen((current) => !current), onKeyDown: onTriggerKeyDown, children: [
352
+ /* @__PURE__ */ jsx6("span", { className: ["fe-selection__value", !selectedOptions.length && "is-placeholder", tag && selectedOptions.length > 1 && "is-tags"].filter(Boolean).join(" "), children: displayLabel ? tag && multiple ? selectedOptions.map((option) => /* @__PURE__ */ jsxs6("span", { className: "fe-selection__tag", children: [
353
+ option.label,
354
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": "true", children: "\xD7" })
355
+ ] }, String(option.value))) : displayLabel.map((item, index) => /* @__PURE__ */ jsxs6("span", { children: [
356
+ item,
357
+ index < displayLabel.length - 1 ? ", " : ""
358
+ ] }, index)) : placeholder }),
359
+ /* @__PURE__ */ jsx6("span", { className: "fe-selection__actions", children: /* @__PURE__ */ jsx6("span", { className: "fe-selection__chevron", "aria-hidden": "true" }) })
360
+ ] }),
361
+ clearable && selectedOptions.length > 0 && /* @__PURE__ */ jsx6("button", { type: "button", className: "fe-selection__clear", "aria-label": "Clear selection", onClick: clear }),
362
+ open && /* @__PURE__ */ jsxs6("div", { id: listboxId, className: "fe-selection__menu", role: "listbox", "aria-multiselectable": multiple || void 0, children: [
363
+ searchable && /* @__PURE__ */ jsxs6("div", { className: "fe-selection__search", children: [
364
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": "true", children: "\u2315" }),
365
+ /* @__PURE__ */ jsx6("input", { autoFocus: true, value: query, onChange: (event) => {
366
+ setQuery(event.target.value);
367
+ setActiveIndex(0);
368
+ }, placeholder: "Search\u2026", "aria-label": "Search options" })
369
+ ] }),
370
+ /* @__PURE__ */ jsx6("div", { className: "fe-selection__options", children: filteredOptions.length ? filteredOptions.map((option, index) => {
371
+ const isSelected = selectedValues.some((item) => String(item) === String(option.value));
372
+ return /* @__PURE__ */ jsxs6("button", { type: "button", className: [isSelected && "is-selected", index === activeIndex && "is-active"].filter(Boolean).join(" "), role: "option", "aria-selected": isSelected, disabled: option.disabled, onMouseMove: () => setActiveIndex(index), onClick: () => choose(option), children: [
373
+ /* @__PURE__ */ jsx6("span", { children: option.label }),
374
+ isSelected && /* @__PURE__ */ jsx6("span", { className: "fe-selection__check", "aria-hidden": "true", children: "\u2713" })
375
+ ] }, String(option.value));
376
+ }) : /* @__PURE__ */ jsx6("p", { className: "fe-selection__empty", children: "No matching options" }) })
377
+ ] })
378
+ ] }),
379
+ helperText && /* @__PURE__ */ jsx6("span", { className: "fe-selection__helper", children: helperText })
380
+ ] });
381
+ });
382
+ Selection.displayName = "Selection";
383
+ function formatNumber(value, decimals = 2, thousandSeparator = ",", decimalSeparator = ".") {
384
+ return new Intl.NumberFormat("en-US", {
385
+ minimumFractionDigits: 0,
386
+ maximumFractionDigits: decimals,
387
+ useGrouping: true
388
+ }).format(value).replace(/,/g, thousandSeparator).replace(/\./g, decimalSeparator);
389
+ }
390
+ function escapeRegExp(value) {
391
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
392
+ }
393
+ function groupInteger(value, separator) {
394
+ return value.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
395
+ }
396
+ function normalizeNumberInput(raw, options) {
397
+ const { allowDecimal, decimalSeparator, thousandSeparator, min, max } = options;
398
+ const decimals = Math.max(0, options.decimals);
399
+ const negative = raw.trimStart().startsWith("-") && (min === void 0 || min < 0);
400
+ const decimalExpression = new RegExp(`[^0-9${allowDecimal ? escapeRegExp(decimalSeparator) : ""}]`, "g");
401
+ const cleaned = raw.replace(decimalExpression, "");
402
+ const decimalIndex = allowDecimal ? cleaned.indexOf(decimalSeparator) : -1;
403
+ const hasDecimal = decimalIndex >= 0;
404
+ const rawInteger = hasDecimal ? cleaned.slice(0, decimalIndex) : cleaned;
405
+ const rawFraction = hasDecimal ? cleaned.slice(decimalIndex + decimalSeparator.length).replace(new RegExp(escapeRegExp(decimalSeparator), "g"), "") : "";
406
+ const integer = rawInteger.replace(/^0+(?=\d)/, "") || (rawInteger ? "0" : "");
407
+ const fraction = rawFraction.slice(0, decimals);
408
+ if (!integer && !fraction) return { display: negative ? "-" : "", value: null };
409
+ const numericValue = Number(`${negative ? "-" : ""}${integer || "0"}${hasDecimal ? `.${fraction || "0"}` : ""}`);
410
+ if (!Number.isFinite(numericValue)) return { display: "", value: null };
411
+ const lowerBound = min === void 0 ? numericValue : Math.max(numericValue, min);
412
+ const value = max === void 0 ? lowerBound : Math.min(lowerBound, max);
413
+ if (value !== numericValue) return { display: formatNumber(value, decimals, thousandSeparator, decimalSeparator), value };
414
+ return { display: `${negative ? "-" : ""}${groupInteger(integer || "0", thousandSeparator)}${hasDecimal ? `${decimalSeparator}${fraction}` : ""}`, value };
415
+ }
416
+ var NumberInput = forwardRef3(function NumberInput2({ value, defaultValue = null, onValueChange, allowDecimal = true, decimals = 2, min, max, decimalSeparator = ".", thousandSeparator = ",", label, helperText, error, className, onBlur, onFocus, ...props }, ref) {
417
+ const isControlled = value !== void 0;
418
+ const [internalValue, setInternalValue] = useState2(defaultValue);
419
+ const [focused, setFocused] = useState2(false);
420
+ const actualValue = isControlled ? value ?? null : internalValue;
421
+ const [text, setText] = useState2(actualValue === null ? "" : formatNumber(actualValue, decimals, thousandSeparator, decimalSeparator));
422
+ useEffect3(() => {
423
+ if (!focused) setText(actualValue === null ? "" : formatNumber(actualValue, decimals, thousandSeparator, decimalSeparator));
424
+ }, [actualValue, decimalSeparator, decimals, focused, thousandSeparator]);
425
+ const updateValue = (nextText) => {
426
+ const next = normalizeNumberInput(nextText, { allowDecimal, decimals, decimalSeparator, thousandSeparator, min, max });
427
+ setText(next.display);
428
+ const nextValue = next.value;
429
+ if (!isControlled) setInternalValue(nextValue);
430
+ onValueChange?.(nextValue);
431
+ };
432
+ return /* @__PURE__ */ jsxs6("label", { className: ["fe-number-input", error && "fe-number-input--error", className].filter(Boolean).join(" "), children: [
433
+ label && /* @__PURE__ */ jsx6("span", { className: "fe-number-input__label", children: label }),
434
+ /* @__PURE__ */ jsx6("input", { ref, inputMode: allowDecimal ? "decimal" : "numeric", value: text, "aria-invalid": error || void 0, onFocus: (event) => {
435
+ setFocused(true);
436
+ onFocus?.(event);
437
+ }, onChange: (event) => updateValue(event.target.value), onBlur: (event) => {
438
+ setFocused(false);
439
+ onBlur?.(event);
440
+ }, ...props }),
441
+ helperText && /* @__PURE__ */ jsx6("span", { className: "fe-number-input__helper", children: helperText })
442
+ ] });
443
+ });
444
+ NumberInput.displayName = "NumberInput";
445
+ function CurrencyInput({ suffix = "VND", currencies, currency, defaultCurrency, onCurrencyChange, className, ...props }) {
446
+ const [internalCurrency, setInternalCurrency] = useState2(defaultCurrency ?? currencies?.[0]?.code ?? "");
447
+ const selectedCurrency = currency ?? internalCurrency;
448
+ const current = currencies?.find((item) => item.code === selectedCurrency);
449
+ const currencySuffix = current?.suffix ?? current?.code ?? suffix;
450
+ return /* @__PURE__ */ jsxs6("div", { className: ["fe-currency-input", className].filter(Boolean).join(" "), children: [
451
+ /* @__PURE__ */ jsx6(NumberInput, { ...props, className: "fe-currency-input__number" }),
452
+ currencies?.length ? /* @__PURE__ */ jsx6("select", { className: "fe-currency-input__currency", "aria-label": "Currency", value: selectedCurrency, onChange: (event) => {
453
+ const next = currencies.find((item) => item.code === event.target.value);
454
+ if (!next) return;
455
+ if (currency === void 0) setInternalCurrency(next.code);
456
+ onCurrencyChange?.(next);
457
+ }, children: currencies.map((item) => /* @__PURE__ */ jsx6("option", { value: item.code, children: item.label ?? item.code }, item.code)) }) : /* @__PURE__ */ jsx6("span", { className: "fe-currency-input__suffix", children: currencySuffix })
458
+ ] });
459
+ }
460
+ var pad = (value) => String(value).padStart(2, "0");
461
+ function formatDate(date, format) {
462
+ return format.replace(/yyyy/g, String(date.getFullYear())).replace(/MM/g, pad(date.getMonth() + 1)).replace(/dd/g, pad(date.getDate())).replace(/HH/g, pad(date.getHours())).replace(/mm/g, pad(date.getMinutes()));
463
+ }
464
+ function startOfDay(date) {
465
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
466
+ }
467
+ function sameDay(first, second) {
468
+ return !!first && !!second && startOfDay(first).getTime() === startOfDay(second).getTime();
469
+ }
470
+ function mergeDateTime(day, time) {
471
+ const [hours, minutes] = time.split(":").map(Number);
472
+ const next = new Date(day);
473
+ next.setHours(hours || 0, minutes || 0, 0, 0);
474
+ return next;
475
+ }
476
+ function DateTimePicker(props) {
477
+ const { label, format = "dd/MM/yyyy HH:mm", disabled, placeholder, locale = "en-US", className, mode = "single" } = props;
478
+ const isRange = mode === "range";
479
+ const controlled = props.value !== void 0;
480
+ const [singleInternal, setSingleInternal] = useState2((!isRange ? props.defaultValue : null) ?? null);
481
+ const [rangeInternal, setRangeInternal] = useState2((isRange ? props.defaultValue : void 0) ?? [null, null]);
482
+ const singleValue = !isRange ? props.value ?? singleInternal : null;
483
+ const rangeValue = isRange ? props.value ?? rangeInternal : [null, null];
484
+ const [open, setOpen] = useState2(false);
485
+ const [month, setMonth] = useState2(() => new Date((isRange ? rangeValue[0] : singleValue) ?? /* @__PURE__ */ new Date()));
486
+ const [editingEnd, setEditingEnd] = useState2(false);
487
+ const rootRef = useRef2(null);
488
+ useEffect3(() => {
489
+ const onPointerDown = (event) => {
490
+ if (!rootRef.current?.contains(event.target)) setOpen(false);
491
+ };
492
+ document.addEventListener("mousedown", onPointerDown);
493
+ return () => document.removeEventListener("mousedown", onPointerDown);
494
+ }, []);
495
+ const setSingle = (next) => {
496
+ if (!controlled) setSingleInternal(next);
497
+ props.onChange?.(next);
498
+ };
499
+ const setRange = (next) => {
500
+ if (!controlled) setRangeInternal(next);
501
+ props.onChange?.(next);
502
+ };
503
+ const selectDay = (day) => {
504
+ if (!isRange) {
505
+ setSingle(mergeDateTime(day, singleValue ? `${pad(singleValue.getHours())}:${pad(singleValue.getMinutes())}` : "00:00"));
506
+ setOpen(false);
507
+ return;
508
+ }
509
+ const [start, end] = rangeValue;
510
+ if (!start || end || editingEnd) {
511
+ setRange([mergeDateTime(day, start ? `${pad(start.getHours())}:${pad(start.getMinutes())}` : "00:00"), null]);
512
+ setEditingEnd(false);
513
+ return;
514
+ }
515
+ const nextEnd = mergeDateTime(day, "00:00");
516
+ setRange(nextEnd < start ? [nextEnd, start] : [start, nextEnd]);
517
+ setEditingEnd(false);
518
+ };
519
+ const shownValue = isRange ? [rangeValue[0] && formatDate(rangeValue[0], format), rangeValue[1] && formatDate(rangeValue[1], format)].filter(Boolean).join(" \u2014 ") : singleValue && formatDate(singleValue, format);
520
+ const firstDay = new Date(month.getFullYear(), month.getMonth(), 1);
521
+ const offset = (firstDay.getDay() + 6) % 7;
522
+ const days = Array.from({ length: 42 }, (_, index) => new Date(month.getFullYear(), month.getMonth(), index - offset + 1));
523
+ const selectedForTime = isRange ? editingEnd ? rangeValue[1] : rangeValue[0] : singleValue;
524
+ const setTime = (time) => {
525
+ if (!selectedForTime) return;
526
+ const next = mergeDateTime(selectedForTime, time);
527
+ if (isRange) setRange(editingEnd ? [rangeValue[0], next] : [next, rangeValue[1]]);
528
+ else setSingle(next);
529
+ };
530
+ return /* @__PURE__ */ jsxs6("div", { ref: rootRef, className: ["fe-datetime-picker", className].filter(Boolean).join(" "), children: [
531
+ label && /* @__PURE__ */ jsx6("span", { className: "fe-datetime-picker__label", children: label }),
532
+ /* @__PURE__ */ jsxs6("button", { type: "button", className: "fe-datetime-picker__trigger", disabled, "aria-expanded": open, onClick: () => setOpen((current) => !current), children: [
533
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": "true", children: "\u25F7" }),
534
+ /* @__PURE__ */ jsx6("span", { children: shownValue || placeholder || (isRange ? "Select date range" : "Select date and time") })
535
+ ] }),
536
+ open && /* @__PURE__ */ jsxs6("div", { className: "fe-datetime-picker__popper", role: "dialog", "aria-label": "Date and time picker", children: [
537
+ /* @__PURE__ */ jsxs6("div", { className: "fe-datetime-picker__month", children: [
538
+ /* @__PURE__ */ jsx6("button", { type: "button", "aria-label": "Previous month", onClick: () => setMonth((current) => new Date(current.getFullYear(), current.getMonth() - 1, 1)), children: "\u2039" }),
539
+ /* @__PURE__ */ jsx6("strong", { children: month.toLocaleDateString(locale, { month: "long", year: "numeric" }) }),
540
+ /* @__PURE__ */ jsx6("button", { type: "button", "aria-label": "Next month", onClick: () => setMonth((current) => new Date(current.getFullYear(), current.getMonth() + 1, 1)), children: "\u203A" })
541
+ ] }),
542
+ /* @__PURE__ */ jsx6("div", { className: "fe-datetime-picker__weekdays", children: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"].map((day) => /* @__PURE__ */ jsx6("span", { children: day }, day)) }),
543
+ /* @__PURE__ */ jsx6("div", { className: "fe-datetime-picker__days", children: days.map((day) => {
544
+ const inMonth = day.getMonth() === month.getMonth();
545
+ const selected = isRange ? sameDay(day, rangeValue[0]) || sameDay(day, rangeValue[1]) : sameDay(day, singleValue);
546
+ const inRange = isRange && rangeValue[0] && rangeValue[1] && day > startOfDay(rangeValue[0]) && day < startOfDay(rangeValue[1]);
547
+ return /* @__PURE__ */ jsx6("button", { type: "button", className: [!inMonth && "is-other-month", selected && "is-selected", inRange && "is-in-range"].filter(Boolean).join(" "), onClick: () => selectDay(day), children: day.getDate() }, day.toISOString());
548
+ }) }),
549
+ isRange && /* @__PURE__ */ jsxs6("div", { className: "fe-datetime-picker__range-tabs", children: [
550
+ /* @__PURE__ */ jsx6("button", { type: "button", className: !editingEnd ? "is-active" : "", onClick: () => setEditingEnd(false), children: "Start" }),
551
+ /* @__PURE__ */ jsx6("button", { type: "button", className: editingEnd ? "is-active" : "", onClick: () => setEditingEnd(true), children: "End" })
552
+ ] }),
553
+ /* @__PURE__ */ jsxs6("label", { className: "fe-datetime-picker__time", children: [
554
+ "Time",
555
+ /* @__PURE__ */ jsx6("input", { type: "time", value: selectedForTime ? `${pad(selectedForTime.getHours())}:${pad(selectedForTime.getMinutes())}` : "00:00", onChange: (event) => setTime(event.target.value), disabled: !selectedForTime })
556
+ ] })
557
+ ] })
558
+ ] });
559
+ }
560
+
561
+ // packages/tooltip/src/index.tsx
562
+ import { cloneElement, useId as useId2, useState as useState3 } from "react";
563
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
564
+ function Tooltip({ content, children, placement = "top", disabled, className, ...props }) {
565
+ const [visible, setVisible] = useState3(false);
566
+ const id = useId2();
567
+ const trigger = cloneElement(children, { "aria-describedby": visible ? id : void 0, onMouseEnter: (event) => {
568
+ children.props.onMouseEnter?.(event);
569
+ setVisible(true);
570
+ }, onMouseLeave: (event) => {
571
+ children.props.onMouseLeave?.(event);
572
+ setVisible(false);
573
+ }, onFocus: (event) => {
574
+ children.props.onFocus?.(event);
575
+ setVisible(true);
576
+ }, onBlur: (event) => {
577
+ children.props.onBlur?.(event);
578
+ setVisible(false);
579
+ } });
580
+ return /* @__PURE__ */ jsxs7("span", { className: ["fe-tooltip", `fe-tooltip--${placement}`, className].filter(Boolean).join(" "), ...props, children: [
581
+ trigger,
582
+ visible && !disabled && /* @__PURE__ */ jsx7("span", { id, role: "tooltip", className: "fe-tooltip__content", children: content })
583
+ ] });
584
+ }
585
+
586
+ // packages/expandable-section/src/index.tsx
587
+ import { useId as useId3, useState as useState4 } from "react";
588
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
589
+ function ExpandableSection({ title, children, expanded, defaultExpanded = false, onExpandedChange, disabled, className, ...props }) {
590
+ const [internalExpanded, setInternalExpanded] = useState4(defaultExpanded);
591
+ const isExpanded = expanded ?? internalExpanded;
592
+ const id = useId3();
593
+ const toggle = () => {
594
+ if (disabled) return;
595
+ const next = !isExpanded;
596
+ if (expanded === void 0) setInternalExpanded(next);
597
+ onExpandedChange?.(next);
598
+ };
599
+ return /* @__PURE__ */ jsxs8("section", { className: ["fe-expandable-section", isExpanded && "fe-expandable-section--expanded", disabled && "fe-expandable-section--disabled", className].filter(Boolean).join(" "), ...props, children: [
600
+ /* @__PURE__ */ jsxs8("button", { className: "fe-expandable-section__trigger", type: "button", "aria-expanded": isExpanded, "aria-controls": id, disabled, onClick: toggle, children: [
601
+ /* @__PURE__ */ jsx8("span", { children: title }),
602
+ /* @__PURE__ */ jsx8("span", { className: "fe-expandable-section__chevron", "aria-hidden": "true" })
603
+ ] }),
604
+ /* @__PURE__ */ jsx8("div", { id, className: "fe-expandable-section__content", "aria-hidden": !isExpanded, children: /* @__PURE__ */ jsx8("div", { className: "fe-expandable-section__content-inner", children }) })
605
+ ] });
606
+ }
104
607
  export {
105
608
  Button,
106
609
  ChevronDownIcon,
107
610
  CloseIcon,
611
+ CurrencyInput,
612
+ DateTimePicker,
613
+ Drawer,
614
+ ExpandableSection,
615
+ NumberInput,
108
616
  PlusIcon,
109
617
  SearchIcon,
618
+ Selection,
110
619
  Table,
111
620
  TableBody,
112
621
  TableCaption,
@@ -114,5 +623,8 @@ export {
114
623
  TableHead,
115
624
  TableHeader,
116
625
  TableRow,
626
+ Toggle,
627
+ Tooltip,
628
+ formatNumber,
117
629
  getIconSize
118
630
  };