@bigtablet/design-system 3.11.0 → 3.13.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/README.md +2 -1
- package/dist/index.css +312 -8
- package/dist/index.d.ts +111 -26
- package/dist/index.js +139 -59
- package/dist/styles/a11y/_index.scss +48 -1
- package/dist/styles/border-width/_index.scss +29 -0
- package/dist/styles/typography/_index.scss +29 -0
- package/dist/vanilla/bigtablet.min.css +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -243,6 +243,7 @@ function useAnchoredPosition({
|
|
|
243
243
|
}, [open, placement, gap, padding, anchorRef, floatingRef]);
|
|
244
244
|
return state;
|
|
245
245
|
}
|
|
246
|
+
var SKIP_AUTOFOCUS_ATTR = "data-focus-trap-skip-autofocus";
|
|
246
247
|
var FOCUSABLE_SELECTORS = [
|
|
247
248
|
"a[href]",
|
|
248
249
|
"button:not([disabled])",
|
|
@@ -263,8 +264,9 @@ function useFocusTrap(containerRef, isActive) {
|
|
|
263
264
|
};
|
|
264
265
|
let wasTabIndexAdded = false;
|
|
265
266
|
const focusableElements = getFocusableElements();
|
|
266
|
-
|
|
267
|
-
|
|
267
|
+
const initialTarget = Array.from(focusableElements).find((el) => !el.hasAttribute(SKIP_AUTOFOCUS_ATTR)) ?? focusableElements[0];
|
|
268
|
+
if (initialTarget) {
|
|
269
|
+
initialTarget.focus();
|
|
268
270
|
} else {
|
|
269
271
|
container.setAttribute("tabindex", "-1");
|
|
270
272
|
wasTabIndexAdded = true;
|
|
@@ -542,15 +544,28 @@ var EmptyState = ({
|
|
|
542
544
|
description,
|
|
543
545
|
action,
|
|
544
546
|
size = "md",
|
|
547
|
+
fillHeight,
|
|
545
548
|
className,
|
|
546
549
|
...props
|
|
547
550
|
}) => {
|
|
548
|
-
return /* @__PURE__ */ jsxs(
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
551
|
+
return /* @__PURE__ */ jsxs(
|
|
552
|
+
"div",
|
|
553
|
+
{
|
|
554
|
+
className: cn(
|
|
555
|
+
"empty_state",
|
|
556
|
+
`empty_state_size_${size}`,
|
|
557
|
+
fillHeight && "empty_state_fill_height",
|
|
558
|
+
className
|
|
559
|
+
),
|
|
560
|
+
...props,
|
|
561
|
+
children: [
|
|
562
|
+
illustration && /* @__PURE__ */ jsx("div", { className: "empty_state_illustration", "aria-hidden": "true", children: illustration }),
|
|
563
|
+
title && /* @__PURE__ */ jsx("h3", { className: "empty_state_title", children: title }),
|
|
564
|
+
description && /* @__PURE__ */ jsx("p", { className: "empty_state_description", children: description }),
|
|
565
|
+
action && /* @__PURE__ */ jsx("div", { className: "empty_state_action", children: action })
|
|
566
|
+
]
|
|
567
|
+
}
|
|
568
|
+
);
|
|
554
569
|
};
|
|
555
570
|
var DEFAULT_ICON_SIZE = {
|
|
556
571
|
page: 48,
|
|
@@ -667,11 +682,12 @@ var BottomNavSpacer = ({ className, ...props }) => {
|
|
|
667
682
|
var Breadcrumb = ({
|
|
668
683
|
items,
|
|
669
684
|
separator,
|
|
685
|
+
navLabel = "\uD604\uC7AC \uC704\uCE58",
|
|
670
686
|
className,
|
|
671
687
|
...props
|
|
672
688
|
}) => {
|
|
673
689
|
const sep = separator ?? /* @__PURE__ */ jsx(ChevronRight, { size: iconSize.xs, "aria-hidden": "true" });
|
|
674
|
-
return /* @__PURE__ */ jsx("nav", { "aria-label":
|
|
690
|
+
return /* @__PURE__ */ jsx("nav", { "aria-label": navLabel, className: cn("breadcrumb", className), ...props, children: /* @__PURE__ */ jsx("ol", { className: "breadcrumb_list", children: items.map((item, idx) => {
|
|
675
691
|
const isLast = idx === items.length - 1;
|
|
676
692
|
return (
|
|
677
693
|
// biome-ignore lint/suspicious/noArrayIndexKey: breadcrumb items have stable order
|
|
@@ -1419,7 +1435,7 @@ var Popover = ({
|
|
|
1419
1435
|
ref: popoverRef,
|
|
1420
1436
|
role: "dialog",
|
|
1421
1437
|
tabIndex: -1,
|
|
1422
|
-
"aria-label":
|
|
1438
|
+
"aria-label": ariaLabel,
|
|
1423
1439
|
"aria-labelledby": ariaLabelledby,
|
|
1424
1440
|
style,
|
|
1425
1441
|
className: cn("popover", className),
|
|
@@ -2433,6 +2449,29 @@ var MediaCard = ({
|
|
|
2433
2449
|
] })
|
|
2434
2450
|
] });
|
|
2435
2451
|
};
|
|
2452
|
+
var Prose = ({ size = "md", className, children, ref, ...props }) => {
|
|
2453
|
+
const rootRef = React16.useRef(null);
|
|
2454
|
+
React16.useImperativeHandle(ref, () => rootRef.current, []);
|
|
2455
|
+
useSafeLayoutEffect(() => {
|
|
2456
|
+
const root = rootRef.current;
|
|
2457
|
+
if (!root) return;
|
|
2458
|
+
const targets = Array.from(root.querySelectorAll("pre, table"));
|
|
2459
|
+
const sync = () => {
|
|
2460
|
+
for (const el of targets) {
|
|
2461
|
+
if (el.scrollWidth > el.clientWidth) el.setAttribute("tabindex", "0");
|
|
2462
|
+
else el.removeAttribute("tabindex");
|
|
2463
|
+
}
|
|
2464
|
+
};
|
|
2465
|
+
sync();
|
|
2466
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
2467
|
+
const observer = new ResizeObserver(sync);
|
|
2468
|
+
observer.observe(root);
|
|
2469
|
+
for (const el of targets) observer.observe(el);
|
|
2470
|
+
return () => observer.disconnect();
|
|
2471
|
+
}, [children]);
|
|
2472
|
+
return /* @__PURE__ */ jsx("div", { ref: rootRef, className: cn("prose", `prose_size_${size}`, className), ...props, children });
|
|
2473
|
+
};
|
|
2474
|
+
Prose.displayName = "Prose";
|
|
2436
2475
|
var Skeleton = ({
|
|
2437
2476
|
variant = "text",
|
|
2438
2477
|
width,
|
|
@@ -2873,7 +2912,7 @@ var LinearProgress = ({
|
|
|
2873
2912
|
}
|
|
2874
2913
|
);
|
|
2875
2914
|
};
|
|
2876
|
-
var Spinner = ({ size = 24, ariaLabel = "
|
|
2915
|
+
var Spinner = ({ size = 24, ariaLabel = "\uB85C\uB529 \uC911" }) => {
|
|
2877
2916
|
return /* @__PURE__ */ jsx(
|
|
2878
2917
|
"span",
|
|
2879
2918
|
{
|
|
@@ -2951,7 +2990,8 @@ var ToastItemComponent = ({ item, onRemove, closeAriaLabel }) => {
|
|
|
2951
2990
|
var ToastProvider = ({
|
|
2952
2991
|
children,
|
|
2953
2992
|
maxCount = 5,
|
|
2954
|
-
closeAriaLabel = "
|
|
2993
|
+
closeAriaLabel = "\uB2EB\uAE30",
|
|
2994
|
+
regionLabel = "\uC54C\uB9BC"
|
|
2955
2995
|
}) => {
|
|
2956
2996
|
const [toasts, setToasts] = React16.useState([]);
|
|
2957
2997
|
const isMounted = useIsMounted();
|
|
@@ -2977,7 +3017,7 @@ var ToastProvider = ({
|
|
|
2977
3017
|
role: "region",
|
|
2978
3018
|
"aria-live": "polite",
|
|
2979
3019
|
"aria-atomic": "false",
|
|
2980
|
-
"aria-label":
|
|
3020
|
+
"aria-label": regionLabel,
|
|
2981
3021
|
children: toasts.map((item) => /* @__PURE__ */ jsx(
|
|
2982
3022
|
ToastItemComponent,
|
|
2983
3023
|
{
|
|
@@ -3018,7 +3058,7 @@ var TopLoading = ({
|
|
|
3018
3058
|
color,
|
|
3019
3059
|
height = 3,
|
|
3020
3060
|
isLoading = true,
|
|
3021
|
-
ariaLabel = "
|
|
3061
|
+
ariaLabel = "\uD398\uC774\uC9C0 \uB85C\uB529 \uC911"
|
|
3022
3062
|
}) => {
|
|
3023
3063
|
if (!isLoading) return null;
|
|
3024
3064
|
const isIndeterminate = progress === void 0;
|
|
@@ -3050,7 +3090,7 @@ var Dropdown = (props) => {
|
|
|
3050
3090
|
const {
|
|
3051
3091
|
id,
|
|
3052
3092
|
label,
|
|
3053
|
-
placeholder = "
|
|
3093
|
+
placeholder = "\uC120\uD0DD\u2026",
|
|
3054
3094
|
options,
|
|
3055
3095
|
disabled,
|
|
3056
3096
|
size = "md",
|
|
@@ -3399,11 +3439,11 @@ var DatePicker = ({
|
|
|
3399
3439
|
disabled,
|
|
3400
3440
|
fullWidth = true,
|
|
3401
3441
|
width,
|
|
3402
|
-
yearLabel = "
|
|
3403
|
-
monthLabel = "
|
|
3404
|
-
dayLabel = "
|
|
3405
|
-
minDateSrFormat = "
|
|
3406
|
-
selectableRangeUntilTodaySrText = "
|
|
3442
|
+
yearLabel = "\uB144",
|
|
3443
|
+
monthLabel = "\uC6D4",
|
|
3444
|
+
dayLabel = "\uC77C",
|
|
3445
|
+
minDateSrFormat = "\uCD5C\uC18C \uB0A0\uC9DC: {date}",
|
|
3446
|
+
selectableRangeUntilTodaySrText = "\uC624\uB298\uAE4C\uC9C0 \uC120\uD0DD \uAC00\uB2A5"
|
|
3407
3447
|
}) => {
|
|
3408
3448
|
const groupId = React16.useId();
|
|
3409
3449
|
const constraintId = React16.useId();
|
|
@@ -3582,7 +3622,7 @@ var DatePicker = ({
|
|
|
3582
3622
|
] });
|
|
3583
3623
|
};
|
|
3584
3624
|
var FileInput = ({
|
|
3585
|
-
label = "
|
|
3625
|
+
label = "\uD30C\uC77C \uC120\uD0DD",
|
|
3586
3626
|
onFiles,
|
|
3587
3627
|
supportingText,
|
|
3588
3628
|
preview = false,
|
|
@@ -4413,7 +4453,7 @@ var Textarea = ({
|
|
|
4413
4453
|
};
|
|
4414
4454
|
Textarea.displayName = "Textarea";
|
|
4415
4455
|
var ClearIcon = () => /* @__PURE__ */ jsx(X, { size: iconSize.lg, "aria-hidden": "true" });
|
|
4416
|
-
var DEFAULT_PASSWORD_TOGGLE_LABELS = { show: "
|
|
4456
|
+
var DEFAULT_PASSWORD_TOGGLE_LABELS = { show: "\uBE44\uBC00\uBC88\uD638 \uD45C\uC2DC", hide: "\uBE44\uBC00\uBC88\uD638 \uC228\uAE30\uAE30" };
|
|
4417
4457
|
var TextField = ({
|
|
4418
4458
|
id,
|
|
4419
4459
|
label,
|
|
@@ -4421,6 +4461,7 @@ var TextField = ({
|
|
|
4421
4461
|
supportingText,
|
|
4422
4462
|
error,
|
|
4423
4463
|
success,
|
|
4464
|
+
identifier,
|
|
4424
4465
|
leadingIcon,
|
|
4425
4466
|
trailingIcon,
|
|
4426
4467
|
leadingAction,
|
|
@@ -4428,6 +4469,7 @@ var TextField = ({
|
|
|
4428
4469
|
showPasswordToggle,
|
|
4429
4470
|
passwordToggleLabels,
|
|
4430
4471
|
clearable,
|
|
4472
|
+
clearLabel = "\uC9C0\uC6B0\uAE30",
|
|
4431
4473
|
type,
|
|
4432
4474
|
fullWidth,
|
|
4433
4475
|
size = "md",
|
|
@@ -4507,7 +4549,7 @@ var TextField = ({
|
|
|
4507
4549
|
type: "button",
|
|
4508
4550
|
className: "text_field_clear",
|
|
4509
4551
|
onClick: handleClear,
|
|
4510
|
-
"aria-label":
|
|
4552
|
+
"aria-label": clearLabel,
|
|
4511
4553
|
disabled: props.disabled,
|
|
4512
4554
|
children: /* @__PURE__ */ jsx(ClearIcon, {})
|
|
4513
4555
|
}
|
|
@@ -4529,7 +4571,7 @@ var TextField = ({
|
|
|
4529
4571
|
{
|
|
4530
4572
|
id: inputId,
|
|
4531
4573
|
ref,
|
|
4532
|
-
className: "text_field_input",
|
|
4574
|
+
className: cn("text_field_input", identifier && "text_field_input_identifier"),
|
|
4533
4575
|
"aria-invalid": isError,
|
|
4534
4576
|
"aria-describedby": helperId,
|
|
4535
4577
|
"aria-label": !showLabel ? label : void 0,
|
|
@@ -4661,14 +4703,15 @@ var Pagination = ({
|
|
|
4661
4703
|
totalPages,
|
|
4662
4704
|
onPageChange,
|
|
4663
4705
|
onChange,
|
|
4664
|
-
prevLabel = "
|
|
4665
|
-
nextLabel = "
|
|
4706
|
+
prevLabel = "\uC774\uC804 \uD398\uC774\uC9C0",
|
|
4707
|
+
nextLabel = "\uB2E4\uC74C \uD398\uC774\uC9C0",
|
|
4708
|
+
navLabel = "\uD398\uC774\uC9C0 \uC774\uB3D9"
|
|
4666
4709
|
}) => {
|
|
4667
4710
|
const emit = onPageChange ?? onChange;
|
|
4668
4711
|
const prevDisabled = page <= 1;
|
|
4669
4712
|
const nextDisabled = page >= totalPages;
|
|
4670
4713
|
const items = React16.useMemo(() => getPaginationItems(page, totalPages), [page, totalPages]);
|
|
4671
|
-
return /* @__PURE__ */ jsxs("nav", { className: "pagination", "aria-label":
|
|
4714
|
+
return /* @__PURE__ */ jsxs("nav", { className: "pagination", "aria-label": navLabel, children: [
|
|
4672
4715
|
/* @__PURE__ */ jsx(
|
|
4673
4716
|
"button",
|
|
4674
4717
|
{
|
|
@@ -4725,25 +4768,38 @@ var Drawer = ({
|
|
|
4725
4768
|
title,
|
|
4726
4769
|
footer,
|
|
4727
4770
|
closeOnOverlay = true,
|
|
4771
|
+
dismissible,
|
|
4728
4772
|
showCloseIcon = true,
|
|
4729
4773
|
closeLabel = "\uB2EB\uAE30",
|
|
4730
4774
|
ariaLabel,
|
|
4775
|
+
onExited,
|
|
4731
4776
|
children,
|
|
4732
4777
|
className,
|
|
4733
4778
|
...props
|
|
4734
4779
|
}) => {
|
|
4780
|
+
const lastContentRef = React16.useRef({ children, title, footer });
|
|
4781
|
+
if (open) lastContentRef.current = { children, title, footer };
|
|
4782
|
+
const content = open ? { children, title, footer } : lastContentRef.current;
|
|
4783
|
+
const overlayDismissible = dismissible ?? closeOnOverlay;
|
|
4784
|
+
const escapeDismissible = dismissible ?? true;
|
|
4735
4785
|
const panelRef = React16.useRef(null);
|
|
4786
|
+
const pressedOverlayRef = React16.useRef(false);
|
|
4736
4787
|
const titleId = React16.useId();
|
|
4737
4788
|
const [shouldRender, setShouldRender] = React16.useState(open);
|
|
4738
4789
|
const reduced = useReducedMotion();
|
|
4739
4790
|
const isMounted = useIsMounted();
|
|
4740
4791
|
useFocusTrap(panelRef, open && isMounted);
|
|
4741
|
-
useOverlayEscape(open && isMounted, () =>
|
|
4792
|
+
useOverlayEscape(open && isMounted, () => {
|
|
4793
|
+
if (escapeDismissible) onClose?.();
|
|
4794
|
+
});
|
|
4742
4795
|
if (open && !shouldRender) setShouldRender(true);
|
|
4743
4796
|
const overlayStyle = useSpringPresence({
|
|
4744
4797
|
visible: open,
|
|
4745
4798
|
from: "translateY(0px)",
|
|
4746
|
-
onExitComplete: () =>
|
|
4799
|
+
onExitComplete: () => {
|
|
4800
|
+
setShouldRender(false);
|
|
4801
|
+
onExited?.();
|
|
4802
|
+
}
|
|
4747
4803
|
});
|
|
4748
4804
|
const slideFrom = SLIDE_FROM[placement];
|
|
4749
4805
|
const slideRest = placement === "bottom" ? "translateY(0%)" : "translateX(0%)";
|
|
@@ -4760,7 +4816,7 @@ var Drawer = ({
|
|
|
4760
4816
|
}, [open]);
|
|
4761
4817
|
if (!open && !shouldRender) return null;
|
|
4762
4818
|
if (typeof document === "undefined" || !isMounted) return null;
|
|
4763
|
-
const hasTitle = !!title;
|
|
4819
|
+
const hasTitle = !!content.title;
|
|
4764
4820
|
const sizeValue = typeof size === "number" ? `${size}px` : size;
|
|
4765
4821
|
const panelSizeStyle = placement === "bottom" ? { height: sizeValue } : { width: sizeValue };
|
|
4766
4822
|
return createPortal(
|
|
@@ -4772,8 +4828,16 @@ var Drawer = ({
|
|
|
4772
4828
|
role: "dialog",
|
|
4773
4829
|
"aria-modal": "true",
|
|
4774
4830
|
"aria-labelledby": hasTitle && !ariaLabel ? titleId : void 0,
|
|
4775
|
-
"aria-label":
|
|
4776
|
-
|
|
4831
|
+
"aria-label": ariaLabel,
|
|
4832
|
+
onPointerDown: (e) => {
|
|
4833
|
+
pressedOverlayRef.current = e.target === e.currentTarget;
|
|
4834
|
+
},
|
|
4835
|
+
onClick: (e) => {
|
|
4836
|
+
if (!overlayDismissible) return;
|
|
4837
|
+
if (e.target !== e.currentTarget) return;
|
|
4838
|
+
if (!pressedOverlayRef.current) return;
|
|
4839
|
+
onClose?.();
|
|
4840
|
+
},
|
|
4777
4841
|
children: /* @__PURE__ */ jsxs(
|
|
4778
4842
|
animated.div,
|
|
4779
4843
|
{
|
|
@@ -4781,8 +4845,6 @@ var Drawer = ({
|
|
|
4781
4845
|
...props,
|
|
4782
4846
|
className: cn("drawer_panel", className),
|
|
4783
4847
|
style: { ...props.style, ...panelStyle, ...panelSizeStyle },
|
|
4784
|
-
role: "document",
|
|
4785
|
-
onClick: (e) => e.stopPropagation(),
|
|
4786
4848
|
onKeyDown: (e) => {
|
|
4787
4849
|
if (e.key !== "Escape") e.stopPropagation();
|
|
4788
4850
|
},
|
|
@@ -4797,9 +4859,12 @@ var Drawer = ({
|
|
|
4797
4859
|
children: /* @__PURE__ */ jsx(X, { size: iconSize.md, "aria-hidden": "true" })
|
|
4798
4860
|
}
|
|
4799
4861
|
),
|
|
4800
|
-
title && /* @__PURE__ */ jsx("div", { className: "drawer_header", children: /* @__PURE__ */ jsx("h2", { id: titleId, className: "drawer_title", children: title }) }),
|
|
4801
|
-
children &&
|
|
4802
|
-
|
|
4862
|
+
content.title && /* @__PURE__ */ jsx("div", { className: "drawer_header", children: /* @__PURE__ */ jsx("h2", { id: titleId, className: "drawer_title", children: content.title }) }),
|
|
4863
|
+
content.children && // 본문이 스크롤 컨테이너라 키보드로도 스크롤할 수 있어야 한다(axe
|
|
4864
|
+
// `scrollable-region-focusable`). 다만 초기 포커스 대상에서는 제외한다 - 안쪽에
|
|
4865
|
+
// 첫 입력이 있는데 빈 wrapper 에 포커스가 놓이면 열자마자 어디에 있는지 알 수 없다.
|
|
4866
|
+
/* @__PURE__ */ jsx("div", { className: "drawer_body", tabIndex: 0, "data-focus-trap-skip-autofocus": "", children: content.children }),
|
|
4867
|
+
content.footer && /* @__PURE__ */ jsx("div", { className: "drawer_footer", children: content.footer })
|
|
4803
4868
|
]
|
|
4804
4869
|
}
|
|
4805
4870
|
)
|
|
@@ -4813,6 +4878,7 @@ var Modal = ({
|
|
|
4813
4878
|
open,
|
|
4814
4879
|
onClose,
|
|
4815
4880
|
closeOnOverlay = true,
|
|
4881
|
+
dismissible,
|
|
4816
4882
|
width = 480,
|
|
4817
4883
|
title,
|
|
4818
4884
|
description,
|
|
@@ -4823,14 +4889,23 @@ var Modal = ({
|
|
|
4823
4889
|
children,
|
|
4824
4890
|
className,
|
|
4825
4891
|
ariaLabel,
|
|
4892
|
+
onExited,
|
|
4826
4893
|
...props
|
|
4827
4894
|
}) => {
|
|
4895
|
+
const lastContentRef = React16.useRef({ children, title, description, footer });
|
|
4896
|
+
if (open) lastContentRef.current = { children, title, description, footer };
|
|
4897
|
+
const content = open ? { children, title, description, footer } : lastContentRef.current;
|
|
4898
|
+
const overlayDismissible = dismissible ?? closeOnOverlay;
|
|
4899
|
+
const escapeDismissible = dismissible ?? true;
|
|
4828
4900
|
const panelRef = React16.useRef(null);
|
|
4901
|
+
const pressedOverlayRef = React16.useRef(false);
|
|
4829
4902
|
const titleId = React16.useId();
|
|
4830
4903
|
const [shouldRender, setShouldRender] = React16.useState(open);
|
|
4831
4904
|
const isMounted = useIsMounted();
|
|
4832
4905
|
useFocusTrap(panelRef, open && isMounted);
|
|
4833
|
-
useOverlayEscape(open && isMounted, () =>
|
|
4906
|
+
useOverlayEscape(open && isMounted, () => {
|
|
4907
|
+
if (escapeDismissible) onClose?.();
|
|
4908
|
+
});
|
|
4834
4909
|
if (open && !shouldRender) setShouldRender(true);
|
|
4835
4910
|
const reduced = useReducedMotion();
|
|
4836
4911
|
const overlayStyle = useSpring({
|
|
@@ -4838,7 +4913,9 @@ var Modal = ({
|
|
|
4838
4913
|
immediate: reduced,
|
|
4839
4914
|
config: { tension: 280, friction: 28, clamp: !open },
|
|
4840
4915
|
onRest: (result) => {
|
|
4841
|
-
if (
|
|
4916
|
+
if (open || !result.finished) return;
|
|
4917
|
+
setShouldRender(false);
|
|
4918
|
+
onExited?.();
|
|
4842
4919
|
}
|
|
4843
4920
|
});
|
|
4844
4921
|
const panelStyle = useSpring({
|
|
@@ -4854,7 +4931,7 @@ var Modal = ({
|
|
|
4854
4931
|
}, [open]);
|
|
4855
4932
|
if (!open && !shouldRender) return null;
|
|
4856
4933
|
if (typeof document === "undefined" || !isMounted) return null;
|
|
4857
|
-
const hasTitle = !!title;
|
|
4934
|
+
const hasTitle = !!content.title;
|
|
4858
4935
|
return createPortal(
|
|
4859
4936
|
/* @__PURE__ */ jsx(
|
|
4860
4937
|
animated.div,
|
|
@@ -4864,8 +4941,16 @@ var Modal = ({
|
|
|
4864
4941
|
role: "dialog",
|
|
4865
4942
|
"aria-modal": "true",
|
|
4866
4943
|
"aria-labelledby": hasTitle && !ariaLabel ? titleId : void 0,
|
|
4867
|
-
"aria-label":
|
|
4868
|
-
|
|
4944
|
+
"aria-label": ariaLabel,
|
|
4945
|
+
onPointerDown: (e) => {
|
|
4946
|
+
pressedOverlayRef.current = e.target === e.currentTarget;
|
|
4947
|
+
},
|
|
4948
|
+
onClick: (e) => {
|
|
4949
|
+
if (!overlayDismissible) return;
|
|
4950
|
+
if (e.target !== e.currentTarget) return;
|
|
4951
|
+
if (!pressedOverlayRef.current) return;
|
|
4952
|
+
onClose?.();
|
|
4953
|
+
},
|
|
4869
4954
|
children: /* @__PURE__ */ jsxs(
|
|
4870
4955
|
animated.div,
|
|
4871
4956
|
{
|
|
@@ -4873,26 +4958,21 @@ var Modal = ({
|
|
|
4873
4958
|
...props,
|
|
4874
4959
|
className: cn("modal_panel", className),
|
|
4875
4960
|
style: { ...props.style, ...panelStyle, width },
|
|
4876
|
-
role: "document",
|
|
4877
|
-
onClick: (e) => e.stopPropagation(),
|
|
4878
4961
|
onKeyDown: (e) => {
|
|
4879
4962
|
if (e.key !== "Escape") e.stopPropagation();
|
|
4880
4963
|
},
|
|
4881
4964
|
children: [
|
|
4882
|
-
showCloseIcon && onClose && /* @__PURE__ */ jsx(
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
),
|
|
4892
|
-
|
|
4893
|
-
description && /* @__PURE__ */ jsx("div", { className: "modal_description", children: description }),
|
|
4894
|
-
children && /* @__PURE__ */ jsx("div", { className: "modal_body", children }),
|
|
4895
|
-
footer && /* @__PURE__ */ jsx("div", { className: cn("modal_footer", `modal_footer_${footerAlign}`), children: footer })
|
|
4965
|
+
showCloseIcon && onClose && /* @__PURE__ */ jsx("button", { type: "button", className: "modal_close", onClick: onClose, "aria-label": closeLabel, children: /* @__PURE__ */ jsx(X, { size: iconSize.md, "aria-hidden": "true" }) }),
|
|
4966
|
+
content.title && /* @__PURE__ */ jsx("h2", { id: titleId, className: "modal_title", children: content.title }),
|
|
4967
|
+
content.description && /* @__PURE__ */ jsx("div", { className: "modal_description", children: content.description }),
|
|
4968
|
+
content.children && // 본문이 스크롤 컨테이너라 키보드로도 스크롤할 수 있어야 한다(axe
|
|
4969
|
+
// `scrollable-region-focusable`). 넘치는지 여부를 측정해 조건부로 주는 방법도 있지만
|
|
4970
|
+
// ResizeObserver 를 들일 만큼의 이득이 없고, 포커스 트랩 안에서 탭 정지 하나가
|
|
4971
|
+
// 늘어나는 정도의 비용이다.
|
|
4972
|
+
// 다만 초기 포커스 대상에서는 제외한다 - 안쪽에 첫 입력이 있는데 빈 wrapper 에
|
|
4973
|
+
// 포커스가 놓이면 열자마자 어디에 있는지 알 수 없다.
|
|
4974
|
+
/* @__PURE__ */ jsx("div", { className: "modal_body", tabIndex: 0, "data-focus-trap-skip-autofocus": "", children: content.children }),
|
|
4975
|
+
content.footer && /* @__PURE__ */ jsx("div", { className: cn("modal_footer", `modal_footer_${footerAlign}`), children: content.footer })
|
|
4896
4976
|
]
|
|
4897
4977
|
}
|
|
4898
4978
|
)
|
|
@@ -5078,4 +5158,4 @@ var Stack = ({
|
|
|
5078
5158
|
);
|
|
5079
5159
|
};
|
|
5080
5160
|
|
|
5081
|
-
export { Accordion, AlertProvider, Avatar, Badge, BottomNav, BottomNavItem, BottomNavSpacer, Breadcrumb, Button, Card, Checkbox, Chip, Container, DatePicker, Divider, Drawer, Dropdown, EmptyState, ErrorState, FileInput, Grid, Hero, Icon, IconButton, ImageCropper, LinearProgress, ListItem, MediaCard, Menu, Modal, NavBar, NavLink, OtpInput, Pagination, Popover, Radio, RadioGroup, Section, Sidebar, SidebarItem, SidebarSection, Skeleton, Spinner, Stack, Tab, TabList, TabPanel, Table, Tabs, TextField, Textarea, ThemeProvider, ToastProvider, Toggle, Tooltip, TopLoading, a11y, baseBorderWidth, baseColors, baseTypography, borderWidth, breakpoints, cn, colors, elevation, iconSize, motion, opacity, radius, skeleton, spacing, typography, useAlert, useFocusTrap, useRadioGroupContext, useReducedMotion, useSpringHover, useSpringPresence, useTheme, useToast, zIndex };
|
|
5161
|
+
export { Accordion, AlertProvider, Avatar, Badge, BottomNav, BottomNavItem, BottomNavSpacer, Breadcrumb, Button, Card, Checkbox, Chip, Container, DatePicker, Divider, Drawer, Dropdown, EmptyState, ErrorState, FileInput, Grid, Hero, Icon, IconButton, ImageCropper, LinearProgress, ListItem, MediaCard, Menu, Modal, NavBar, NavLink, OtpInput, Pagination, Popover, Prose, Radio, RadioGroup, Section, Sidebar, SidebarItem, SidebarSection, Skeleton, Spinner, Stack, Tab, TabList, TabPanel, Table, Tabs, TextField, Textarea, ThemeProvider, ToastProvider, Toggle, Tooltip, TopLoading, a11y, baseBorderWidth, baseColors, baseTypography, borderWidth, breakpoints, cn, colors, elevation, iconSize, motion, opacity, radius, skeleton, spacing, typography, useAlert, useFocusTrap, useRadioGroupContext, useReducedMotion, useSpringHover, useSpringPresence, useTheme, useToast, zIndex };
|
|
@@ -12,7 +12,9 @@ $focus_ring_success: var(--bt-focus-ring-success);
|
|
|
12
12
|
// WCAG 2.1 SC 2.5.5 권장 최소: 44×44
|
|
13
13
|
|
|
14
14
|
$tap_min_size: 44px; // a11y 최소 권장
|
|
15
|
-
$tap_target_dense: 32px; // 데스크탑 인라인 폼, 컴팩트 UI
|
|
15
|
+
$tap_target_dense: 32px; // 데스크탑 인라인 폼, 컴팩트 UI. 인터랙티브 컨트롤의 하한이다
|
|
16
|
+
// - 터치 입력이 있는 화면에서는 responsive_height 로 승급할 것
|
|
17
|
+
// - 예외: 마우스 전용 내부 도구(admin 등)는 28px 까지 허용
|
|
16
18
|
$tap_target_compact: 40px; // 데스크탑 기본
|
|
17
19
|
$tap_target_comfortable: 48px; // 모바일/터치 기본 (44 초과)
|
|
18
20
|
$tap_target_spacious: 56px; // 강조 CTA, 풀스크린 액션
|
|
@@ -44,3 +46,48 @@ $tap_target_spacious: 56px; // 강조 CTA, 풀스크린 액션
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
}
|
|
49
|
+
|
|
50
|
+
// ── Visually hidden ───────────────────────────────────────────────────────
|
|
51
|
+
// 시각적으로 숨기되 보조기기에는 남긴다. 라이브 리전, 아이콘 버튼의 텍스트 라벨,
|
|
52
|
+
// 스크린리더 전용 표 헤더 등. `display: none` · `visibility: hidden` 은 접근성 트리에서도
|
|
53
|
+
// 빠지므로 대신 쓸 수 없다.
|
|
54
|
+
//
|
|
55
|
+
// `clip: rect()` 는 CSS Masking 에서 deprecated 지만 구형 폴백으로 함께 둔다. `clip-path`
|
|
56
|
+
// 단독은 일부 조합에서 요소가 잘리지 않는다.
|
|
57
|
+
@mixin visually_hidden {
|
|
58
|
+
position: absolute;
|
|
59
|
+
width: 1px;
|
|
60
|
+
height: 1px;
|
|
61
|
+
padding: 0;
|
|
62
|
+
margin: -1px;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
clip: rect(0 0 0 0);
|
|
65
|
+
clip-path: inset(50%);
|
|
66
|
+
white-space: nowrap;
|
|
67
|
+
border: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 조건부로 숨기는 자리(브레이크포인트·상태에 따라 다시 보여야 하는 라벨 등)에서 위를 되돌린다.
|
|
71
|
+
// 짝을 손으로 쓰면 한쪽에 속성을 추가할 때 다른 쪽을 빠뜨린다 - `clip-path` 를 되돌리지 않아
|
|
72
|
+
// 라벨이 계속 잘려 있던 것이 실제로 있었던 실수다.
|
|
73
|
+
@mixin visually_hidden_reset {
|
|
74
|
+
position: static;
|
|
75
|
+
width: auto;
|
|
76
|
+
height: auto;
|
|
77
|
+
padding: 0;
|
|
78
|
+
margin: 0;
|
|
79
|
+
overflow: visible;
|
|
80
|
+
clip: auto;
|
|
81
|
+
clip-path: none;
|
|
82
|
+
white-space: normal;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 포커스를 받으면 드러난다. 스킵 링크("본문으로 건너뛰기") 패턴 - 평소엔 안 보이지만
|
|
86
|
+
// 키보드 사용자가 Tab 을 누르면 나타나야 한다.
|
|
87
|
+
@mixin visually_hidden_focusable {
|
|
88
|
+
@include visually_hidden;
|
|
89
|
+
|
|
90
|
+
&:focus-visible {
|
|
91
|
+
@include visually_hidden_reset;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
@use "sass:list";
|
|
2
|
+
@use "../colors" as colors;
|
|
3
|
+
|
|
1
4
|
// ── Base border-width ────────────────────────────────────────────────────────
|
|
2
5
|
|
|
3
6
|
$border_width_0: 0px;
|
|
@@ -10,3 +13,29 @@ $border_width_none: $border_width_0;
|
|
|
10
13
|
$border_width_standard: $border_width_1;
|
|
11
14
|
$border_width_thick: $border_width_2;
|
|
12
15
|
$border_width_indicator: $border_width_2;
|
|
16
|
+
|
|
17
|
+
// ── Hairline mixin ───────────────────────────────────────────────────────────
|
|
18
|
+
//
|
|
19
|
+
// 1px 경계선용 단축. `border: $border_width_standard solid $color_border_default` 는
|
|
20
|
+
// `border: 1px solid #E5E5E5` 보다 길어서, 실측상 소비 앱 SCSS 에 `1px` 하드코딩이
|
|
21
|
+
// 232 건 남아 있었다. 짧게 쓸 수 있어야 실제로 토큰을 쓴다.
|
|
22
|
+
//
|
|
23
|
+
// @example
|
|
24
|
+
// ```scss
|
|
25
|
+
// .card { @include token.hairline; } // 사방
|
|
26
|
+
// .header { @include token.hairline(bottom); } // 한 면
|
|
27
|
+
// .active { @include token.hairline(left, token.$color_border_hover); }
|
|
28
|
+
// ```
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@mixin hairline($side: all, $color: colors.$color_border_default) {
|
|
32
|
+
@if $side == all {
|
|
33
|
+
border: $border_width_standard solid $color;
|
|
34
|
+
} @else if list.index(top right bottom left, $side) {
|
|
35
|
+
border-#{$side}: $border_width_standard solid $color;
|
|
36
|
+
} @else {
|
|
37
|
+
// 검증이 없으면 `hairline(letf)` 같은 오타가 컴파일을 통과해 `border-letf` 라는
|
|
38
|
+
// 무효 속성을 조용히 만든다. 소비 앱에 권장하는 공개 API 라 여기서 끊는다.
|
|
39
|
+
@error "token.hairline: $side 는 all · top · right · bottom · left 중 하나여야 합니다 (받은 값: #{$side})";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -145,8 +145,37 @@ $letter_spacing_tight: 0.32px;
|
|
|
145
145
|
white-space: nowrap;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// 한글 줄바꿈. `word-break: break-word` 는 어절 중간에서 끊어 "안녕하세 / 요" 를 만든다.
|
|
149
|
+
// 표준값이 아니고, 명세상 `word-break: normal` + `overflow-wrap: anywhere` 와 같게 동작한다.
|
|
150
|
+
// keep-all 이 어절을 지키고, 끊을 수 없는 긴 영문·URL 만 overflow-wrap 이 처리한다.
|
|
151
|
+
// 텍스트를 줄바꿈하는 모든 컴포넌트는 이 믹스인을 쓴다 (Button·Toast·Tooltip).
|
|
152
|
+
//
|
|
153
|
+
// `break-word` 가 아니라 `anywhere` 를 쓴다. 눈에 보이는 줄바꿈 지점은 둘이 완전히 같지만
|
|
154
|
+
// (120·60px 폭에서 한글·혼합 문자열 실측 결과 동일), min-content 폭이 다르다 —
|
|
155
|
+
// break-word 는 최장 단어(122px), anywhere 는 한 글자(13px). flex item 의 기본
|
|
156
|
+
// min-width: auto 는 min-content 아래로 줄지 못하므로 break-word 면 좁은 컨테이너를 넘친다
|
|
157
|
+
// (실측: 120px 컨테이너에서 98px 초과). anywhere 는 min-width: 0 없이도 넘치지 않고,
|
|
158
|
+
// 텍스트가 inline-flex 의 직접 자식인 vanilla `.bt-button` 처럼 익명 flex item 이라
|
|
159
|
+
// min-width 를 줄 선택자조차 없는 경우까지 함께 해결한다.
|
|
160
|
+
@mixin wrap_keep_all {
|
|
161
|
+
overflow-wrap: anywhere;
|
|
162
|
+
word-break: keep-all;
|
|
163
|
+
}
|
|
164
|
+
|
|
148
165
|
// ── Numeric (v3.1) ────────────────────────────────────────────────────────
|
|
149
166
|
// 매출/지표 표시할 때 숫자 자릿수 정렬.
|
|
150
167
|
|
|
151
168
|
@mixin tabular_nums { font-variant-numeric: tabular-nums; }
|
|
152
169
|
@mixin slashed_zero { font-variant-numeric: slashed-zero; }
|
|
170
|
+
|
|
171
|
+
// ── Legibility ────────────────────────────────────────────────────────────
|
|
172
|
+
// Pretendard 기본 글자꼴은 l · I · 1 이 거의 같고 0 과 O 도 헷갈린다. 사용자가 한 글자씩
|
|
173
|
+
// 옮겨 적는 값(아이디·인증코드·시리얼·사업자번호)에서 오탈자를 만든다.
|
|
174
|
+
// cv05 = l 에 꼬리, cv08 = I 에 세리프. Pretendard 가 아닌 폴백 폰트는 두 feature 를
|
|
175
|
+
// 그냥 무시하므로 안전하다.
|
|
176
|
+
// 주의: slashed-zero 를 포함하므로 tabular_nums / slashed_zero 와 같은 선택자에서
|
|
177
|
+
// 겹쳐 쓰면 안 된다 - 셋 다 font-variant-numeric 이라 뒤에 온 것만 남는다.
|
|
178
|
+
@mixin legible_identifiers {
|
|
179
|
+
font-feature-settings: "cv05" 1, "cv08" 1;
|
|
180
|
+
font-variant-numeric: slashed-zero;
|
|
181
|
+
}
|