@bigtablet/design-system 1.17.4 → 1.18.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/dist/index.css CHANGED
@@ -310,6 +310,9 @@
310
310
  .button_variant_danger:active:not(:disabled) {
311
311
  transform: scale(0.98);
312
312
  }
313
+ .button_full_width {
314
+ width: 100%;
315
+ }
313
316
 
314
317
  /* src/ui/form/checkbox/style.scss */
315
318
  .checkbox {
@@ -999,13 +1002,13 @@
999
1002
  gap: 0.25rem;
1000
1003
  }
1001
1004
  }
1002
- .date_picker_full {
1005
+ .date_picker_full_width {
1003
1006
  width: 100%;
1004
1007
  }
1005
- .date_picker_full .date_picker_fields {
1008
+ .date_picker_full_width .date_picker_fields {
1006
1009
  width: 100%;
1007
1010
  }
1008
- .date_picker_full select {
1011
+ .date_picker_full_width select {
1009
1012
  flex: 1;
1010
1013
  min-width: 0;
1011
1014
  }
package/dist/index.d.ts CHANGED
@@ -33,8 +33,10 @@ declare const AlertProvider: React.FC<{
33
33
 
34
34
  interface SpinnerProps {
35
35
  size?: number;
36
+ /** Accessible label for the spinner (default: "Loading") */
37
+ ariaLabel?: string;
36
38
  }
37
- declare const Spinner: ({ size }: SpinnerProps) => react_jsx_runtime.JSX.Element;
39
+ declare const Spinner: ({ size, ariaLabel }: SpinnerProps) => react_jsx_runtime.JSX.Element;
38
40
 
39
41
  interface TopLoadingProps {
40
42
  /** 진행률 (0-100). undefined면 indeterminate 모드 */
@@ -45,8 +47,10 @@ interface TopLoadingProps {
45
47
  height?: number;
46
48
  /** 표시 여부 */
47
49
  isLoading?: boolean;
50
+ /** 프로그레스 바의 접근성 레이블 (기본값: "Page loading") */
51
+ ariaLabel?: string;
48
52
  }
49
- declare const TopLoading: ({ progress, color, height, isLoading, }: TopLoadingProps) => react_jsx_runtime.JSX.Element | null;
53
+ declare const TopLoading: ({ progress, color, height, isLoading, ariaLabel, }: TopLoadingProps) => react_jsx_runtime.JSX.Element | null;
50
54
 
51
55
  interface ToastProviderProps {
52
56
  containerId?: string;
@@ -64,16 +68,22 @@ declare const useToast: (containerId?: string) => {
64
68
  interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
65
69
  variant?: "primary" | "secondary" | "ghost" | "danger";
66
70
  size?: "sm" | "md" | "lg";
71
+ /** Whether the button should take the full width of its container */
72
+ fullWidth?: boolean;
73
+ /**
74
+ * Custom width for the button
75
+ * @deprecated Use `fullWidth` prop or CSS instead
76
+ */
67
77
  width?: string;
68
78
  }
69
- declare const Button: ({ variant, size, width, className, ...props }: ButtonProps) => react_jsx_runtime.JSX.Element;
79
+ declare const Button: ({ variant, size, fullWidth, width, className, style, ...props }: ButtonProps) => react_jsx_runtime.JSX.Element;
70
80
 
71
81
  interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
72
82
  label?: React.ReactNode;
73
83
  size?: "sm" | "md" | "lg";
74
84
  indeterminate?: boolean;
75
85
  }
76
- declare const Checkbox: ({ label, size, indeterminate, className, ...props }: CheckboxProps) => react_jsx_runtime.JSX.Element;
86
+ declare const Checkbox: React.ForwardRefExoticComponent<CheckboxProps & React.RefAttributes<HTMLInputElement>>;
77
87
 
78
88
  interface FileInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
79
89
  label?: string;
@@ -85,7 +95,7 @@ interface RadioProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "
85
95
  label?: React.ReactNode;
86
96
  size?: "sm" | "md" | "lg";
87
97
  }
88
- declare const Radio: ({ label, size, className, ...props }: RadioProps) => react_jsx_runtime.JSX.Element;
98
+ declare const Radio: React.ForwardRefExoticComponent<RadioProps & React.RefAttributes<HTMLInputElement>>;
89
99
 
90
100
  type SelectSize = "sm" | "md" | "lg";
91
101
  type SelectVariant = "outline" | "filled" | "ghost";
@@ -117,8 +127,10 @@ interface SwitchProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>
117
127
  onChange?: (checked: boolean) => void;
118
128
  size?: "sm" | "md" | "lg";
119
129
  disabled?: boolean;
130
+ /** Accessible label for the switch (for screen readers) */
131
+ ariaLabel: string;
120
132
  }
121
- declare const Switch: ({ checked, defaultChecked, onChange, size, disabled, className, ...props }: SwitchProps) => react_jsx_runtime.JSX.Element;
133
+ declare const Switch: React.ForwardRefExoticComponent<SwitchProps & React.RefAttributes<HTMLButtonElement>>;
122
134
 
123
135
  type TextFieldVariant = "outline" | "filled" | "ghost";
124
136
  type TextFieldSize = "sm" | "md" | "lg";
@@ -151,9 +163,15 @@ interface DatePickerProps {
151
163
  minDate?: string;
152
164
  selectableRange?: SelectableRange;
153
165
  disabled?: boolean;
166
+ /** Whether the date picker should take the full width of its container */
167
+ fullWidth?: boolean;
168
+ /**
169
+ * Custom width for the date picker
170
+ * @deprecated Use `fullWidth` prop or CSS instead
171
+ */
154
172
  width?: number | string;
155
173
  }
156
- declare const DatePicker: ({ label, value, onChange, mode, startYear, endYear, minDate, selectableRange, disabled, width, }: DatePickerProps) => react_jsx_runtime.JSX.Element;
174
+ declare const DatePicker: ({ label, value, onChange, mode, startYear, endYear, minDate, selectableRange, disabled, fullWidth, width, }: DatePickerProps) => react_jsx_runtime.JSX.Element;
157
175
 
158
176
  interface PaginationProps {
159
177
  page: number;
@@ -168,7 +186,9 @@ interface ModalProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">
168
186
  closeOnOverlay?: boolean;
169
187
  width?: number | string;
170
188
  title?: React.ReactNode;
189
+ /** Accessible label for the modal (default uses title or "Dialog") */
190
+ ariaLabel?: string;
171
191
  }
172
- declare const Modal: ({ open, onClose, closeOnOverlay, width, title, children, className, ...props }: ModalProps) => react_jsx_runtime.JSX.Element | null;
192
+ declare const Modal: ({ open, onClose, closeOnOverlay, width, title, children, className, ariaLabel, ...props }: ModalProps) => react_jsx_runtime.JSX.Element | null;
173
193
 
174
194
  export { AlertProvider, Button, Card, Checkbox, DatePicker, FileInput, Modal, Pagination, Radio, Select, type SelectOption, Spinner, Switch, TextField, ToastProvider, TopLoading, useAlert, useToast };
package/dist/index.js CHANGED
@@ -1,14 +1,95 @@
1
1
  "use client";
2
2
  import './index.css';
3
- import { jsxs, jsx } from 'react/jsx-runtime';
4
- import * as React3 from 'react';
3
+ import * as React5 from 'react';
5
4
  import { createContext, useContext, useState, useCallback } from 'react';
5
+ import { jsxs, jsx } from 'react/jsx-runtime';
6
6
  import { createPortal } from 'react-dom';
7
7
  import { ToastContainer, Slide, toast } from 'react-toastify';
8
8
  import 'react-toastify/dist/ReactToastify.css';
9
9
  import { ChevronDown, Check } from 'lucide-react';
10
10
 
11
- // src/ui/display/card/index.tsx
11
+ // src/utils/cn.ts
12
+ var cn = (...classes) => {
13
+ const classNames = [];
14
+ for (const item of classes) {
15
+ if (!item) continue;
16
+ if (typeof item === "string" || typeof item === "number") {
17
+ classNames.push(String(item));
18
+ } else if (Array.isArray(item)) {
19
+ const nested = cn(...item);
20
+ if (nested) {
21
+ classNames.push(nested);
22
+ }
23
+ } else if (typeof item === "object") {
24
+ for (const key in item) {
25
+ if (Object.prototype.hasOwnProperty.call(item, key) && item[key]) {
26
+ classNames.push(key);
27
+ }
28
+ }
29
+ }
30
+ }
31
+ return classNames.join(" ");
32
+ };
33
+ var FOCUSABLE_SELECTORS = [
34
+ "a[href]",
35
+ "button:not([disabled])",
36
+ "input:not([disabled])",
37
+ "select:not([disabled])",
38
+ "textarea:not([disabled])",
39
+ '[tabindex]:not([tabindex="-1"])'
40
+ ].join(", ");
41
+ function useFocusTrap(containerRef, isActive) {
42
+ const previousActiveElement = React5.useRef(null);
43
+ React5.useEffect(() => {
44
+ if (!isActive) return;
45
+ const container = containerRef.current;
46
+ if (!container) return;
47
+ previousActiveElement.current = document.activeElement;
48
+ const getFocusableElements = () => {
49
+ return container.querySelectorAll(FOCUSABLE_SELECTORS);
50
+ };
51
+ let wasTabIndexAdded = false;
52
+ const focusableElements = getFocusableElements();
53
+ if (focusableElements.length > 0) {
54
+ focusableElements[0].focus();
55
+ } else {
56
+ container.setAttribute("tabindex", "-1");
57
+ wasTabIndexAdded = true;
58
+ container.focus();
59
+ }
60
+ const handleKeyDown = (e) => {
61
+ if (e.key !== "Tab") return;
62
+ const focusableElements2 = getFocusableElements();
63
+ if (focusableElements2.length === 0) {
64
+ e.preventDefault();
65
+ return;
66
+ }
67
+ const firstElement = focusableElements2[0];
68
+ const lastElement = focusableElements2[focusableElements2.length - 1];
69
+ if (e.shiftKey) {
70
+ if (document.activeElement === firstElement) {
71
+ e.preventDefault();
72
+ lastElement.focus();
73
+ }
74
+ } else {
75
+ if (document.activeElement === lastElement) {
76
+ e.preventDefault();
77
+ firstElement.focus();
78
+ }
79
+ }
80
+ };
81
+ document.addEventListener("keydown", handleKeyDown);
82
+ return () => {
83
+ document.removeEventListener("keydown", handleKeyDown);
84
+ if (wasTabIndexAdded) {
85
+ container.removeAttribute("tabindex");
86
+ }
87
+ if (previousActiveElement.current && previousActiveElement.current.focus) {
88
+ previousActiveElement.current.focus();
89
+ }
90
+ };
91
+ }, [isActive, containerRef]);
92
+ }
12
93
  var Card = ({
13
94
  heading,
14
95
  shadow = "sm",
@@ -18,14 +99,14 @@ var Card = ({
18
99
  children,
19
100
  ...props
20
101
  }) => {
21
- const cls = [
102
+ const cardClassName = cn(
22
103
  "card",
23
104
  `card_shadow_${shadow}`,
24
105
  `card_p_${padding}`,
25
- bordered && "card_bordered",
26
- className ?? ""
27
- ].filter(Boolean).join(" ");
28
- return /* @__PURE__ */ jsxs("div", { className: cls, ...props, children: [
106
+ { card_bordered: bordered },
107
+ className
108
+ );
109
+ return /* @__PURE__ */ jsxs("div", { className: cardClassName, ...props, children: [
29
110
  heading ? /* @__PURE__ */ jsx("div", { className: "card_title", children: heading }) : null,
30
111
  /* @__PURE__ */ jsx("div", { className: "card_body", children })
31
112
  ] });
@@ -133,14 +214,14 @@ var AlertModal = ({
133
214
  }
134
215
  ) });
135
216
  };
136
- var Spinner = ({ size = 24 }) => {
217
+ var Spinner = ({ size = 24, ariaLabel = "Loading" }) => {
137
218
  return /* @__PURE__ */ jsx(
138
219
  "span",
139
220
  {
140
221
  className: "spinner",
141
222
  style: { width: size, height: size },
142
223
  role: "status",
143
- "aria-label": "\uB85C\uB529 \uC911"
224
+ "aria-label": ariaLabel
144
225
  }
145
226
  );
146
227
  };
@@ -148,7 +229,8 @@ var TopLoading = ({
148
229
  progress,
149
230
  color,
150
231
  height = 3,
151
- isLoading = true
232
+ isLoading = true,
233
+ ariaLabel = "Page loading"
152
234
  }) => {
153
235
  if (!isLoading) return null;
154
236
  const isIndeterminate = progress === void 0;
@@ -161,7 +243,7 @@ var TopLoading = ({
161
243
  "aria-valuemin": 0,
162
244
  "aria-valuemax": 100,
163
245
  "aria-valuenow": isIndeterminate ? void 0 : progress,
164
- "aria-label": "\uD398\uC774\uC9C0 \uB85C\uB529 \uC911",
246
+ "aria-label": ariaLabel,
165
247
  children: /* @__PURE__ */ jsx(
166
248
  "div",
167
249
  {
@@ -210,49 +292,51 @@ var useToast = (containerId = "default") => {
210
292
  var Button = ({
211
293
  variant = "primary",
212
294
  size = "md",
213
- width = "100%",
295
+ fullWidth = true,
296
+ width,
214
297
  className,
298
+ style,
215
299
  ...props
216
300
  }) => {
217
- const buttonClassName = [
301
+ const buttonClassName = cn(
218
302
  "button",
219
303
  `button_variant_${variant}`,
220
304
  `button_size_${size}`,
221
- className ?? ""
222
- ].filter(Boolean).join(" ");
223
- return /* @__PURE__ */ jsx("button", { className: buttonClassName, style: { width }, ...props });
224
- };
225
- var Checkbox = ({
226
- label,
227
- size = "md",
228
- indeterminate,
229
- className,
230
- ...props
231
- }) => {
232
- const inputRef = React3.useRef(null);
233
- React3.useEffect(() => {
234
- if (!inputRef.current) return;
235
- inputRef.current.indeterminate = Boolean(indeterminate);
236
- }, [indeterminate]);
237
- const rootClassName = [
238
- "checkbox",
239
- `checkbox_size_${size}`,
240
- className ?? ""
241
- ].filter(Boolean).join(" ");
242
- return /* @__PURE__ */ jsxs("label", { className: rootClassName, children: [
243
- /* @__PURE__ */ jsx(
244
- "input",
245
- {
246
- ref: inputRef,
247
- type: "checkbox",
248
- className: "checkbox_input",
249
- ...props
250
- }
251
- ),
252
- /* @__PURE__ */ jsx("span", { className: "checkbox_box", "aria-hidden": "true" }),
253
- label ? /* @__PURE__ */ jsx("span", { className: "checkbox_label", children: label }) : null
254
- ] });
305
+ fullWidth && !width && "button_full_width",
306
+ className
307
+ );
308
+ const buttonStyle = width ? { ...style, width } : style;
309
+ return /* @__PURE__ */ jsx("button", { className: buttonClassName, style: buttonStyle, ...props });
255
310
  };
311
+ var Checkbox = React5.forwardRef(
312
+ ({ label, size = "md", indeterminate, className, ...props }, ref) => {
313
+ const inputRef = React5.useRef(null);
314
+ React5.useImperativeHandle(ref, () => inputRef.current);
315
+ React5.useEffect(() => {
316
+ if (!inputRef.current) return;
317
+ inputRef.current.indeterminate = Boolean(indeterminate);
318
+ }, [indeterminate]);
319
+ const rootClassName = cn(
320
+ "checkbox",
321
+ `checkbox_size_${size}`,
322
+ className
323
+ );
324
+ return /* @__PURE__ */ jsxs("label", { className: rootClassName, children: [
325
+ /* @__PURE__ */ jsx(
326
+ "input",
327
+ {
328
+ ref: inputRef,
329
+ type: "checkbox",
330
+ className: "checkbox_input",
331
+ ...props
332
+ }
333
+ ),
334
+ /* @__PURE__ */ jsx("span", { className: "checkbox_box", "aria-hidden": "true" }),
335
+ label ? /* @__PURE__ */ jsx("span", { className: "checkbox_label", children: label }) : null
336
+ ] });
337
+ }
338
+ );
339
+ Checkbox.displayName = "Checkbox";
256
340
  var FileInput = ({
257
341
  label = "\uD30C\uC77C \uC120\uD0DD",
258
342
  onFiles,
@@ -260,7 +344,7 @@ var FileInput = ({
260
344
  disabled,
261
345
  ...props
262
346
  }) => {
263
- const inputId = React3.useId();
347
+ const inputId = React5.useId();
264
348
  const rootClassName = [
265
349
  "file_input",
266
350
  disabled && "file_input_disabled",
@@ -281,18 +365,21 @@ var FileInput = ({
281
365
  /* @__PURE__ */ jsx("label", { htmlFor: inputId, className: "file_input_label", children: label })
282
366
  ] });
283
367
  };
284
- var Radio = ({ label, size = "md", className, ...props }) => {
285
- const rootClassName = [
286
- "radio",
287
- `radio_size_${size}`,
288
- className ?? ""
289
- ].filter(Boolean).join(" ");
290
- return /* @__PURE__ */ jsxs("label", { className: rootClassName, children: [
291
- /* @__PURE__ */ jsx("input", { type: "radio", className: "radio_input", ...props }),
292
- /* @__PURE__ */ jsx("span", { className: "radio_dot", "aria-hidden": "true" }),
293
- label ? /* @__PURE__ */ jsx("span", { className: "radio_label", children: label }) : null
294
- ] });
295
- };
368
+ var Radio = React5.forwardRef(
369
+ ({ label, size = "md", className, ...props }, ref) => {
370
+ const rootClassName = cn(
371
+ "radio",
372
+ `radio_size_${size}`,
373
+ className
374
+ );
375
+ return /* @__PURE__ */ jsxs("label", { className: rootClassName, children: [
376
+ /* @__PURE__ */ jsx("input", { ref, type: "radio", className: "radio_input", ...props }),
377
+ /* @__PURE__ */ jsx("span", { className: "radio_dot", "aria-hidden": "true" }),
378
+ label ? /* @__PURE__ */ jsx("span", { className: "radio_label", children: label }) : null
379
+ ] });
380
+ }
381
+ );
382
+ Radio.displayName = "Radio";
296
383
  var Select = ({
297
384
  id,
298
385
  label,
@@ -308,21 +395,21 @@ var Select = ({
308
395
  className,
309
396
  textAlign = "left"
310
397
  }) => {
311
- const internalId = React3.useId();
398
+ const internalId = React5.useId();
312
399
  const selectId = id ?? internalId;
313
400
  const isControlled = value !== void 0;
314
- const [internalValue, setInternalValue] = React3.useState(defaultValue);
401
+ const [internalValue, setInternalValue] = React5.useState(defaultValue);
315
402
  const currentValue = isControlled ? value ?? null : internalValue;
316
- const [isOpen, setIsOpen] = React3.useState(false);
317
- const [activeIndex, setActiveIndex] = React3.useState(-1);
318
- const [dropUp, setDropUp] = React3.useState(false);
319
- const wrapperRef = React3.useRef(null);
320
- const controlRef = React3.useRef(null);
321
- const currentOption = React3.useMemo(
403
+ const [isOpen, setIsOpen] = React5.useState(false);
404
+ const [activeIndex, setActiveIndex] = React5.useState(-1);
405
+ const [dropUp, setDropUp] = React5.useState(false);
406
+ const wrapperRef = React5.useRef(null);
407
+ const controlRef = React5.useRef(null);
408
+ const currentOption = React5.useMemo(
322
409
  () => options.find((o) => o.value === currentValue) ?? null,
323
410
  [options, currentValue]
324
411
  );
325
- const setValue = React3.useCallback(
412
+ const setValue = React5.useCallback(
326
413
  (next) => {
327
414
  const option = options.find((o) => o.value === next) ?? null;
328
415
  if (!isControlled) setInternalValue(next);
@@ -330,15 +417,14 @@ var Select = ({
330
417
  },
331
418
  [isControlled, onChange, options]
332
419
  );
333
- React3.useEffect(() => {
334
- const onDocClick = (e) => {
335
- if (!wrapperRef.current) return;
336
- if (!wrapperRef.current.contains(e.target)) {
337
- setIsOpen(false);
338
- }
339
- };
340
- document.addEventListener("mousedown", onDocClick);
341
- return () => document.removeEventListener("mousedown", onDocClick);
420
+ const handleOutsideClick = React5.useEffectEvent((e) => {
421
+ if (!wrapperRef.current?.contains(e.target)) {
422
+ setIsOpen(false);
423
+ }
424
+ });
425
+ React5.useEffect(() => {
426
+ document.addEventListener("mousedown", handleOutsideClick);
427
+ return () => document.removeEventListener("mousedown", handleOutsideClick);
342
428
  }, []);
343
429
  const moveActive = (dir) => {
344
430
  if (!isOpen) {
@@ -401,12 +487,12 @@ var Select = ({
401
487
  break;
402
488
  }
403
489
  };
404
- React3.useEffect(() => {
490
+ React5.useEffect(() => {
405
491
  if (!isOpen) return;
406
492
  const idx = options.findIndex((o) => o.value === currentValue && !o.disabled);
407
493
  setActiveIndex(idx >= 0 ? idx : Math.max(0, options.findIndex((o) => !o.disabled)));
408
494
  }, [isOpen, options, currentValue]);
409
- React3.useLayoutEffect(() => {
495
+ React5.useLayoutEffect(() => {
410
496
  if (!isOpen || !controlRef.current) return;
411
497
  const rect = controlRef.current.getBoundingClientRect();
412
498
  const listHeight = Math.min(options.length * 40, 288);
@@ -414,18 +500,14 @@ var Select = ({
414
500
  const spaceAbove = rect.top;
415
501
  setDropUp(spaceBelow < listHeight && spaceAbove > spaceBelow);
416
502
  }, [isOpen, options.length]);
417
- const rootClassName = ["select", className ?? ""].filter(Boolean).join(" ");
418
- const controlClassName = [
503
+ const rootClassName = cn("select", className);
504
+ const controlClassName = cn(
419
505
  "select_control",
420
506
  `select_variant_${variant}`,
421
507
  `select_size_${size}`,
422
- isOpen && "is_open",
423
- disabled && "is_disabled"
424
- ].filter(Boolean).join(" ");
425
- const listClassName = [
426
- "select_list",
427
- dropUp && "select_list_up"
428
- ].filter(Boolean).join(" ");
508
+ { is_open: isOpen, is_disabled: disabled }
509
+ );
510
+ const listClassName = cn("select_list", { select_list_up: dropUp });
429
511
  return /* @__PURE__ */ jsxs("div", { ref: wrapperRef, className: rootClassName, style: fullWidth ? { width: "100%" } : void 0, children: [
430
512
  label && /* @__PURE__ */ jsx("label", { htmlFor: selectId, className: "select_label", children: label }),
431
513
  /* @__PURE__ */ jsxs(
@@ -463,12 +545,10 @@ var Select = ({
463
545
  children: options.map((opt, i) => {
464
546
  const selected = currentValue === opt.value;
465
547
  const active = i === activeIndex;
466
- const optionClassName = [
548
+ const optionClassName = cn(
467
549
  "select_option",
468
- selected && "is_selected",
469
- active && "is_active",
470
- opt.disabled && "is_disabled"
471
- ].filter(Boolean).join(" ");
550
+ { is_selected: selected, is_active: active, is_disabled: opt.disabled }
551
+ );
472
552
  return /* @__PURE__ */ jsxs(
473
553
  "li",
474
554
  {
@@ -493,46 +573,51 @@ var Select = ({
493
573
  )
494
574
  ] });
495
575
  };
496
- var Switch = ({
497
- checked,
498
- defaultChecked,
499
- onChange,
500
- size = "md",
501
- disabled,
502
- className,
503
- ...props
504
- }) => {
505
- const isControlled = checked !== void 0;
506
- const [innerChecked, setInnerChecked] = React3.useState(!!defaultChecked);
507
- const isOn = isControlled ? !!checked : innerChecked;
508
- const handleToggle = () => {
509
- if (disabled) return;
510
- const next = !isOn;
511
- if (!isControlled) setInnerChecked(next);
512
- onChange?.(next);
513
- };
514
- const rootClassName = [
515
- "switch",
516
- `switch_size_${size}`,
517
- isOn && "switch_on",
518
- disabled && "switch_disabled",
519
- className ?? ""
520
- ].filter(Boolean).join(" ");
521
- return /* @__PURE__ */ jsx(
522
- "button",
523
- {
524
- type: "button",
525
- role: "switch",
526
- "aria-checked": isOn,
527
- disabled,
528
- onClick: handleToggle,
529
- className: rootClassName,
530
- ...props,
531
- children: /* @__PURE__ */ jsx("span", { className: "switch_thumb" })
532
- }
533
- );
534
- };
535
- var TextField = React3.forwardRef(
576
+ var Switch = React5.forwardRef(
577
+ ({
578
+ checked,
579
+ defaultChecked,
580
+ onChange,
581
+ size = "md",
582
+ disabled,
583
+ className,
584
+ ariaLabel,
585
+ ...props
586
+ }, ref) => {
587
+ const isControlled = checked !== void 0;
588
+ const [innerChecked, setInnerChecked] = React5.useState(!!defaultChecked);
589
+ const isOn = isControlled ? !!checked : innerChecked;
590
+ const handleToggle = () => {
591
+ if (disabled) return;
592
+ const next = !isOn;
593
+ if (!isControlled) setInnerChecked(next);
594
+ onChange?.(next);
595
+ };
596
+ const rootClassName = cn(
597
+ "switch",
598
+ `switch_size_${size}`,
599
+ { switch_on: isOn, switch_disabled: disabled },
600
+ className
601
+ );
602
+ return /* @__PURE__ */ jsx(
603
+ "button",
604
+ {
605
+ ref,
606
+ type: "button",
607
+ role: "switch",
608
+ "aria-checked": isOn,
609
+ "aria-label": ariaLabel,
610
+ disabled,
611
+ onClick: handleToggle,
612
+ className: rootClassName,
613
+ ...props,
614
+ children: /* @__PURE__ */ jsx("span", { className: "switch_thumb" })
615
+ }
616
+ );
617
+ }
618
+ );
619
+ Switch.displayName = "Switch";
620
+ var TextField = React5.forwardRef(
536
621
  ({
537
622
  id,
538
623
  label,
@@ -551,37 +636,41 @@ var TextField = React3.forwardRef(
551
636
  transformValue,
552
637
  ...props
553
638
  }, ref) => {
554
- const inputId = id ?? React3.useId();
639
+ const inputId = id ?? React5.useId();
555
640
  const helperId = helperText ? `${inputId}-help` : void 0;
556
641
  const isControlled = value !== void 0;
557
642
  const applyTransform = (nextValue) => transformValue ? transformValue(nextValue) : nextValue;
558
- const [innerValue, setInnerValue] = React3.useState(
643
+ const [innerValue, setInnerValue] = React5.useState(
559
644
  () => applyTransform(value ?? defaultValue ?? "")
560
645
  );
561
- const isComposingRef = React3.useRef(false);
562
- React3.useEffect(() => {
646
+ const isComposingRef = React5.useRef(false);
647
+ React5.useEffect(() => {
563
648
  if (!isControlled) return;
564
649
  setInnerValue(applyTransform(value ?? ""));
565
650
  }, [isControlled, value, transformValue]);
566
- const rootClassName = [
651
+ const rootClassName = cn(
567
652
  "text_field",
568
- fullWidth && "text_field_full_width",
569
- className ?? ""
570
- ].filter(Boolean).join(" ");
571
- const inputClassName = [
653
+ { text_field_full_width: fullWidth },
654
+ className
655
+ );
656
+ const inputClassName = cn(
572
657
  "text_field_input",
573
658
  `text_field_variant_${variant}`,
574
659
  `text_field_size_${size}`,
575
- leftIcon && "text_field_with_left",
576
- rightIcon && "text_field_with_right",
577
- error && "text_field_error",
578
- success && "text_field_success"
579
- ].filter(Boolean).join(" ");
580
- const helperClassName = [
660
+ {
661
+ text_field_with_left: !!leftIcon,
662
+ text_field_with_right: !!rightIcon,
663
+ text_field_error: !!error,
664
+ text_field_success: !!success
665
+ }
666
+ );
667
+ const helperClassName = cn(
581
668
  "text_field_helper",
582
- error && "text_field_helper_error",
583
- success && "text_field_helper_success"
584
- ].filter(Boolean).join(" ");
669
+ {
670
+ text_field_helper_error: error,
671
+ text_field_helper_success: success
672
+ }
673
+ );
585
674
  return /* @__PURE__ */ jsxs("div", { className: rootClassName, children: [
586
675
  label ? /* @__PURE__ */ jsx("label", { className: "text_field_label", htmlFor: inputId, children: label }) : null,
587
676
  /* @__PURE__ */ jsxs("div", { className: "text_field_wrap", children: [
@@ -638,6 +727,7 @@ var DatePicker = ({
638
727
  minDate,
639
728
  selectableRange = "all",
640
729
  disabled,
730
+ fullWidth = true,
641
731
  width
642
732
  }) => {
643
733
  const today = /* @__PURE__ */ new Date();
@@ -665,7 +755,8 @@ var DatePicker = ({
665
755
  onChange(`${yy}-${pad(mm)}-${pad(safeDay)}`);
666
756
  };
667
757
  const containerStyle = width ? { width: normalizeWidth(width) } : void 0;
668
- return /* @__PURE__ */ jsxs("div", { className: "date_picker date_picker_full", style: containerStyle, children: [
758
+ const rootClassName = cn("date_picker", { date_picker_full_width: fullWidth && !width });
759
+ return /* @__PURE__ */ jsxs("div", { className: rootClassName, style: containerStyle, children: [
669
760
  label && /* @__PURE__ */ jsx("label", { className: "date_picker_label", children: label }),
670
761
  /* @__PURE__ */ jsxs("div", { className: "date_picker_fields", children: [
671
762
  /* @__PURE__ */ jsxs(
@@ -747,7 +838,7 @@ var getPaginationItems = (page, totalPages) => {
747
838
  var Pagination = ({ page, totalPages, onChange }) => {
748
839
  const prevDisabled = page <= 1;
749
840
  const nextDisabled = page >= totalPages;
750
- const items = React3.useMemo(
841
+ const items = React5.useMemo(
751
842
  () => getPaginationItems(page, totalPages),
752
843
  [page, totalPages]
753
844
  );
@@ -767,10 +858,10 @@ var Pagination = ({ page, totalPages, onChange }) => {
767
858
  return /* @__PURE__ */ jsx("span", { className: "pagination_ellipsis", "aria-hidden": "true", children: "\u2026" }, `e-${idx}`);
768
859
  }
769
860
  const isActive = it === page;
770
- const buttonClassName = [
861
+ const buttonClassName = cn(
771
862
  "pagination_page_button",
772
- isActive && "pagination_active"
773
- ].filter(Boolean).join(" ");
863
+ { pagination_active: isActive }
864
+ );
774
865
  return /* @__PURE__ */ jsx(
775
866
  "button",
776
867
  {
@@ -803,28 +894,55 @@ var Modal = ({
803
894
  title,
804
895
  children,
805
896
  className,
897
+ ariaLabel,
806
898
  ...props
807
899
  }) => {
808
- React3.useEffect(() => {
900
+ const panelRef = React5.useRef(null);
901
+ useFocusTrap(panelRef, open);
902
+ const handleEscape = React5.useEffectEvent((e) => {
903
+ if (e.key === "Escape") onClose?.();
904
+ });
905
+ React5.useEffect(() => {
809
906
  if (!open) return;
810
- const onKeyDown = (e) => {
811
- if (e.key === "Escape") onClose?.();
907
+ document.addEventListener("keydown", handleEscape);
908
+ return () => document.removeEventListener("keydown", handleEscape);
909
+ }, [open]);
910
+ React5.useEffect(() => {
911
+ if (!open) return;
912
+ const body = document.body;
913
+ const openModals = parseInt(body.dataset.openModals || "0", 10);
914
+ if (openModals === 0) {
915
+ body.dataset.originalOverflow = window.getComputedStyle(body).overflow;
916
+ body.style.overflow = "hidden";
917
+ }
918
+ body.dataset.openModals = String(openModals + 1);
919
+ return () => {
920
+ const currentOpenModals = parseInt(body.dataset.openModals || "1", 10);
921
+ const nextOpenModals = currentOpenModals - 1;
922
+ if (nextOpenModals === 0) {
923
+ body.style.overflow = body.dataset.originalOverflow || "";
924
+ delete body.dataset.openModals;
925
+ delete body.dataset.originalOverflow;
926
+ } else {
927
+ body.dataset.openModals = String(nextOpenModals);
928
+ }
812
929
  };
813
- document.addEventListener("keydown", onKeyDown);
814
- return () => document.removeEventListener("keydown", onKeyDown);
815
- }, [open, onClose]);
930
+ }, [open]);
816
931
  if (!open) return null;
817
- const panelClassName = ["modal_panel", className].filter(Boolean).join(" ");
932
+ const panelClassName = cn("modal_panel", className);
933
+ const modalAriaLabel = ariaLabel ?? (typeof title === "string" ? title : "Dialog");
818
934
  return /* @__PURE__ */ jsx(
819
935
  "div",
820
936
  {
821
937
  className: "modal",
822
938
  role: "dialog",
823
939
  "aria-modal": "true",
940
+ "aria-label": modalAriaLabel,
824
941
  onClick: () => closeOnOverlay && onClose?.(),
825
942
  children: /* @__PURE__ */ jsxs(
826
943
  "div",
827
944
  {
945
+ ref: panelRef,
828
946
  className: panelClassName,
829
947
  style: { width },
830
948
  onClick: (e) => e.stopPropagation(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigtablet/design-system",
3
- "version": "1.17.4",
3
+ "version": "1.18.0",
4
4
  "description": "Bigtablet Design System UI Components",
5
5
  "type": "module",
6
6
  "types": "dist/index.d.ts",
@@ -47,7 +47,9 @@
47
47
  "dev": "tsup --watch",
48
48
  "storybook": "storybook dev -p 6006",
49
49
  "build:sb": "storybook build",
50
- "test": "echo \"no tests yet\"",
50
+ "test": "vitest run --project unit",
51
+ "test:watch": "vitest --project unit",
52
+ "test:coverage": "vitest run --project unit --coverage",
51
53
  "chromatic": "npx chromatic --project-token=chpt_2f758912f0dde5c --build-script-name=build:sb"
52
54
  },
53
55
  "keywords": [
@@ -83,6 +85,9 @@
83
85
  "@storybook/addon-vitest": "10.1.11",
84
86
  "@storybook/react": "10.1.11",
85
87
  "@storybook/react-vite": "10.1.11",
88
+ "@testing-library/dom": "^10.4.1",
89
+ "@testing-library/jest-dom": "^6.9.1",
90
+ "@testing-library/react": "^16.3.2",
86
91
  "@types/node": "^24",
87
92
  "@types/react": "^19",
88
93
  "@types/react-dom": "^19",
@@ -91,8 +96,9 @@
91
96
  "chromatic": "^13.3.3",
92
97
  "conventional-changelog-conventionalcommits": "^9.1.0",
93
98
  "esbuild-sass-plugin": "^3",
99
+ "jsdom": "^28.0.0",
94
100
  "lucide-react": "^0.552.0",
95
- "next": "16.0.10",
101
+ "next": "16.1.5",
96
102
  "playwright": "^1.57.0",
97
103
  "react": "19.2.0",
98
104
  "react-dom": "19.2.0",