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