@bigtablet/design-system 3.16.0 → 3.17.1

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
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  import './index.css';
3
3
  import * as React11 from 'react';
4
- import { createContext, useRef, useState, useId, useEffect, useContext, useCallback, useMemo, Fragment, useImperativeHandle, useLayoutEffect } from 'react';
4
+ import { createContext, useState, useRef, useCallback, useEffect, useMemo, useContext, useId, Fragment, useImperativeHandle, useLayoutEffect } from 'react';
5
5
  import { useSpring, animated } from '@react-spring/web';
6
- import { ChevronDown, ChevronRight, Globe, ChevronLeft, ArrowUp, ArrowDown, ArrowUpDown, XCircle, AlertTriangle, CheckCircle2, Info, Bell, Search, Check, Image, X, EyeOff, Eye, TriangleAlert } from 'lucide-react';
6
+ import { ChevronDown, ChevronRight, Globe, ChevronLeft, EyeOff, Eye, ArrowUp, ArrowDown, ArrowUpDown, Search, XCircle, AlertTriangle, CheckCircle2, Info, Bell, Check, Image, X, TriangleAlert } from 'lucide-react';
7
7
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
8
8
  import { createPortal } from 'react-dom';
9
9
 
@@ -91,15 +91,23 @@ function lockBodyScroll() {
91
91
  const open = Number.parseInt(body.dataset[COUNTER] || "0", 10);
92
92
  if (open === 0) {
93
93
  const scrollbarWidth = measureViewportInset();
94
+ const scroller = document.scrollingElement ?? html;
95
+ const documentScrolls = scroller.scrollHeight > scroller.clientHeight;
96
+ const canReserveGutter = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("scrollbar-gutter: stable");
94
97
  body.dataset[PREV_OVERFLOW] = body.style.overflow;
95
98
  body.dataset[PREV_GUTTER] = html.style.scrollbarGutter;
96
99
  body.dataset[PREV_PADDING_RIGHT] = body.style.paddingRight;
97
100
  body.dataset[PREV_SCROLLBAR_WIDTH_VAR] = html.style.getPropertyValue(SCROLLBAR_WIDTH_VAR);
98
101
  if (scrollbarWidth > 0) {
99
102
  html.style.setProperty(SCROLLBAR_WIDTH_VAR, `${scrollbarWidth}px`);
100
- html.style.scrollbarGutter = "auto";
101
- const current = Number.parseFloat(window.getComputedStyle(body).paddingRight) || 0;
102
- body.style.paddingRight = `${current + scrollbarWidth}px`;
103
+ }
104
+ if (documentScrolls && scrollbarWidth > 0) {
105
+ if (canReserveGutter) {
106
+ html.style.scrollbarGutter = "stable";
107
+ } else {
108
+ const current = Number.parseFloat(window.getComputedStyle(body).paddingRight) || 0;
109
+ body.style.paddingRight = `${current + scrollbarWidth}px`;
110
+ }
103
111
  }
104
112
  body.style.overflow = "hidden";
105
113
  }
@@ -409,6 +417,168 @@ function useSpringPresence({
409
417
  }
410
418
  });
411
419
  }
420
+ var MIN_SPACE_BELOW = 120;
421
+ function useListboxPopup({
422
+ items,
423
+ onCommit,
424
+ disabled = false,
425
+ returnFocusOnClose = false,
426
+ initialActiveIndex
427
+ }) {
428
+ const [isOpen, setIsOpen] = useState(false);
429
+ const [activeIndex, setActiveIndex] = useState(-1);
430
+ const [dropUp, setDropUp] = useState(false);
431
+ const wrapperRef = useRef(null);
432
+ const triggerRef = useRef(null);
433
+ const listRef = useRef(null);
434
+ const close = useCallback(() => {
435
+ setIsOpen(false);
436
+ if (returnFocusOnClose) triggerRef.current?.focus();
437
+ }, [returnFocusOnClose]);
438
+ useEffect(() => {
439
+ const handleOutsideClick = (event) => {
440
+ if (!wrapperRef.current?.contains(event.target)) setIsOpen(false);
441
+ };
442
+ document.addEventListener("mousedown", handleOutsideClick);
443
+ return () => document.removeEventListener("mousedown", handleOutsideClick);
444
+ }, []);
445
+ const moveActive = useCallback(
446
+ (dir) => {
447
+ if (items.length === 0) return;
448
+ if (!isOpen) {
449
+ setIsOpen(true);
450
+ return;
451
+ }
452
+ let i = activeIndex;
453
+ if (i === -1) {
454
+ i = dir === 1 ? -1 : 0;
455
+ }
456
+ const len = items.length;
457
+ for (let step = 0; step < len; step++) {
458
+ i = (i + dir + len) % len;
459
+ if (!items[i].disabled) {
460
+ setActiveIndex(i);
461
+ break;
462
+ }
463
+ }
464
+ },
465
+ [items, isOpen, activeIndex]
466
+ );
467
+ const commitActive = useCallback(() => {
468
+ if (activeIndex < 0 || activeIndex >= items.length) return;
469
+ const item = items[activeIndex];
470
+ if (item.disabled) return;
471
+ onCommit(item);
472
+ }, [activeIndex, items, onCommit]);
473
+ const firstEnabled = useCallback(() => items.findIndex((o) => !o.disabled), [items]);
474
+ const lastEnabled = useCallback(() => {
475
+ for (let i = items.length - 1; i >= 0; i--) {
476
+ if (!items[i].disabled) return i;
477
+ }
478
+ return -1;
479
+ }, [items]);
480
+ const onTriggerKeyDown = useCallback(
481
+ (event) => {
482
+ if (disabled) return;
483
+ switch (event.key) {
484
+ case " ":
485
+ case "Enter":
486
+ event.preventDefault();
487
+ if (!isOpen) setIsOpen(true);
488
+ else commitActive();
489
+ break;
490
+ case "ArrowDown":
491
+ event.preventDefault();
492
+ moveActive(1);
493
+ break;
494
+ case "ArrowUp":
495
+ event.preventDefault();
496
+ moveActive(-1);
497
+ break;
498
+ case "Home":
499
+ event.preventDefault();
500
+ setIsOpen(true);
501
+ setActiveIndex(firstEnabled());
502
+ break;
503
+ case "End":
504
+ event.preventDefault();
505
+ setIsOpen(true);
506
+ setActiveIndex(lastEnabled());
507
+ break;
508
+ case "Escape":
509
+ event.preventDefault();
510
+ setIsOpen(false);
511
+ break;
512
+ case "Tab":
513
+ setIsOpen(false);
514
+ break;
515
+ }
516
+ },
517
+ [disabled, isOpen, commitActive, moveActive, firstEnabled, lastEnabled]
518
+ );
519
+ const onInputKeyDown = useCallback(
520
+ (event) => {
521
+ if (disabled) return;
522
+ if (event.nativeEvent.isComposing) return;
523
+ switch (event.key) {
524
+ case "ArrowDown":
525
+ event.preventDefault();
526
+ moveActive(1);
527
+ break;
528
+ case "ArrowUp":
529
+ event.preventDefault();
530
+ moveActive(-1);
531
+ break;
532
+ case "Enter":
533
+ event.preventDefault();
534
+ commitActive();
535
+ break;
536
+ case "Escape":
537
+ event.preventDefault();
538
+ close();
539
+ break;
540
+ case "Tab":
541
+ close();
542
+ break;
543
+ }
544
+ },
545
+ [disabled, moveActive, commitActive, close]
546
+ );
547
+ useEffect(() => {
548
+ if (!isOpen) return;
549
+ const preferred = initialActiveIndex?.(items) ?? -1;
550
+ setActiveIndex(preferred >= 0 ? preferred : items.findIndex((o) => !o.disabled));
551
+ }, [isOpen, items]);
552
+ useEffect(() => {
553
+ if (!isOpen || activeIndex < 0) return;
554
+ const list = listRef.current;
555
+ if (!list) return;
556
+ const option = list.querySelectorAll('[role="option"]')[activeIndex];
557
+ option?.scrollIntoView?.({ block: "nearest" });
558
+ }, [isOpen, activeIndex, items]);
559
+ useEffect(() => {
560
+ if (!isOpen || !triggerRef.current) return;
561
+ const rect = triggerRef.current.getBoundingClientRect();
562
+ const spaceBelow = window.innerHeight - rect.bottom;
563
+ const spaceAbove = rect.top;
564
+ setDropUp(spaceBelow < MIN_SPACE_BELOW && spaceAbove > spaceBelow);
565
+ }, [isOpen]);
566
+ return {
567
+ isOpen,
568
+ setIsOpen,
569
+ dropUp,
570
+ activeIndex,
571
+ setActiveIndex,
572
+ wrapperRef,
573
+ triggerRef,
574
+ listRef,
575
+ close,
576
+ moveActive,
577
+ commitActive,
578
+ onTriggerKeyDown,
579
+ onInputKeyDown
580
+ };
581
+ }
412
582
 
413
583
  // src/styles/icon/index.ts
414
584
  var iconSize = {
@@ -550,6 +720,90 @@ var Badge = ({
550
720
  }
551
721
  );
552
722
  };
723
+ var DescriptionList = ({
724
+ items,
725
+ layout = "row",
726
+ divided = false,
727
+ className,
728
+ ref,
729
+ ...props
730
+ }) => /* @__PURE__ */ jsx(
731
+ "dl",
732
+ {
733
+ ref,
734
+ className: cn(
735
+ "description_list",
736
+ `description_list_layout_${layout}`,
737
+ { description_list_divided: divided },
738
+ className
739
+ ),
740
+ ...props,
741
+ children: items.map((item, index) => /* @__PURE__ */ jsxs(
742
+ "div",
743
+ {
744
+ className: cn("description_list_item", { description_list_item_full: item.full }),
745
+ children: [
746
+ /* @__PURE__ */ jsx("dt", { className: "description_list_label", children: item.label }),
747
+ /* @__PURE__ */ jsx("dd", { className: "description_list_value", children: item.value })
748
+ ]
749
+ },
750
+ index
751
+ ))
752
+ }
753
+ );
754
+ var isPresent = (value) => value !== void 0 && value !== null && value !== "";
755
+ var Stat = ({
756
+ label,
757
+ value,
758
+ delta,
759
+ deltaTone = "neutral",
760
+ icon,
761
+ className,
762
+ ref,
763
+ ...props
764
+ }) => /* @__PURE__ */ jsxs("div", { ref, className: cn("stat", className), ...props, children: [
765
+ /* @__PURE__ */ jsxs("div", { className: "stat_label", children: [
766
+ icon && /* @__PURE__ */ jsx("span", { className: "stat_icon", "aria-hidden": "true", children: icon }),
767
+ label
768
+ ] }),
769
+ /* @__PURE__ */ jsx("div", { className: "stat_value", children: value }),
770
+ isPresent(delta) && /* @__PURE__ */ jsx("div", { className: cn("stat_delta", `stat_delta_${deltaTone}`), children: delta })
771
+ ] });
772
+ var CheckGlyph = () => /* @__PURE__ */ jsx(
773
+ "svg",
774
+ {
775
+ viewBox: "0 0 20 20",
776
+ fill: "none",
777
+ stroke: "currentColor",
778
+ strokeWidth: 2,
779
+ strokeLinecap: "round",
780
+ strokeLinejoin: "round",
781
+ "aria-hidden": "true",
782
+ focusable: "false",
783
+ className: "timeline_glyph",
784
+ children: /* @__PURE__ */ jsx("polyline", { points: "4 10 8 14 16 6" })
785
+ }
786
+ );
787
+ var isPresent2 = (value) => value !== void 0 && value !== null && value !== "";
788
+ var Timeline = ({ items, className, ref, ...props }) => /* @__PURE__ */ jsx("ol", { ref, className: cn("timeline", className), ...props, children: items.map((item) => {
789
+ const status = item.status ?? "pending";
790
+ return /* @__PURE__ */ jsxs("li", { className: cn("timeline_item", `timeline_item_${status}`), children: [
791
+ /* @__PURE__ */ jsx("span", { className: "timeline_indicator", "aria-hidden": "true", children: item.icon ?? (status === "done" ? /* @__PURE__ */ jsx(CheckGlyph, {}) : /* @__PURE__ */ jsx(
792
+ "span",
793
+ {
794
+ className: cn("timeline_dot", { timeline_dot_hollow: status === "pending" })
795
+ }
796
+ )) }),
797
+ /* @__PURE__ */ jsxs("div", { className: "timeline_body", children: [
798
+ /* @__PURE__ */ jsxs("div", { className: "timeline_head", children: [
799
+ /* @__PURE__ */ jsx("div", { className: "timeline_title", children: item.title }),
800
+ isPresent2(item.time) && /* @__PURE__ */ jsx("div", { className: "timeline_time", children: item.time })
801
+ ] }),
802
+ isPresent2(item.description) && /* @__PURE__ */ jsx("div", { className: "timeline_description", children: item.description }),
803
+ item.children
804
+ ] })
805
+ ] }, item.id);
806
+ }) });
553
807
  var EmptyState = ({
554
808
  illustration,
555
809
  title,
@@ -579,12 +833,181 @@ var EmptyState = ({
579
833
  }
580
834
  );
581
835
  };
836
+
837
+ // src/ui/system/locale-provider/messages.ts
838
+ var ko = {
839
+ "chip.remove": "{label} \uC81C\uAC70",
840
+ "dataView.clearSelection": "\uC120\uD0DD \uD574\uC81C",
841
+ "dataView.empty": "\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
842
+ "dataView.errorTitle": "\uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4",
843
+ "dataView.retry": "\uB2E4\uC2DC \uC2DC\uB3C4",
844
+ "dataView.search": "\uAC80\uC0C9",
845
+ "dataView.selectionSummary": "{count}\uAC1C \uC120\uD0DD\uB428",
846
+ "table.empty": "\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
847
+ "table.rowClickHint": "\uD074\uB9AD \uAC00\uB2A5\uD55C \uD589",
848
+ "table.selectAll": "\uC804\uCCB4 \uC120\uD0DD",
849
+ "table.selectRow": "{index}\uBC88\uC9F8 \uD589 \uC120\uD0DD",
850
+ "alert.cancel": "\uCDE8\uC18C",
851
+ "alert.confirm": "\uD655\uC778",
852
+ "errorState.title": "\uBB38\uC81C\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4",
853
+ "spinner.label": "\uB85C\uB529 \uC911",
854
+ "toast.close": "\uB2EB\uAE30",
855
+ "toast.region": "\uC54C\uB9BC",
856
+ "topLoading.label": "\uD398\uC774\uC9C0 \uB85C\uB529 \uC911",
857
+ "combobox.empty": "\uC77C\uCE58\uD558\uB294 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4",
858
+ "combobox.idle": "\uAC80\uC0C9\uC5B4\uB97C \uC785\uB825\uD558\uC138\uC694",
859
+ "combobox.loading": "\uAC80\uC0C9 \uC911",
860
+ "combobox.placeholder": "\uAC80\uC0C9\uD574\uC11C \uC120\uD0DD",
861
+ "datePicker.day": "\uC77C",
862
+ "datePicker.minDateSr": "\uCD5C\uC18C \uB0A0\uC9DC: {date}",
863
+ "datePicker.month": "\uC6D4",
864
+ "datePicker.rangeUntilTodaySr": "\uC624\uB298\uAE4C\uC9C0 \uC120\uD0DD \uAC00\uB2A5",
865
+ "datePicker.year": "\uB144",
866
+ "dateRange.end": "\uC885\uB8CC\uC77C",
867
+ "dateRange.start": "\uC2DC\uC791\uC77C",
868
+ "dropdown.empty": "\uACB0\uACFC \uC5C6\uC74C",
869
+ "dropdown.placeholder": "\uC120\uD0DD\u2026",
870
+ "dropdown.searchPlaceholder": "\uAC80\uC0C9\u2026",
871
+ "dropdown.selectedSummary": "{count}\uAC1C \uC120\uD0DD",
872
+ "fileInput.label": "\uD30C\uC77C \uC120\uD0DD",
873
+ "fileInput.removeImage": "\uC774\uBBF8\uC9C0 \uC81C\uAC70",
874
+ "imageCropper.hint": "\uB4DC\uB798\uADF8(\uB610\uB294 \uBC29\uD5A5\uD0A4)\uB85C \uC704\uCE58, \uD720\xB7\uC2AC\uB77C\uC774\uB354\uB85C \uBC30\uC728\uC744 \uB9DE\uCD94\uC138\uC694.",
875
+ "imageCropper.label": "\uC774\uBBF8\uC9C0 \uC704\uCE58\uC640 \uBC30\uC728 \uC870\uC815",
876
+ "imageCropper.noPanHint": "\uC774\uBBF8\uC9C0\uAC00 \uBDF0\uD3EC\uD2B8\uB97C \uB531 \uCC44\uC6CC \uC774\uB3D9 \uC5EC\uC720\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
877
+ "imageCropper.zoom": "\uBC30\uC728",
878
+ "imageCropper.zoomIn": "\uD655\uB300",
879
+ "imageCropper.zoomOut": "\uCD95\uC18C",
880
+ "otpInput.digit": "{index}\uBC88\uC9F8 \uC790\uB9AC",
881
+ "otpInput.label": "OTP \uC785\uB825",
882
+ "tagInput.added": "{names} \uCD94\uAC00\uB428",
883
+ "tagInput.addedWithNotes": "{names} \uCD94\uAC00\uB428 ({notes})",
884
+ "tagInput.atCap": "\uCD5C\uB300 {max}\uAC1C\uAE4C\uC9C0 \uCD94\uAC00\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4",
885
+ "tagInput.duplicate": "{names} \uC774\uBBF8 \uC788\uC74C",
886
+ "tagInput.placeholder": "\uC785\uB825 \uD6C4 Enter",
887
+ "tagInput.removed": "{name} \uC81C\uAC70\uB428",
888
+ "textField.clear": "\uC9C0\uC6B0\uAE30",
889
+ "textField.passwordHide": "\uBE44\uBC00\uBC88\uD638 \uC228\uAE30\uAE30",
890
+ "textField.passwordShow": "\uBE44\uBC00\uBC88\uD638 \uD45C\uC2DC",
891
+ "timePicker.hour": "\uC2DC",
892
+ "timePicker.minute": "\uBD84",
893
+ "timePicker.rangeSr": "{min} \uBD80\uD130 {max} \uAE4C\uC9C0 \uC120\uD0DD \uAC00\uB2A5",
894
+ "bottomNav.label": "\uC8FC\uC694 \uBA54\uB274",
895
+ "breadcrumb.label": "\uD604\uC7AC \uC704\uCE58",
896
+ "pagination.label": "\uD398\uC774\uC9C0 \uC774\uB3D9",
897
+ "pagination.next": "\uB2E4\uC74C \uD398\uC774\uC9C0",
898
+ "pagination.prev": "\uC774\uC804 \uD398\uC774\uC9C0",
899
+ "sidebar.toggle": "\uC0AC\uC774\uB4DC\uBC14 \uD1A0\uAE00",
900
+ "drawer.close": "\uB2EB\uAE30",
901
+ "modal.close": "\uB2EB\uAE30"
902
+ };
903
+ var en = {
904
+ "chip.remove": "Remove {label}",
905
+ "dataView.clearSelection": "Clear selection",
906
+ "dataView.empty": "No data",
907
+ "dataView.errorTitle": "Could not load",
908
+ "dataView.retry": "Try again",
909
+ "dataView.search": "Search",
910
+ "dataView.selectionSummary": "{count} selected",
911
+ "table.empty": "No data",
912
+ "table.rowClickHint": "Clickable row",
913
+ "table.selectAll": "Select all",
914
+ "table.selectRow": "Select row {index}",
915
+ "alert.cancel": "Cancel",
916
+ "alert.confirm": "OK",
917
+ "errorState.title": "Something went wrong",
918
+ "spinner.label": "Loading",
919
+ "toast.close": "Close",
920
+ "toast.region": "Notifications",
921
+ "topLoading.label": "Loading page",
922
+ "combobox.empty": "No matches",
923
+ "combobox.idle": "Type to search",
924
+ "combobox.loading": "Searching",
925
+ "combobox.placeholder": "Search to select",
926
+ "datePicker.day": "Day",
927
+ "datePicker.minDateSr": "Earliest date: {date}",
928
+ "datePicker.month": "Month",
929
+ "datePicker.rangeUntilTodaySr": "Selectable up to today",
930
+ "datePicker.year": "Year",
931
+ "dateRange.end": "End date",
932
+ "dateRange.start": "Start date",
933
+ "dropdown.empty": "No results",
934
+ "dropdown.placeholder": "Select\u2026",
935
+ "dropdown.searchPlaceholder": "Search\u2026",
936
+ "dropdown.selectedSummary": "{count} selected",
937
+ "fileInput.label": "Choose file",
938
+ "fileInput.removeImage": "Remove image",
939
+ "imageCropper.hint": "Drag (or use arrow keys) to move, wheel or slider to zoom.",
940
+ "imageCropper.label": "Adjust image position and zoom",
941
+ "imageCropper.noPanHint": "The image fills the viewport exactly, so there is no room to move it.",
942
+ "imageCropper.zoom": "Zoom",
943
+ "imageCropper.zoomIn": "Zoom in",
944
+ "imageCropper.zoomOut": "Zoom out",
945
+ "otpInput.digit": "Digit {index}",
946
+ "otpInput.label": "One-time code",
947
+ "tagInput.added": "{names} added",
948
+ "tagInput.addedWithNotes": "{names} added ({notes})",
949
+ "tagInput.atCap": "You can add up to {max}",
950
+ "tagInput.duplicate": "{names} already added",
951
+ "tagInput.placeholder": "Type and press Enter",
952
+ "tagInput.removed": "{name} removed",
953
+ "textField.clear": "Clear",
954
+ "textField.passwordHide": "Hide password",
955
+ "textField.passwordShow": "Show password",
956
+ "timePicker.hour": "Hour",
957
+ "timePicker.minute": "Minute",
958
+ "timePicker.rangeSr": "Selectable from {min} to {max}",
959
+ "bottomNav.label": "Main menu",
960
+ "breadcrumb.label": "Breadcrumb",
961
+ "pagination.label": "Pagination",
962
+ "pagination.next": "Next page",
963
+ "pagination.prev": "Previous page",
964
+ "sidebar.toggle": "Toggle sidebar",
965
+ "drawer.close": "Close",
966
+ "modal.close": "Close"
967
+ };
968
+ var catalogs = { ko, en };
969
+ function format(template, vars) {
970
+ if (!vars) return template;
971
+ return template.replace(
972
+ /\{(\w+)\}/g,
973
+ (whole, name) => name in vars ? String(vars[name]) : whole
974
+ );
975
+ }
976
+ function makeText(messages) {
977
+ return (key, vars) => format(messages[key], vars);
978
+ }
979
+ var FALLBACK = { locale: "ko", t: makeText(ko) };
980
+ var LocaleContext = createContext(void 0);
981
+ var LocaleProvider = ({ locale = "ko", messages, children }) => {
982
+ const stableMessages = useStableMessages(messages);
983
+ const value = useMemo(() => {
984
+ const base = catalogs[locale];
985
+ const merged = stableMessages ? { ...base, ...stableMessages } : base;
986
+ return { locale, t: makeText(merged) };
987
+ }, [locale, stableMessages]);
988
+ return /* @__PURE__ */ jsx(LocaleContext.Provider, { value, children });
989
+ };
990
+ function useStableMessages(messages) {
991
+ const ref = useRef(messages);
992
+ const previous = ref.current;
993
+ const same = previous === messages || !!previous && !!messages && Object.keys(previous).length === Object.keys(messages).length && Object.keys(messages).every((key) => previous[key] === messages[key]);
994
+ useEffect(() => {
995
+ if (!same) ref.current = messages;
996
+ }, [same, messages]);
997
+ return same ? previous : messages;
998
+ }
999
+ function useLocaleText() {
1000
+ return (useContext(LocaleContext) ?? FALLBACK).t;
1001
+ }
1002
+ function useLocaleName() {
1003
+ return (useContext(LocaleContext) ?? FALLBACK).locale;
1004
+ }
582
1005
  var DEFAULT_ICON_SIZE = {
583
1006
  page: 48,
584
1007
  widget: 28
585
1008
  };
586
1009
  var ErrorState = ({
587
- title = "\uBB38\uC81C\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4",
1010
+ title: titleProp,
588
1011
  description,
589
1012
  icon,
590
1013
  action,
@@ -592,6 +1015,8 @@ var ErrorState = ({
592
1015
  className,
593
1016
  ...props
594
1017
  }) => {
1018
+ const t = useLocaleText();
1019
+ const title = titleProp === void 0 ? t("errorState.title") : titleProp;
595
1020
  const resolvedIcon = icon === null ? null : icon ?? /* @__PURE__ */ jsx(TriangleAlert, { size: DEFAULT_ICON_SIZE[variant], strokeWidth: 1.5 });
596
1021
  return /* @__PURE__ */ jsxs(
597
1022
  "div",
@@ -609,15 +1034,17 @@ var ErrorState = ({
609
1034
  );
610
1035
  };
611
1036
  var BottomNav = ({
612
- ariaLabel = "\uC8FC\uC694 \uBA54\uB274",
1037
+ ariaLabel: ariaLabelProp,
613
1038
  className,
614
1039
  children,
615
1040
  ...props
616
1041
  }) => {
1042
+ const t = useLocaleText();
1043
+ const ariaLabel = ariaLabelProp ?? t("bottomNav.label");
617
1044
  return /* @__PURE__ */ jsx("nav", { className: cn("bottom_nav", className), "aria-label": ariaLabel, ...props, children });
618
1045
  };
619
1046
  var BottomNavItem = (props) => {
620
- const { icon, label, active, badge, as = "button", className, disabled, ...rest } = props;
1047
+ const { icon, label, active, badge, as, className, disabled, ref, ...rest } = props;
621
1048
  const classes = cn(
622
1049
  "bottom_nav_item",
623
1050
  active && "bottom_nav_item_active",
@@ -625,6 +1052,8 @@ var BottomNavItem = (props) => {
625
1052
  className
626
1053
  );
627
1054
  const ariaCurrent = active ? "page" : void 0;
1055
+ const anchorRest = rest;
1056
+ const Tag = as ?? (anchorRest.href != null ? "a" : "button");
628
1057
  const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
629
1058
  /* @__PURE__ */ jsxs("span", { className: "bottom_nav_item_icon", "aria-hidden": "true", children: [
630
1059
  icon,
@@ -632,38 +1061,45 @@ var BottomNavItem = (props) => {
632
1061
  ] }),
633
1062
  /* @__PURE__ */ jsx("span", { className: "bottom_nav_item_label", children: label })
634
1063
  ] });
635
- if (as === "a") {
636
- const { href, onClick: onClick2, ...anchorRest } = rest;
1064
+ if (Tag === "button") {
1065
+ const {
1066
+ type,
1067
+ onClick: onClick2,
1068
+ href: _href,
1069
+ ...buttonRest
1070
+ } = rest;
637
1071
  return /* @__PURE__ */ jsx(
638
- "a",
1072
+ "button",
639
1073
  {
1074
+ ref,
1075
+ type: type ?? "button",
640
1076
  className: classes,
641
- href,
1077
+ disabled,
642
1078
  "aria-current": ariaCurrent,
643
- "aria-disabled": disabled ? "true" : void 0,
644
- tabIndex: disabled ? -1 : void 0,
645
- onClick: (e) => {
646
- if (disabled) {
647
- e.preventDefault();
648
- return;
649
- }
650
- onClick2?.(e);
651
- },
652
- ...anchorRest,
1079
+ onClick: onClick2,
1080
+ ...buttonRest,
653
1081
  children: content
654
1082
  }
655
1083
  );
656
1084
  }
657
- const { type, onClick, ...buttonRest } = rest;
1085
+ const { onClick, tabIndex, ...tagProps } = anchorRest;
658
1086
  return /* @__PURE__ */ jsx(
659
- "button",
1087
+ Tag,
660
1088
  {
661
- type: type ?? "button",
1089
+ ...tagProps,
1090
+ ref,
662
1091
  className: classes,
663
- disabled,
664
1092
  "aria-current": ariaCurrent,
665
- onClick,
666
- ...buttonRest,
1093
+ "aria-disabled": disabled ? "true" : void 0,
1094
+ tabIndex: disabled ? -1 : tabIndex,
1095
+ onClick: (event) => {
1096
+ if (disabled) {
1097
+ event.preventDefault();
1098
+ event.stopPropagation();
1099
+ return;
1100
+ }
1101
+ onClick?.(event);
1102
+ },
667
1103
  children: content
668
1104
  }
669
1105
  );
@@ -674,10 +1110,12 @@ var BottomNavSpacer = ({ className, ...props }) => {
674
1110
  var Breadcrumb = ({
675
1111
  items,
676
1112
  separator,
677
- navLabel = "\uD604\uC7AC \uC704\uCE58",
1113
+ navLabel: navLabelProp,
678
1114
  className,
679
1115
  ...props
680
1116
  }) => {
1117
+ const t = useLocaleText();
1118
+ const navLabel = navLabelProp ?? t("breadcrumb.label");
681
1119
  const sep = separator ?? /* @__PURE__ */ jsx(ChevronRight, { size: iconSize.xs, "aria-hidden": "true" });
682
1120
  return /* @__PURE__ */ jsx("nav", { "aria-label": navLabel, className: cn("breadcrumb", className), ...props, children: /* @__PURE__ */ jsx("ol", { className: "breadcrumb_list", children: items.map((item, idx) => {
683
1121
  const isLast = idx === items.length - 1;
@@ -1018,7 +1456,7 @@ var Sidebar = ({
1018
1456
  defaultCollapsed = false,
1019
1457
  onCollapsedChange,
1020
1458
  collapsible = true,
1021
- toggleLabel = "\uC0AC\uC774\uB4DC\uBC14 \uD1A0\uAE00",
1459
+ toggleLabel: toggleLabelProp,
1022
1460
  width = 240,
1023
1461
  collapsedWidth = 64,
1024
1462
  mode = "auto",
@@ -1027,6 +1465,8 @@ var Sidebar = ({
1027
1465
  style,
1028
1466
  ...props
1029
1467
  }) => {
1468
+ const t = useLocaleText();
1469
+ const toggleLabel = toggleLabelProp ?? t("sidebar.toggle");
1030
1470
  const isControlled = collapsedProp !== void 0;
1031
1471
  const [internalCollapsed, setInternalCollapsed] = React11.useState(defaultCollapsed);
1032
1472
  const collapsed = isControlled ? collapsedProp : internalCollapsed;
@@ -1070,20 +1510,61 @@ var Sidebar = ({
1070
1510
  );
1071
1511
  };
1072
1512
  var SidebarItem = (props) => {
1073
- const { icon, active, trailing, as = "button", className, children, ...rest } = props;
1074
- const classes = cn("sidebar_item", active && "sidebar_item_active", className);
1513
+ const { icon, active, trailing, as, className, children, disabled, ref, ...rest } = props;
1514
+ const classes = cn(
1515
+ "sidebar_item",
1516
+ active && "sidebar_item_active",
1517
+ disabled && "sidebar_item_disabled",
1518
+ className
1519
+ );
1075
1520
  const ariaCurrent = active ? "page" : void 0;
1521
+ const anchorRest = rest;
1522
+ const Tag = as ?? (anchorRest.href != null ? "a" : "button");
1076
1523
  const inner = /* @__PURE__ */ jsxs(Fragment$1, { children: [
1077
1524
  icon && /* @__PURE__ */ jsx("span", { className: "sidebar_item_icon", "aria-hidden": "true", children: icon }),
1078
1525
  /* @__PURE__ */ jsx("span", { className: "sidebar_item_label", children }),
1079
1526
  trailing && /* @__PURE__ */ jsx("span", { className: "sidebar_item_trailing", children: trailing })
1080
1527
  ] });
1081
- if (as === "a") {
1082
- const { href, ...anchorRest } = rest;
1083
- return /* @__PURE__ */ jsx("a", { className: classes, href, "aria-current": ariaCurrent, ...anchorRest, children: inner });
1528
+ if (Tag === "button") {
1529
+ const {
1530
+ type,
1531
+ href: _href,
1532
+ ...buttonRest
1533
+ } = rest;
1534
+ return /* @__PURE__ */ jsx(
1535
+ "button",
1536
+ {
1537
+ ref,
1538
+ type: type ?? "button",
1539
+ className: classes,
1540
+ disabled,
1541
+ "aria-current": ariaCurrent,
1542
+ ...buttonRest,
1543
+ children: inner
1544
+ }
1545
+ );
1084
1546
  }
1085
- const { type, ...buttonRest } = rest;
1086
- return /* @__PURE__ */ jsx("button", { type: type ?? "button", className: classes, "aria-current": ariaCurrent, ...buttonRest, children: inner });
1547
+ const { onClick, tabIndex, ...tagProps } = anchorRest;
1548
+ return /* @__PURE__ */ jsx(
1549
+ Tag,
1550
+ {
1551
+ ...tagProps,
1552
+ ref,
1553
+ className: classes,
1554
+ "aria-current": ariaCurrent,
1555
+ "aria-disabled": disabled || void 0,
1556
+ tabIndex: disabled ? -1 : tabIndex,
1557
+ onClick: (event) => {
1558
+ if (disabled) {
1559
+ event.preventDefault();
1560
+ event.stopPropagation();
1561
+ return;
1562
+ }
1563
+ onClick?.(event);
1564
+ },
1565
+ children: inner
1566
+ }
1567
+ );
1087
1568
  };
1088
1569
  var SidebarSection = ({ label, className, children, ...props }) => {
1089
1570
  return /* @__PURE__ */ jsxs("div", { className: cn("sidebar_section", className), ...props, children: [
@@ -2103,7 +2584,8 @@ var Chip = ({
2103
2584
  className,
2104
2585
  ...props
2105
2586
  }) => {
2106
- const removeAriaLabel = removeLabel ?? `${label} \uC81C\uAC70`;
2587
+ const t = useLocaleText();
2588
+ const removeAriaLabel = removeLabel ?? t("chip.remove", { label: String(label) });
2107
2589
  const [iconHovered, setIconHovered] = useState(false);
2108
2590
  const isStatic = type === "static";
2109
2591
  const hasLeading = !isStatic && selected;
@@ -2200,252 +2682,382 @@ var Chip = ({
2200
2682
  )
2201
2683
  ] });
2202
2684
  };
2203
- var Divider = ({ weight = "standard", className, ...props }) => {
2204
- const dividerClassName = cn("divider", `divider_weight_${weight}`, className);
2205
- return /* @__PURE__ */ jsx("hr", { className: dividerClassName, ...props });
2206
- };
2207
- var Button = (props) => {
2208
- const {
2209
- variant = "filled",
2210
- size = "md",
2211
- leadingIcon,
2212
- trailingIcon,
2213
- fullWidth = false,
2214
- radius: radius2,
2215
- danger = false,
2216
- disabled = false,
2217
- as,
2218
- className,
2219
- children,
2220
- ref,
2221
- ...rest
2222
- } = props;
2223
- const buttonClassName = cn(
2224
- "button",
2225
- `button_variant_${variant}`,
2226
- `button_size_${size}`,
2227
- fullWidth && "button_full_width",
2228
- radius2 && `button_radius_${radius2}`,
2229
- danger && "button_danger",
2230
- // anchor 엔 native :disabled 가 안 먹으므로 클래스로 비활성 스타일 적용 (button 도 무해)
2231
- disabled && "button_disabled",
2232
- className
2233
- );
2234
- const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
2235
- leadingIcon && /* @__PURE__ */ jsx("span", { className: "button_icon", "aria-hidden": "true", children: leadingIcon }),
2236
- children && /* @__PURE__ */ jsx("span", { className: "button_label", children }),
2237
- trailingIcon && /* @__PURE__ */ jsx("span", { className: "button_icon", "aria-hidden": "true", children: trailingIcon })
2238
- ] });
2239
- const anchorRest = rest;
2240
- const renderAnchor = as === "a" || as === void 0 && anchorRest.href != null;
2241
- if (renderAnchor) {
2242
- const { onClick, tabIndex, ...anchorProps } = anchorRest;
2243
- return /* @__PURE__ */ jsx(
2244
- "a",
2245
- {
2246
- ...anchorProps,
2247
- ref,
2248
- className: buttonClassName,
2249
- "aria-disabled": disabled || void 0,
2250
- tabIndex: disabled ? -1 : tabIndex,
2251
- onClick: disabled ? (e) => e.preventDefault() : onClick,
2252
- children: content
2253
- }
2254
- );
2685
+ var FormContext = createContext(void 0);
2686
+ function useFormError(name) {
2687
+ return useContext(FormContext)?.errors?.[name];
2688
+ }
2689
+ var Form = ({ errors, onSubmit, children, className, ...props }) => /* @__PURE__ */ jsx(FormContext.Provider, { value: { errors }, children: /* @__PURE__ */ jsx(
2690
+ "form",
2691
+ {
2692
+ className: cn("form", className),
2693
+ onSubmit: (event) => {
2694
+ event.preventDefault();
2695
+ onSubmit?.(event);
2696
+ },
2697
+ ...props,
2698
+ children
2255
2699
  }
2256
- const { type = "button", ...buttonRest } = rest;
2257
- return /* @__PURE__ */ jsx(
2258
- "button",
2259
- {
2260
- ref,
2261
- type,
2262
- disabled,
2263
- className: buttonClassName,
2264
- ...buttonRest,
2265
- children: content
2266
- }
2267
- );
2268
- };
2269
- var HeroActionButton = ({
2270
- action,
2271
- variant
2272
- }) => action.href ? /* @__PURE__ */ jsx(Button, { as: "a", href: action.href, size: "lg", variant, onClick: action.onClick, children: action.label }) : /* @__PURE__ */ jsx(Button, { size: "lg", variant, onClick: action.onClick, children: action.label });
2273
- var Hero = ({
2274
- height = "md",
2275
- align = "left",
2276
- backgroundImage,
2277
- backgroundColor,
2278
- overlay,
2279
- title,
2280
- subtitle,
2281
- eyebrow,
2282
- textColor = "auto",
2283
- primaryAction,
2284
- secondaryAction,
2700
+ ) });
2701
+ var FormActions = ({ align = "end", children, className, ...props }) => /* @__PURE__ */ jsx("div", { className: cn("form_actions", `form_actions_${align}`, className), ...props, children });
2702
+ FormActions.displayName = "Form.Actions";
2703
+ Form.Actions = FormActions;
2704
+ var FieldContext = createContext(void 0);
2705
+ function useFieldControl() {
2706
+ return useContext(FieldContext);
2707
+ }
2708
+ var Field = ({
2709
+ name,
2710
+ label,
2711
+ required = false,
2712
+ help,
2713
+ error: errorProp,
2285
2714
  children,
2286
2715
  className,
2287
- style,
2288
2716
  ...props
2289
2717
  }) => {
2290
- const resolvedOverlay = overlay === true ? "dark" : overlay;
2291
- const isDarkOverlay = resolvedOverlay === "dark";
2292
- const resolvedTextColor = textColor === "auto" ? isDarkOverlay || backgroundImage && !resolvedOverlay ? "inverse" : "default" : textColor;
2293
- const heroClassName = cn(
2294
- "hero",
2295
- `hero_height_${height}`,
2296
- `hero_align_${align}`,
2297
- resolvedOverlay && `hero_overlay_${resolvedOverlay}`,
2298
- `hero_text_${resolvedTextColor}`,
2299
- className
2300
- );
2301
- const inlineStyle = { ...style };
2302
- if (backgroundImage) inlineStyle.backgroundImage = `url("${backgroundImage}")`;
2303
- if (backgroundColor) inlineStyle.backgroundColor = backgroundColor;
2304
- return /* @__PURE__ */ jsxs("section", { className: heroClassName, style: inlineStyle, ...props, children: [
2305
- resolvedOverlay && /* @__PURE__ */ jsx("div", { className: "hero_overlay", "aria-hidden": "true" }),
2306
- /* @__PURE__ */ jsxs("div", { className: "hero_content", children: [
2307
- eyebrow && /* @__PURE__ */ jsx("div", { className: "hero_eyebrow", children: eyebrow }),
2308
- title && /* @__PURE__ */ jsx("h1", { className: "hero_title", children: title }),
2309
- subtitle && /* @__PURE__ */ jsx("p", { className: "hero_subtitle", children: subtitle }),
2310
- (primaryAction || secondaryAction || children) && /* @__PURE__ */ jsxs("div", { className: "hero_actions", children: [
2311
- primaryAction && /* @__PURE__ */ jsx(HeroActionButton, { action: primaryAction, variant: "filled" }),
2312
- secondaryAction && /* @__PURE__ */ jsx(HeroActionButton, { action: secondaryAction, variant: "outline" }),
2313
- children
2314
- ] })
2315
- ] })
2718
+ const generatedId = useId();
2719
+ const inputId = `${name}-${generatedId}`;
2720
+ const formError = useFormError(name);
2721
+ const error = errorProp ?? formError;
2722
+ const labelId = label ? `${inputId}-label` : void 0;
2723
+ const helpId = help ? `${inputId}-help` : void 0;
2724
+ const errorId = error ? `${inputId}-error` : void 0;
2725
+ const showHelp = !error && !!help;
2726
+ const control = {
2727
+ inputId,
2728
+ labelId,
2729
+ describedBy: [errorId, showHelp ? helpId : void 0].filter(Boolean).join(" ") || void 0,
2730
+ invalid: !!error,
2731
+ required
2732
+ };
2733
+ return /* @__PURE__ */ jsxs("div", { className: cn("field", !!error && "field_error", className), ...props, children: [
2734
+ label && /* @__PURE__ */ jsxs("label", { id: labelId, htmlFor: inputId, className: "field_label", children: [
2735
+ label,
2736
+ required && /* @__PURE__ */ jsx("span", { className: "field_required", "aria-hidden": "true", children: "*" })
2737
+ ] }),
2738
+ /* @__PURE__ */ jsx(FieldContext.Provider, { value: control, children }),
2739
+ showHelp && /* @__PURE__ */ jsx("div", { id: helpId, className: "field_help", children: help }),
2740
+ error && /* @__PURE__ */ jsx("div", { id: errorId, className: "field_message", children: error })
2316
2741
  ] });
2317
2742
  };
2318
- var Icon = ({ icon: IconComponent, ...props }) => {
2319
- const hasLabel = !!props["aria-label"];
2320
- return /* @__PURE__ */ jsx(
2321
- IconComponent,
2322
- {
2323
- "aria-hidden": hasLabel ? void 0 : true,
2324
- focusable: hasLabel ? void 0 : false,
2325
- ...props
2326
- }
2327
- );
2328
- };
2329
- Icon.displayName = "Icon";
2330
- var ListItem = ({
2331
- overline,
2743
+ var ClearIcon = () => /* @__PURE__ */ jsx(X, { size: iconSize.lg, "aria-hidden": "true" });
2744
+ var TextField = ({
2745
+ id,
2332
2746
  label,
2747
+ showLabel = true,
2333
2748
  supportingText,
2334
- metadata,
2335
- leadingElement,
2336
- trailingElement,
2337
- alignment,
2338
- disabled,
2339
- selected,
2340
- onClick,
2749
+ error,
2750
+ success,
2751
+ identifier,
2752
+ leadingIcon,
2753
+ trailingIcon,
2754
+ leadingAction,
2755
+ trailingAction,
2756
+ showPasswordToggle,
2757
+ passwordToggleLabels,
2758
+ clearable,
2759
+ clearLabel: clearLabelProp,
2760
+ type,
2761
+ fullWidth,
2762
+ size = "md",
2763
+ variant = "outline",
2341
2764
  className,
2765
+ onValueChange,
2766
+ onChangeAction,
2767
+ imeStrategy = "delayed",
2768
+ value,
2769
+ defaultValue,
2770
+ transformValue,
2771
+ ref,
2342
2772
  ...props
2343
2773
  }) => {
2344
- const isOneLine = !overline && !supportingText && !metadata;
2345
- const effectiveAlignment = alignment ?? (isOneLine ? "middle" : "top");
2774
+ const t = useLocaleText();
2775
+ const clearLabel = clearLabelProp ?? t("textField.clear");
2776
+ const generatedId = useId();
2777
+ const field = useFieldControl();
2778
+ const inputId = id ?? field?.inputId ?? generatedId;
2779
+ const helperId = supportingText ? `${inputId}-help` : void 0;
2780
+ const describedBy = field?.describedBy ?? helperId;
2781
+ const isControlled = value !== void 0;
2782
+ const applyTransform = (nextValue) => transformValue ? transformValue(nextValue) : nextValue;
2783
+ const [innerValue, setInnerValue] = useState(() => applyTransform(value ?? defaultValue ?? ""));
2784
+ const isComposingRef = useRef(false);
2785
+ const lastEmittedValueRef = useRef(innerValue);
2786
+ const [prevValue, setPrevValue] = useState(value);
2787
+ if (isControlled && value !== prevValue && !isComposingRef.current) {
2788
+ setPrevValue(value);
2789
+ const nextValue = applyTransform(value ?? "");
2790
+ setInnerValue(nextValue);
2791
+ lastEmittedValueRef.current = nextValue;
2792
+ }
2793
+ const emit = useCallback(
2794
+ (nextValue) => {
2795
+ setInnerValue(nextValue);
2796
+ if (nextValue !== lastEmittedValueRef.current) {
2797
+ lastEmittedValueRef.current = nextValue;
2798
+ (onValueChange ?? onChangeAction)?.(nextValue);
2799
+ }
2800
+ },
2801
+ [onValueChange, onChangeAction]
2802
+ );
2803
+ const handleClear = useCallback(() => {
2804
+ emit("");
2805
+ }, [emit]);
2806
+ const [passwordRevealed, setPasswordRevealed] = useState(false);
2807
+ const togglePassword = useCallback(() => {
2808
+ setPasswordRevealed((revealed) => !revealed);
2809
+ }, []);
2810
+ let resolvedType = type;
2811
+ if (showPasswordToggle) {
2812
+ resolvedType = passwordRevealed ? "text" : type ?? "password";
2813
+ }
2814
+ const isError = !!error || !!field?.invalid;
2815
+ const isSuccess = !!success && !isError;
2346
2816
  const rootClassName = cn(
2347
- "list_item",
2348
- `list_item_align_${effectiveAlignment}`,
2349
- disabled && "list_item_disabled",
2350
- selected && "list_item_selected",
2351
- onClick && "list_item_interactive",
2817
+ "text_field",
2818
+ `text_field_variant_${variant}`,
2819
+ size === "sm" && "text_field_size_sm",
2820
+ size === "lg" && "text_field_size_lg",
2821
+ fullWidth && "text_field_full_width",
2822
+ isError && "text_field_error",
2823
+ isSuccess && "text_field_success",
2824
+ props.disabled && "text_field_disabled",
2352
2825
  className
2353
2826
  );
2354
- return (
2355
- // biome-ignore lint/a11y/noStaticElementInteractions: optional interactive list item - role=button + tabIndex set conditionally based on onClick
2356
- /* @__PURE__ */ jsx(
2357
- "div",
2358
- {
2359
- className: rootClassName,
2360
- onClick: disabled ? void 0 : onClick,
2361
- onKeyDown: (e) => {
2362
- if (disabled || !onClick) return;
2363
- if (e.key === "Enter" || e.key === " ") {
2364
- e.preventDefault();
2365
- e.currentTarget.click();
2366
- }
2367
- },
2368
- role: onClick ? "button" : void 0,
2369
- tabIndex: onClick && !disabled ? 0 : void 0,
2370
- "aria-disabled": disabled || void 0,
2371
- "aria-pressed": onClick && selected !== void 0 ? selected : void 0,
2372
- ...props,
2373
- children: /* @__PURE__ */ jsxs("div", { className: "list_item_state_layer", children: [
2374
- leadingElement && /* @__PURE__ */ jsx("div", { className: "list_item_leading", children: leadingElement }),
2375
- /* @__PURE__ */ jsxs("div", { className: "list_item_content", children: [
2376
- overline && /* @__PURE__ */ jsx("div", { className: "list_item_overline", children: overline }),
2377
- /* @__PURE__ */ jsx("div", { className: "list_item_label", children: label }),
2378
- supportingText && /* @__PURE__ */ jsx("div", { className: "list_item_supporting", children: supportingText }),
2379
- metadata && /* @__PURE__ */ jsx("div", { className: "list_item_metadata", children: metadata })
2380
- ] }),
2381
- trailingElement && /* @__PURE__ */ jsx("div", { className: "list_item_trailing", children: trailingElement })
2382
- ] })
2383
- }
2384
- )
2385
- );
2827
+ const passwordToggleLabel = passwordRevealed ? passwordToggleLabels?.hide ?? t("textField.passwordHide") : passwordToggleLabels?.show ?? t("textField.passwordShow");
2828
+ const resolvedTrailing = showPasswordToggle ? /* @__PURE__ */ jsx("span", { className: "text_field_icon text_field_action", children: /* @__PURE__ */ jsx(
2829
+ "button",
2830
+ {
2831
+ type: "button",
2832
+ onClick: togglePassword,
2833
+ "aria-label": passwordToggleLabel,
2834
+ disabled: props.disabled,
2835
+ children: passwordRevealed ? /* @__PURE__ */ jsx(EyeOff, { size: iconSize.lg, "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Eye, { size: iconSize.lg, "aria-hidden": "true" })
2836
+ }
2837
+ ) }) : clearable && innerValue ? /* @__PURE__ */ jsx(
2838
+ "button",
2839
+ {
2840
+ type: "button",
2841
+ className: "text_field_clear",
2842
+ onClick: handleClear,
2843
+ "aria-label": clearLabel,
2844
+ disabled: props.disabled,
2845
+ children: /* @__PURE__ */ jsx(ClearIcon, {})
2846
+ }
2847
+ ) : trailingAction ? /* @__PURE__ */ jsx("span", { className: "text_field_icon text_field_action", children: trailingAction }) : trailingIcon ? /* @__PURE__ */ jsx("span", { className: "text_field_icon", "aria-hidden": "true", children: trailingIcon }) : null;
2848
+ const resolvedLeading = leadingAction ? /* @__PURE__ */ jsx("span", { className: "text_field_icon text_field_action", children: leadingAction }) : leadingIcon ? /* @__PURE__ */ jsx("span", { className: "text_field_icon", "aria-hidden": "true", children: leadingIcon }) : null;
2849
+ return /* @__PURE__ */ jsxs("div", { className: rootClassName, children: [
2850
+ label && showLabel && /* @__PURE__ */ jsx("label", { htmlFor: inputId, className: "text_field_label", children: label }),
2851
+ /* @__PURE__ */ jsx("div", { className: "text_field_container", children: /* @__PURE__ */ jsxs("div", { className: "text_field_inner", children: [
2852
+ resolvedLeading,
2853
+ /* @__PURE__ */ jsx(
2854
+ "div",
2855
+ {
2856
+ className: cn(
2857
+ "text_field_input_wrap",
2858
+ resolvedTrailing && "text_field_input_wrap_no_pad_right"
2859
+ ),
2860
+ children: /* @__PURE__ */ jsx(
2861
+ "input",
2862
+ {
2863
+ id: inputId,
2864
+ ref,
2865
+ className: cn("text_field_input", identifier && "text_field_input_identifier"),
2866
+ "aria-invalid": isError,
2867
+ "aria-describedby": describedBy,
2868
+ "aria-required": field?.required || void 0,
2869
+ "aria-label": !showLabel ? label : void 0,
2870
+ ...props,
2871
+ type: resolvedType,
2872
+ value: innerValue,
2873
+ onCompositionStart: () => {
2874
+ isComposingRef.current = true;
2875
+ },
2876
+ onCompositionEnd: (event) => {
2877
+ isComposingRef.current = false;
2878
+ emit(applyTransform(event.currentTarget.value));
2879
+ },
2880
+ onChange: (event) => {
2881
+ const rawValue = event.target.value;
2882
+ if (isComposingRef.current) {
2883
+ setInnerValue(rawValue);
2884
+ if (imeStrategy === "immediate" && rawValue !== lastEmittedValueRef.current) {
2885
+ lastEmittedValueRef.current = rawValue;
2886
+ (onValueChange ?? onChangeAction)?.(rawValue);
2887
+ }
2888
+ return;
2889
+ }
2890
+ emit(applyTransform(rawValue));
2891
+ }
2892
+ }
2893
+ )
2894
+ }
2895
+ ),
2896
+ resolvedTrailing
2897
+ ] }) }),
2898
+ supportingText && /* @__PURE__ */ jsx("div", { id: helperId, className: "text_field_helper", children: supportingText })
2899
+ ] });
2386
2900
  };
2387
- var MediaCard = ({
2388
- image,
2389
- imagePosition = "top",
2390
- aspectRatio,
2391
- heading,
2392
- headingAs: HeadingTag = "h3",
2393
- eyebrow,
2394
- shadow = "sm",
2395
- bordered = false,
2396
- clickable = false,
2397
- meta,
2398
- children,
2399
- className,
2400
- ...props
2401
- }) => {
2402
- const cardClassName = cn(
2403
- "media_card",
2404
- `media_card_image_${imagePosition}`,
2405
- `media_card_shadow_${shadow}`,
2406
- bordered && "media_card_bordered",
2407
- clickable && "media_card_clickable",
2901
+ TextField.displayName = "TextField";
2902
+ var Button = (props) => {
2903
+ const {
2904
+ variant = "filled",
2905
+ size = "md",
2906
+ leadingIcon,
2907
+ trailingIcon,
2908
+ fullWidth = false,
2909
+ radius: radius2,
2910
+ danger = false,
2911
+ disabled = false,
2912
+ as,
2913
+ className,
2914
+ children,
2915
+ ref,
2916
+ ...rest
2917
+ } = props;
2918
+ const buttonClassName = cn(
2919
+ "button",
2920
+ `button_variant_${variant}`,
2921
+ `button_size_${size}`,
2922
+ fullWidth && "button_full_width",
2923
+ radius2 && `button_radius_${radius2}`,
2924
+ danger && "button_danger",
2925
+ // anchor 엔 native :disabled 가 안 먹으므로 클래스로 비활성 스타일 적용 (button 도 무해)
2926
+ disabled && "button_disabled",
2408
2927
  className
2409
2928
  );
2410
- const isOverlay = imagePosition === "overlay";
2411
- const cardStyle = isOverlay && aspectRatio ? { aspectRatio } : void 0;
2412
- const wrapStyle = !isOverlay && aspectRatio ? { aspectRatio } : void 0;
2413
- return /* @__PURE__ */ jsxs("div", { className: cardClassName, style: cardStyle, ...props, children: [
2414
- /* @__PURE__ */ jsxs("div", { className: "media_card_image_wrap", style: wrapStyle, children: [
2415
- /* @__PURE__ */ jsx("img", { className: "media_card_image", src: image.src, alt: image.alt, loading: "lazy" }),
2416
- isOverlay && /* @__PURE__ */ jsx("div", { className: "media_card_overlay", "aria-hidden": "true" })
2417
- ] }),
2418
- /* @__PURE__ */ jsxs("div", { className: "media_card_body", children: [
2419
- eyebrow && /* @__PURE__ */ jsx("div", { className: "media_card_eyebrow", children: eyebrow }),
2420
- heading && /* @__PURE__ */ jsx(HeadingTag, { className: "media_card_heading", children: heading }),
2421
- children && /* @__PURE__ */ jsx("div", { className: "media_card_content", children }),
2422
- meta && /* @__PURE__ */ jsx("div", { className: "media_card_meta", children: meta })
2423
- ] })
2929
+ const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
2930
+ leadingIcon && /* @__PURE__ */ jsx("span", { className: "button_icon", "aria-hidden": "true", children: leadingIcon }),
2931
+ children && /* @__PURE__ */ jsx("span", { className: "button_label", children }),
2932
+ trailingIcon && /* @__PURE__ */ jsx("span", { className: "button_icon", "aria-hidden": "true", children: trailingIcon })
2424
2933
  ] });
2934
+ const anchorRest = rest;
2935
+ const Tag = as ?? (anchorRest.href != null ? "a" : "button");
2936
+ if (Tag === "button") {
2937
+ const {
2938
+ type = "button",
2939
+ href: _href,
2940
+ ...buttonRest
2941
+ } = rest;
2942
+ return /* @__PURE__ */ jsx(
2943
+ "button",
2944
+ {
2945
+ ref,
2946
+ type,
2947
+ disabled,
2948
+ className: buttonClassName,
2949
+ ...buttonRest,
2950
+ children: content
2951
+ }
2952
+ );
2953
+ }
2954
+ const { onClick, tabIndex, ...tagProps } = anchorRest;
2955
+ return /* @__PURE__ */ jsx(
2956
+ Tag,
2957
+ {
2958
+ ...tagProps,
2959
+ ref,
2960
+ className: buttonClassName,
2961
+ "aria-disabled": disabled || void 0,
2962
+ tabIndex: disabled ? -1 : tabIndex,
2963
+ onClick: disabled ? (event) => {
2964
+ event.preventDefault();
2965
+ event.stopPropagation();
2966
+ } : onClick,
2967
+ children: content
2968
+ }
2969
+ );
2425
2970
  };
2426
- var Prose = ({ size = "md", className, children, ref, ...props }) => {
2427
- const rootRef = React11.useRef(null);
2428
- React11.useImperativeHandle(ref, () => rootRef.current, []);
2429
- useSafeLayoutEffect(() => {
2430
- const root = rootRef.current;
2431
- if (!root) return;
2432
- const targets = Array.from(root.querySelectorAll("pre, table"));
2433
- const sync = () => {
2434
- for (const el of targets) {
2435
- if (el.scrollWidth > el.clientWidth) el.setAttribute("tabindex", "0");
2436
- else el.removeAttribute("tabindex");
2971
+ var range = (start, end) => {
2972
+ const out = [];
2973
+ for (let i = start; i <= end; i += 1) out.push(i);
2974
+ return out;
2975
+ };
2976
+ var getPaginationItems = (page, totalPages) => {
2977
+ if (totalPages <= 7) return range(1, totalPages);
2978
+ const items = [];
2979
+ const last = totalPages;
2980
+ const sibling = 2;
2981
+ if (page <= sibling + 2) {
2982
+ for (const p of range(1, sibling + 3)) items.push(p);
2983
+ items.push("ellipsis");
2984
+ items.push(last);
2985
+ return items;
2986
+ }
2987
+ if (page >= last - sibling - 1) {
2988
+ items.push(1);
2989
+ items.push("ellipsis");
2990
+ for (const p of range(last - sibling - 2, last)) items.push(p);
2991
+ return items;
2992
+ }
2993
+ items.push(1);
2994
+ items.push("ellipsis");
2995
+ for (const p of range(page - sibling, page + sibling)) items.push(p);
2996
+ items.push("ellipsis");
2997
+ items.push(last);
2998
+ return items;
2999
+ };
3000
+ var Pagination = ({
3001
+ page,
3002
+ totalPages,
3003
+ onPageChange,
3004
+ onChange,
3005
+ prevLabel: prevLabelProp,
3006
+ nextLabel: nextLabelProp,
3007
+ navLabel: navLabelProp
3008
+ }) => {
3009
+ const t = useLocaleText();
3010
+ const prevLabel = prevLabelProp ?? t("pagination.prev");
3011
+ const nextLabel = nextLabelProp ?? t("pagination.next");
3012
+ const navLabel = navLabelProp ?? t("pagination.label");
3013
+ const emit = onPageChange ?? onChange;
3014
+ const prevDisabled = page <= 1;
3015
+ const nextDisabled = page >= totalPages;
3016
+ const items = React11.useMemo(() => getPaginationItems(page, totalPages), [page, totalPages]);
3017
+ return /* @__PURE__ */ jsxs("nav", { className: "pagination", "aria-label": navLabel, children: [
3018
+ /* @__PURE__ */ jsx(
3019
+ "button",
3020
+ {
3021
+ type: "button",
3022
+ className: "pagination_item",
3023
+ onClick: () => emit?.(page - 1),
3024
+ disabled: prevDisabled,
3025
+ "aria-label": prevLabel,
3026
+ children: "\u2039"
2437
3027
  }
2438
- };
2439
- sync();
2440
- if (typeof ResizeObserver === "undefined") return;
2441
- const observer = new ResizeObserver(sync);
2442
- observer.observe(root);
2443
- for (const el of targets) observer.observe(el);
2444
- return () => observer.disconnect();
2445
- }, [children]);
2446
- return /* @__PURE__ */ jsx("div", { ref: rootRef, className: cn("prose", `prose_size_${size}`, className), ...props, children });
3028
+ ),
3029
+ /* @__PURE__ */ jsx("ul", { className: "pagination_pages", children: items.map((it, idx) => {
3030
+ if (it === "ellipsis") {
3031
+ const prev = items[idx - 1];
3032
+ const next = items[idx + 1];
3033
+ return /* @__PURE__ */ jsx("li", { className: "pagination_ellipsis", "aria-hidden": "true", children: "\u2026" }, `e-${prev}-${next}`);
3034
+ }
3035
+ const isActive = it === page;
3036
+ const buttonClassName = cn("pagination_page_button", { pagination_active: isActive });
3037
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
3038
+ "button",
3039
+ {
3040
+ type: "button",
3041
+ className: buttonClassName,
3042
+ onClick: () => emit?.(it),
3043
+ "aria-current": isActive ? "page" : void 0,
3044
+ children: it
3045
+ }
3046
+ ) }, it);
3047
+ }) }),
3048
+ /* @__PURE__ */ jsx(
3049
+ "button",
3050
+ {
3051
+ type: "button",
3052
+ className: "pagination_item",
3053
+ onClick: () => emit?.(page + 1),
3054
+ disabled: nextDisabled,
3055
+ "aria-label": nextLabel,
3056
+ children: "\u203A"
3057
+ }
3058
+ )
3059
+ ] });
2447
3060
  };
2448
- Prose.displayName = "Prose";
2449
3061
  var Skeleton = ({
2450
3062
  variant = "text",
2451
3063
  width,
@@ -2496,6 +3108,7 @@ var Checkbox = ({
2496
3108
  props.disabled && "checkbox_disabled",
2497
3109
  className
2498
3110
  );
3111
+ const field = useFieldControl();
2499
3112
  return /* @__PURE__ */ jsxs("label", { className: rootClassName, children: [
2500
3113
  /* @__PURE__ */ jsx(
2501
3114
  "input",
@@ -2504,6 +3117,9 @@ var Checkbox = ({
2504
3117
  ref: inputRef,
2505
3118
  type: "checkbox",
2506
3119
  className: "checkbox_input",
3120
+ id: field?.inputId ?? props.id,
3121
+ "aria-describedby": field?.describedBy ?? props["aria-describedby"],
3122
+ "aria-required": field?.required || void 0,
2507
3123
  "aria-invalid": error || void 0
2508
3124
  }
2509
3125
  ),
@@ -2516,7 +3132,7 @@ var Table = ({
2516
3132
  columns,
2517
3133
  data,
2518
3134
  keyExtractor,
2519
- emptyMessage = "\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
3135
+ emptyMessage: emptyMessageProp,
2520
3136
  isLoading = false,
2521
3137
  skeletonRows = 5,
2522
3138
  size = "md",
@@ -2525,16 +3141,21 @@ var Table = ({
2525
3141
  ariaLabel,
2526
3142
  className,
2527
3143
  onRowClick,
2528
- rowClickHint = "\uD074\uB9AD \uAC00\uB2A5\uD55C \uD589",
3144
+ rowClickHint: rowClickHintProp,
2529
3145
  sort,
2530
3146
  onSortChange,
2531
- selectAllAriaLabel = "\uC804\uCCB4 \uC120\uD0DD",
2532
- selectRowAriaLabel = (index) => `${index + 1}\uBC88\uC9F8 \uD589 \uC120\uD0DD`,
3147
+ selectAllAriaLabel: selectAllAriaLabelProp,
3148
+ selectRowAriaLabel: selectRowAriaLabelProp,
2533
3149
  selectable = false,
2534
3150
  rowKey,
2535
3151
  selectedKeys,
2536
3152
  onSelectionChange
2537
3153
  }) => {
3154
+ const t = useLocaleText();
3155
+ const rowClickHint = rowClickHintProp ?? t("table.rowClickHint");
3156
+ const emptyMessage = emptyMessageProp === void 0 ? t("table.empty") : emptyMessageProp;
3157
+ const selectAllAriaLabel = selectAllAriaLabelProp ?? t("table.selectAll");
3158
+ const selectRowAriaLabel = selectRowAriaLabelProp ?? ((index) => t("table.selectRow", { index: index + 1 }));
2538
3159
  const wrapperClassName = cn(
2539
3160
  "table_wrapper",
2540
3161
  `table_size_${size}`,
@@ -2702,10 +3323,312 @@ var Table = ({
2702
3323
  );
2703
3324
  }) })
2704
3325
  ] }),
2705
- onRowClick && rowClickHint && /* @__PURE__ */ jsx("span", { id: rowClickHintId, className: "table_sr_only", children: rowClickHint }),
2706
- isEmpty && /* @__PURE__ */ jsx("div", { className: "table_empty", role: "status", children: emptyMessage })
3326
+ onRowClick && rowClickHint && /* @__PURE__ */ jsx("span", { id: rowClickHintId, className: "table_sr_only", children: rowClickHint }),
3327
+ isEmpty && /* @__PURE__ */ jsx("div", { className: "table_empty", role: "status", children: emptyMessage })
3328
+ ] });
3329
+ };
3330
+ var DataView = ({
3331
+ query,
3332
+ columns,
3333
+ rowKey,
3334
+ toolbar,
3335
+ selectionActions,
3336
+ pagination,
3337
+ empty,
3338
+ sort,
3339
+ onSortChange,
3340
+ onRowClick,
3341
+ ariaLabel,
3342
+ selectionSummary: selectionSummaryProp,
3343
+ clearSelectionLabel: clearSelectionLabelProp,
3344
+ errorTitle: errorTitleProp,
3345
+ retryLabel: retryLabelProp,
3346
+ className,
3347
+ ...props
3348
+ }) => {
3349
+ const t = useLocaleText();
3350
+ const selectionSummary = selectionSummaryProp ?? ((count) => t("dataView.selectionSummary", { count }));
3351
+ const clearSelectionLabel = clearSelectionLabelProp ?? t("dataView.clearSelection");
3352
+ const errorTitle = errorTitleProp ?? t("dataView.errorTitle");
3353
+ const retryLabel = retryLabelProp ?? t("dataView.retry");
3354
+ const searchLabel = toolbar?.searchPlaceholder ?? t("dataView.search");
3355
+ const [selectedKeys, setSelectedKeys] = useState([]);
3356
+ const selectionBarId = useId();
3357
+ const selectable = !!selectionActions?.length;
3358
+ const rows = query.data ?? [];
3359
+ const showEmpty = !query.isLoading && !query.error && rows.length === 0;
3360
+ if (query.error) {
3361
+ return /* @__PURE__ */ jsx("div", { className: cn("data_view", className), ...props, children: /* @__PURE__ */ jsx(
3362
+ ErrorState,
3363
+ {
3364
+ variant: "widget",
3365
+ title: errorTitle,
3366
+ action: query.refetch ? /* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", onClick: query.refetch, children: retryLabel }) : void 0
3367
+ }
3368
+ ) });
3369
+ }
3370
+ return /* @__PURE__ */ jsxs("div", { className: cn("data_view", className), ...props, children: [
3371
+ toolbar && /* @__PURE__ */ jsxs("div", { className: "data_view_toolbar", children: [
3372
+ toolbar.search && /* @__PURE__ */ jsx("div", { className: "data_view_search", children: /* @__PURE__ */ jsx(
3373
+ TextField,
3374
+ {
3375
+ fullWidth: true,
3376
+ size: "sm",
3377
+ type: "search",
3378
+ value: toolbar.searchValue,
3379
+ onValueChange: toolbar.onSearchChange,
3380
+ placeholder: searchLabel,
3381
+ "aria-label": searchLabel,
3382
+ leadingIcon: /* @__PURE__ */ jsx(Search, { size: iconSize.sm })
3383
+ }
3384
+ ) }),
3385
+ toolbar.filters && /* @__PURE__ */ jsx("div", { className: "data_view_filters", children: toolbar.filters })
3386
+ ] }),
3387
+ selectable && selectedKeys.length > 0 && // `role="status"` - 선택이 바뀔 때마다 스크린리더가 개수를 읽는다. 액션 줄이
3388
+ // 시각적으로만 나타나면 키보드 사용자는 무엇이 가능해졌는지 알 수 없다.
3389
+ /* @__PURE__ */ jsxs("div", { id: selectionBarId, className: "data_view_selection", role: "status", children: [
3390
+ /* @__PURE__ */ jsx("span", { className: "data_view_selection_count", children: selectionSummary(selectedKeys.length) }),
3391
+ /* @__PURE__ */ jsxs("div", { className: "data_view_selection_actions", children: [
3392
+ selectionActions?.map((action) => /* @__PURE__ */ jsx(
3393
+ Button,
3394
+ {
3395
+ size: "sm",
3396
+ variant: "outline",
3397
+ danger: action.danger,
3398
+ onClick: () => action.onRun(selectedKeys),
3399
+ children: action.label
3400
+ },
3401
+ action.label
3402
+ )),
3403
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "text", onClick: () => setSelectedKeys([]), children: clearSelectionLabel })
3404
+ ] })
3405
+ ] }),
3406
+ showEmpty ? empty ?? /* @__PURE__ */ jsx(EmptyState, { title: t("dataView.empty") }) : selectable ? (
3407
+ // 판별 union 이라 조건부 스프레드로는 좁혀지지 않는다 - 분기를 명시한다.
3408
+ /* @__PURE__ */ jsx(
3409
+ Table,
3410
+ {
3411
+ columns,
3412
+ data: rows,
3413
+ keyExtractor: rowKey,
3414
+ isLoading: query.isLoading,
3415
+ sort,
3416
+ onSortChange,
3417
+ onRowClick,
3418
+ ariaLabel,
3419
+ selectable: true,
3420
+ rowKey,
3421
+ selectedKeys,
3422
+ onSelectionChange: setSelectedKeys
3423
+ }
3424
+ )
3425
+ ) : /* @__PURE__ */ jsx(
3426
+ Table,
3427
+ {
3428
+ columns,
3429
+ data: rows,
3430
+ keyExtractor: rowKey,
3431
+ isLoading: query.isLoading,
3432
+ sort,
3433
+ onSortChange,
3434
+ onRowClick,
3435
+ ariaLabel
3436
+ }
3437
+ ),
3438
+ pagination && pagination.totalPages > 1 && /* @__PURE__ */ jsx("div", { className: "data_view_pagination", children: /* @__PURE__ */ jsx(
3439
+ Pagination,
3440
+ {
3441
+ page: pagination.page,
3442
+ totalPages: pagination.totalPages,
3443
+ onPageChange: pagination.onPageChange
3444
+ }
3445
+ ) })
3446
+ ] });
3447
+ };
3448
+ var Divider = ({ weight = "standard", className, ...props }) => {
3449
+ const dividerClassName = cn("divider", `divider_weight_${weight}`, className);
3450
+ return /* @__PURE__ */ jsx("hr", { className: dividerClassName, ...props });
3451
+ };
3452
+ var HeroActionButton = ({
3453
+ action,
3454
+ variant
3455
+ }) => action.href ? /* @__PURE__ */ jsx(Button, { as: "a", href: action.href, size: "lg", variant, onClick: action.onClick, children: action.label }) : /* @__PURE__ */ jsx(Button, { size: "lg", variant, onClick: action.onClick, children: action.label });
3456
+ var Hero = ({
3457
+ height = "md",
3458
+ align = "left",
3459
+ backgroundImage,
3460
+ backgroundColor,
3461
+ overlay,
3462
+ title,
3463
+ subtitle,
3464
+ eyebrow,
3465
+ textColor = "auto",
3466
+ primaryAction,
3467
+ secondaryAction,
3468
+ children,
3469
+ className,
3470
+ style,
3471
+ ...props
3472
+ }) => {
3473
+ const resolvedOverlay = overlay === true ? "dark" : overlay;
3474
+ const isDarkOverlay = resolvedOverlay === "dark";
3475
+ const resolvedTextColor = textColor === "auto" ? isDarkOverlay || backgroundImage && !resolvedOverlay ? "inverse" : "default" : textColor;
3476
+ const heroClassName = cn(
3477
+ "hero",
3478
+ `hero_height_${height}`,
3479
+ `hero_align_${align}`,
3480
+ resolvedOverlay && `hero_overlay_${resolvedOverlay}`,
3481
+ `hero_text_${resolvedTextColor}`,
3482
+ className
3483
+ );
3484
+ const inlineStyle = { ...style };
3485
+ if (backgroundImage) inlineStyle.backgroundImage = `url("${backgroundImage}")`;
3486
+ if (backgroundColor) inlineStyle.backgroundColor = backgroundColor;
3487
+ return /* @__PURE__ */ jsxs("section", { className: heroClassName, style: inlineStyle, ...props, children: [
3488
+ resolvedOverlay && /* @__PURE__ */ jsx("div", { className: "hero_overlay", "aria-hidden": "true" }),
3489
+ /* @__PURE__ */ jsxs("div", { className: "hero_content", children: [
3490
+ eyebrow && /* @__PURE__ */ jsx("div", { className: "hero_eyebrow", children: eyebrow }),
3491
+ title && /* @__PURE__ */ jsx("h1", { className: "hero_title", children: title }),
3492
+ subtitle && /* @__PURE__ */ jsx("p", { className: "hero_subtitle", children: subtitle }),
3493
+ (primaryAction || secondaryAction || children) && /* @__PURE__ */ jsxs("div", { className: "hero_actions", children: [
3494
+ primaryAction && /* @__PURE__ */ jsx(HeroActionButton, { action: primaryAction, variant: "filled" }),
3495
+ secondaryAction && /* @__PURE__ */ jsx(HeroActionButton, { action: secondaryAction, variant: "outline" }),
3496
+ children
3497
+ ] })
3498
+ ] })
3499
+ ] });
3500
+ };
3501
+ var Icon = ({ icon: IconComponent, ...props }) => {
3502
+ const hasLabel = !!props["aria-label"];
3503
+ return /* @__PURE__ */ jsx(
3504
+ IconComponent,
3505
+ {
3506
+ "aria-hidden": hasLabel ? void 0 : true,
3507
+ focusable: hasLabel ? void 0 : false,
3508
+ ...props
3509
+ }
3510
+ );
3511
+ };
3512
+ Icon.displayName = "Icon";
3513
+ var ListItem = ({
3514
+ overline,
3515
+ label,
3516
+ supportingText,
3517
+ metadata,
3518
+ leadingElement,
3519
+ trailingElement,
3520
+ alignment,
3521
+ disabled,
3522
+ selected,
3523
+ onClick,
3524
+ className,
3525
+ ...props
3526
+ }) => {
3527
+ const isOneLine = !overline && !supportingText && !metadata;
3528
+ const effectiveAlignment = alignment ?? (isOneLine ? "middle" : "top");
3529
+ const rootClassName = cn(
3530
+ "list_item",
3531
+ `list_item_align_${effectiveAlignment}`,
3532
+ disabled && "list_item_disabled",
3533
+ selected && "list_item_selected",
3534
+ onClick && "list_item_interactive",
3535
+ className
3536
+ );
3537
+ return (
3538
+ // biome-ignore lint/a11y/noStaticElementInteractions: optional interactive list item - role=button + tabIndex set conditionally based on onClick
3539
+ /* @__PURE__ */ jsx(
3540
+ "div",
3541
+ {
3542
+ className: rootClassName,
3543
+ onClick: disabled ? void 0 : onClick,
3544
+ onKeyDown: (e) => {
3545
+ if (disabled || !onClick) return;
3546
+ if (e.key === "Enter" || e.key === " ") {
3547
+ e.preventDefault();
3548
+ e.currentTarget.click();
3549
+ }
3550
+ },
3551
+ role: onClick ? "button" : void 0,
3552
+ tabIndex: onClick && !disabled ? 0 : void 0,
3553
+ "aria-disabled": disabled || void 0,
3554
+ "aria-pressed": onClick && selected !== void 0 ? selected : void 0,
3555
+ ...props,
3556
+ children: /* @__PURE__ */ jsxs("div", { className: "list_item_state_layer", children: [
3557
+ leadingElement && /* @__PURE__ */ jsx("div", { className: "list_item_leading", children: leadingElement }),
3558
+ /* @__PURE__ */ jsxs("div", { className: "list_item_content", children: [
3559
+ overline && /* @__PURE__ */ jsx("div", { className: "list_item_overline", children: overline }),
3560
+ /* @__PURE__ */ jsx("div", { className: "list_item_label", children: label }),
3561
+ supportingText && /* @__PURE__ */ jsx("div", { className: "list_item_supporting", children: supportingText }),
3562
+ metadata && /* @__PURE__ */ jsx("div", { className: "list_item_metadata", children: metadata })
3563
+ ] }),
3564
+ trailingElement && /* @__PURE__ */ jsx("div", { className: "list_item_trailing", children: trailingElement })
3565
+ ] })
3566
+ }
3567
+ )
3568
+ );
3569
+ };
3570
+ var MediaCard = ({
3571
+ image,
3572
+ imagePosition = "top",
3573
+ aspectRatio,
3574
+ heading,
3575
+ headingAs: HeadingTag = "h3",
3576
+ eyebrow,
3577
+ shadow = "sm",
3578
+ bordered = false,
3579
+ clickable = false,
3580
+ meta,
3581
+ children,
3582
+ className,
3583
+ ...props
3584
+ }) => {
3585
+ const cardClassName = cn(
3586
+ "media_card",
3587
+ `media_card_image_${imagePosition}`,
3588
+ `media_card_shadow_${shadow}`,
3589
+ bordered && "media_card_bordered",
3590
+ clickable && "media_card_clickable",
3591
+ className
3592
+ );
3593
+ const isOverlay = imagePosition === "overlay";
3594
+ const cardStyle = isOverlay && aspectRatio ? { aspectRatio } : void 0;
3595
+ const wrapStyle = !isOverlay && aspectRatio ? { aspectRatio } : void 0;
3596
+ return /* @__PURE__ */ jsxs("div", { className: cardClassName, style: cardStyle, ...props, children: [
3597
+ /* @__PURE__ */ jsxs("div", { className: "media_card_image_wrap", style: wrapStyle, children: [
3598
+ /* @__PURE__ */ jsx("img", { className: "media_card_image", src: image.src, alt: image.alt, loading: "lazy" }),
3599
+ isOverlay && /* @__PURE__ */ jsx("div", { className: "media_card_overlay", "aria-hidden": "true" })
3600
+ ] }),
3601
+ /* @__PURE__ */ jsxs("div", { className: "media_card_body", children: [
3602
+ eyebrow && /* @__PURE__ */ jsx("div", { className: "media_card_eyebrow", children: eyebrow }),
3603
+ heading && /* @__PURE__ */ jsx(HeadingTag, { className: "media_card_heading", children: heading }),
3604
+ children && /* @__PURE__ */ jsx("div", { className: "media_card_content", children }),
3605
+ meta && /* @__PURE__ */ jsx("div", { className: "media_card_meta", children: meta })
3606
+ ] })
2707
3607
  ] });
2708
3608
  };
3609
+ var Prose = ({ size = "md", className, children, ref, ...props }) => {
3610
+ const rootRef = React11.useRef(null);
3611
+ React11.useImperativeHandle(ref, () => rootRef.current, []);
3612
+ useSafeLayoutEffect(() => {
3613
+ const root = rootRef.current;
3614
+ if (!root) return;
3615
+ const targets = Array.from(root.querySelectorAll("pre, table"));
3616
+ const sync = () => {
3617
+ for (const el of targets) {
3618
+ if (el.scrollWidth > el.clientWidth) el.setAttribute("tabindex", "0");
3619
+ else el.removeAttribute("tabindex");
3620
+ }
3621
+ };
3622
+ sync();
3623
+ if (typeof ResizeObserver === "undefined") return;
3624
+ const observer = new ResizeObserver(sync);
3625
+ observer.observe(root);
3626
+ for (const el of targets) observer.observe(el);
3627
+ return () => observer.disconnect();
3628
+ }, [children]);
3629
+ return /* @__PURE__ */ jsx("div", { ref: rootRef, className: cn("prose", `prose_size_${size}`, className), ...props, children });
3630
+ };
3631
+ Prose.displayName = "Prose";
2709
3632
  var ICONS = {
2710
3633
  info: /* @__PURE__ */ jsx(Info, { size: iconSize.lg, "aria-hidden": "true" }),
2711
3634
  success: /* @__PURE__ */ jsx(CheckCircle2, { size: iconSize.lg, "aria-hidden": "true" }),
@@ -2756,8 +3679,8 @@ var AlertModal = ({
2756
3679
  variant = "info",
2757
3680
  title,
2758
3681
  message,
2759
- confirmText = "\uD655\uC778",
2760
- cancelText = "\uCDE8\uC18C",
3682
+ confirmText: confirmTextProp,
3683
+ cancelText: cancelTextProp,
2761
3684
  showCancel = false,
2762
3685
  destructive = false,
2763
3686
  actionsAlign = "right",
@@ -2767,6 +3690,9 @@ var AlertModal = ({
2767
3690
  onCancel,
2768
3691
  onClose
2769
3692
  }) => {
3693
+ const t = useLocaleText();
3694
+ const confirmText = confirmTextProp ?? t("alert.confirm");
3695
+ const cancelText = cancelTextProp ?? t("alert.cancel");
2770
3696
  const dismiss = onCancel ?? onClose;
2771
3697
  const panelRef = React11.useRef(null);
2772
3698
  const titleId = React11.useId();
@@ -2886,7 +3812,9 @@ var LinearProgress = ({
2886
3812
  }
2887
3813
  );
2888
3814
  };
2889
- var Spinner = ({ size = 24, ariaLabel = "\uB85C\uB529 \uC911" }) => {
3815
+ var Spinner = ({ size = 24, ariaLabel: ariaLabelProp }) => {
3816
+ const t = useLocaleText();
3817
+ const ariaLabel = ariaLabelProp ?? t("spinner.label");
2890
3818
  return /* @__PURE__ */ jsx(
2891
3819
  "span",
2892
3820
  {
@@ -2964,9 +3892,12 @@ var ToastItemComponent = ({ item, onRemove, closeAriaLabel }) => {
2964
3892
  var ToastProvider = ({
2965
3893
  children,
2966
3894
  maxCount = 5,
2967
- closeAriaLabel = "\uB2EB\uAE30",
2968
- regionLabel = "\uC54C\uB9BC"
3895
+ closeAriaLabel: closeAriaLabelProp,
3896
+ regionLabel: regionLabelProp
2969
3897
  }) => {
3898
+ const t = useLocaleText();
3899
+ const closeAriaLabel = closeAriaLabelProp ?? t("toast.close");
3900
+ const regionLabel = regionLabelProp ?? t("toast.region");
2970
3901
  const [toasts, setToasts] = React11.useState([]);
2971
3902
  const isMounted = useIsMounted();
2972
3903
  const addToast = React11.useCallback(
@@ -2977,7 +3908,7 @@ var ToastProvider = ({
2977
3908
  [maxCount]
2978
3909
  );
2979
3910
  const removeToast = React11.useCallback((id) => {
2980
- setToasts((prev) => prev.filter((t) => t.id !== id));
3911
+ setToasts((prev) => prev.filter((t2) => t2.id !== id));
2981
3912
  }, []);
2982
3913
  const contextValue = React11.useMemo(() => ({ addToast }), [addToast]);
2983
3914
  return /* @__PURE__ */ jsxs(ToastContext.Provider, { value: contextValue, children: [
@@ -3032,8 +3963,10 @@ var TopLoading = ({
3032
3963
  color,
3033
3964
  height = 3,
3034
3965
  isLoading = true,
3035
- ariaLabel = "\uD398\uC774\uC9C0 \uB85C\uB529 \uC911"
3966
+ ariaLabel: ariaLabelProp
3036
3967
  }) => {
3968
+ const t = useLocaleText();
3969
+ const ariaLabel = ariaLabelProp ?? t("topLoading.label");
3037
3970
  if (!isLoading) return null;
3038
3971
  const isIndeterminate = progress === void 0;
3039
3972
  return /* @__PURE__ */ jsx(
@@ -3059,26 +3992,212 @@ var TopLoading = ({
3059
3992
  }
3060
3993
  );
3061
3994
  };
3995
+ var Combobox = ({
3996
+ value = null,
3997
+ onValueChange,
3998
+ onSearch,
3999
+ defaultOptions = [],
4000
+ debounceMs = 250,
4001
+ placeholder: placeholderProp,
4002
+ emptyMessage: emptyMessageProp,
4003
+ idleMessage: idleMessageProp,
4004
+ size = "md",
4005
+ disabled = false,
4006
+ fullWidth = false,
4007
+ renderOption,
4008
+ ariaLabel,
4009
+ loadingLabel: loadingLabelProp,
4010
+ className,
4011
+ ...props
4012
+ }) => {
4013
+ const t = useLocaleText();
4014
+ const placeholder = placeholderProp ?? t("combobox.placeholder");
4015
+ const emptyMessage = emptyMessageProp ?? t("combobox.empty");
4016
+ const idleMessage = idleMessageProp ?? t("combobox.idle");
4017
+ const loadingLabel = loadingLabelProp ?? t("combobox.loading");
4018
+ const generatedId = useId();
4019
+ const field = useFieldControl();
4020
+ const inputId = field?.inputId ?? generatedId;
4021
+ const listId = `${inputId}-listbox`;
4022
+ const [query, setQuery] = useState("");
4023
+ const [options, setOptions] = useState(defaultOptions);
4024
+ const [isLoading, setIsLoading] = useState(false);
4025
+ const [hasSearched, setHasSearched] = useState(false);
4026
+ const requestSeq = useRef(0);
4027
+ const defaultOptionsRef = useRef(defaultOptions);
4028
+ useEffect(() => {
4029
+ defaultOptionsRef.current = defaultOptions;
4030
+ }, [defaultOptions]);
4031
+ const closeRef = useRef(() => {
4032
+ });
4033
+ const commit = useCallback(
4034
+ (option) => {
4035
+ onValueChange?.(option);
4036
+ setQuery("");
4037
+ closeRef.current();
4038
+ },
4039
+ [onValueChange]
4040
+ );
4041
+ const popup = useListboxPopup({
4042
+ items: options,
4043
+ onCommit: commit,
4044
+ disabled,
4045
+ // 상시 컨트롤이 입력창이라 포커스를 되돌릴 필요가 없다. triggerRef 는 장식용
4046
+ // chevron 버튼(tabIndex=-1)에 붙어 있어, 켜면 Escape 가 포커스를 그 숨은 버튼으로 던진다.
4047
+ returnFocusOnClose: false
4048
+ });
4049
+ const { isOpen, setIsOpen, close, activeIndex, setActiveIndex } = popup;
4050
+ closeRef.current = close;
4051
+ useEffect(() => {
4052
+ if (!isOpen) return;
4053
+ if (query === "") {
4054
+ requestSeq.current++;
4055
+ setOptions(defaultOptionsRef.current);
4056
+ setHasSearched(false);
4057
+ setIsLoading(false);
4058
+ return;
4059
+ }
4060
+ const seq = ++requestSeq.current;
4061
+ setIsLoading(true);
4062
+ const timer = setTimeout(() => {
4063
+ onSearch(query).then((result) => {
4064
+ if (seq !== requestSeq.current) return;
4065
+ setOptions(result);
4066
+ setHasSearched(true);
4067
+ }).catch(() => {
4068
+ if (seq !== requestSeq.current) return;
4069
+ setOptions([]);
4070
+ setHasSearched(true);
4071
+ }).finally(() => {
4072
+ if (seq !== requestSeq.current) return;
4073
+ setIsLoading(false);
4074
+ });
4075
+ }, debounceMs);
4076
+ return () => clearTimeout(timer);
4077
+ }, [query, isOpen, debounceMs, onSearch]);
4078
+ const rootClassName = cn(
4079
+ "combobox",
4080
+ `combobox_size_${size}`,
4081
+ { combobox_full_width: fullWidth, combobox_disabled: disabled },
4082
+ className
4083
+ );
4084
+ const showIdle = !isLoading && !hasSearched && options.length === 0;
4085
+ const showEmpty = !isLoading && hasSearched && options.length === 0;
4086
+ const hasList = !showIdle && !showEmpty;
4087
+ const panelStyle = useSpringPresence({
4088
+ visible: isOpen,
4089
+ from: popup.dropUp ? "translateY(4px)" : "translateY(-4px)"
4090
+ });
4091
+ return /* @__PURE__ */ jsxs("div", { ref: popup.wrapperRef, className: rootClassName, ...props, children: [
4092
+ /* @__PURE__ */ jsxs("div", { className: "combobox_control", children: [
4093
+ /* @__PURE__ */ jsx(
4094
+ "input",
4095
+ {
4096
+ id: inputId,
4097
+ className: "combobox_input",
4098
+ role: "combobox",
4099
+ type: "text",
4100
+ autoComplete: "off",
4101
+ disabled,
4102
+ value: isOpen ? query : value?.label ?? "",
4103
+ placeholder: value ? value.label : placeholder,
4104
+ "aria-expanded": isOpen,
4105
+ "aria-controls": isOpen && hasList ? listId : void 0,
4106
+ "aria-autocomplete": "list",
4107
+ "aria-activedescendant": isOpen && activeIndex >= 0 && options[activeIndex] ? `${listId}-${options[activeIndex].value}` : void 0,
4108
+ "aria-labelledby": field?.labelId,
4109
+ "aria-label": field?.labelId ? void 0 : ariaLabel,
4110
+ "aria-describedby": field?.describedBy,
4111
+ "aria-invalid": field?.invalid || void 0,
4112
+ "aria-required": field?.required || void 0,
4113
+ onChange: (event) => {
4114
+ setQuery(event.target.value);
4115
+ if (!isOpen) setIsOpen(true);
4116
+ },
4117
+ onFocus: () => !disabled && setIsOpen(true),
4118
+ onKeyDown: popup.onInputKeyDown
4119
+ }
4120
+ ),
4121
+ isLoading && /* @__PURE__ */ jsx("span", { className: "combobox_spinner", children: /* @__PURE__ */ jsx(Spinner, { size: iconSize.sm, ariaLabel: loadingLabel }) }),
4122
+ /* @__PURE__ */ jsx(
4123
+ "button",
4124
+ {
4125
+ type: "button",
4126
+ ref: popup.triggerRef,
4127
+ className: "combobox_toggle",
4128
+ tabIndex: -1,
4129
+ disabled,
4130
+ "aria-hidden": "true",
4131
+ onClick: () => isOpen ? close() : setIsOpen(true),
4132
+ children: /* @__PURE__ */ jsx(ChevronDown, { size: iconSize.lg })
4133
+ }
4134
+ )
4135
+ ] }),
4136
+ isOpen && /* @__PURE__ */ jsx(
4137
+ animated.div,
4138
+ {
4139
+ className: cn("combobox_panel", { combobox_panel_up: popup.dropUp }),
4140
+ style: panelStyle,
4141
+ children: !hasList ? /* @__PURE__ */ jsx("p", { className: "combobox_message", role: "status", children: showIdle ? idleMessage : emptyMessage }) : /* @__PURE__ */ jsx(
4142
+ "div",
4143
+ {
4144
+ ref: popup.listRef,
4145
+ id: listId,
4146
+ className: "combobox_list",
4147
+ role: "listbox",
4148
+ children: options.map((option, index) => (
4149
+ /* biome-ignore lint/a11y/useKeyWithClickEvents: 키보드는 입력의 onKeyDown 이 담당한다 - option 은 aria-activedescendant 로 가리키는 비포커스 요소다 (APG Combobox) */
4150
+ /* @__PURE__ */ jsx(
4151
+ "div",
4152
+ {
4153
+ id: `${listId}-${option.value}`,
4154
+ role: "option",
4155
+ tabIndex: -1,
4156
+ "aria-selected": value?.value === option.value,
4157
+ "aria-disabled": option.disabled || void 0,
4158
+ className: cn("combobox_option", {
4159
+ is_active: index === activeIndex,
4160
+ is_disabled: option.disabled
4161
+ }),
4162
+ onMouseEnter: () => !option.disabled && setActiveIndex(index),
4163
+ onClick: () => !option.disabled && commit(option),
4164
+ children: renderOption ? renderOption(option) : option.label
4165
+ },
4166
+ option.value
4167
+ )
4168
+ ))
4169
+ }
4170
+ )
4171
+ }
4172
+ )
4173
+ ] });
4174
+ };
3062
4175
  var normalizeForSearch = (s) => s.toLowerCase().replace(/\s+/g, "");
3063
4176
  var Dropdown = (props) => {
4177
+ const t = useLocaleText();
3064
4178
  const {
3065
4179
  id,
3066
4180
  label,
3067
- placeholder = "\uC120\uD0DD\u2026",
4181
+ placeholder: placeholderProp,
3068
4182
  options,
3069
4183
  disabled,
3070
4184
  size = "md",
3071
4185
  variant = "outline",
3072
4186
  className,
3073
4187
  searchable = false,
3074
- searchPlaceholder = "\uAC80\uC0C9\u2026",
3075
- emptyText = "\uACB0\uACFC \uC5C6\uC74C",
3076
- selectedSummary = (count) => `${count}\uAC1C \uC120\uD0DD`,
4188
+ searchPlaceholder: searchPlaceholderProp,
4189
+ emptyText: emptyTextProp,
4190
+ selectedSummary: selectedSummaryProp,
3077
4191
  name
3078
4192
  } = props;
4193
+ const placeholder = placeholderProp ?? t("dropdown.placeholder");
4194
+ const searchPlaceholder = searchPlaceholderProp ?? t("dropdown.searchPlaceholder");
4195
+ const emptyText = emptyTextProp ?? t("dropdown.empty");
4196
+ const selectedSummary = selectedSummaryProp ?? ((count) => t("dropdown.selectedSummary", { count }));
3079
4197
  const multiple = props.multiple === true;
3080
4198
  const internalId = useId();
3081
- const dropdownId = id ?? internalId;
4199
+ const field = useFieldControl();
4200
+ const dropdownId = id ?? field?.inputId ?? internalId;
3082
4201
  const isControlled = props.value !== void 0;
3083
4202
  const [internalSingle, setInternalSingle] = useState(
3084
4203
  () => props.multiple === true ? null : props.defaultValue ?? null
@@ -3086,14 +4205,9 @@ var Dropdown = (props) => {
3086
4205
  const [internalMulti, setInternalMulti] = useState(
3087
4206
  () => props.multiple === true ? props.defaultValue ?? [] : []
3088
4207
  );
3089
- const [isOpen, setIsOpen] = useState(false);
3090
- const [activeIndex, setActiveIndex] = useState(-1);
3091
- const [dropUp, setDropUp] = useState(false);
3092
4208
  const [searchText, setSearchText] = useState("");
3093
4209
  const [committedQuery, setCommittedQuery] = useState("");
3094
4210
  const isComposingRef = useRef(false);
3095
- const wrapperRef = useRef(null);
3096
- const controlRef = useRef(null);
3097
4211
  const searchRef = useRef(null);
3098
4212
  const selectedValues = useMemo(() => {
3099
4213
  if (multiple) {
@@ -3131,129 +4245,41 @@ var Dropdown = (props) => {
3131
4245
  },
3132
4246
  [selectedValues, options, isControlled, props.multiple, props.onValueChange, props.onChange]
3133
4247
  );
3134
- const closePanel = useCallback(() => {
3135
- setIsOpen(false);
3136
- if (searchable) controlRef.current?.focus();
3137
- }, [searchable]);
3138
4248
  const selectOption = useCallback(
3139
- (opt) => {
3140
- if (opt.disabled) return;
3141
- if (multiple) {
3142
- toggleMultiple(opt);
3143
- } else {
3144
- selectSingle(opt.value);
3145
- closePanel();
3146
- }
3147
- },
3148
- [multiple, toggleMultiple, selectSingle, closePanel]
3149
- );
3150
- useEffect(() => {
3151
- const handleOutsideClick = (e) => {
3152
- if (!wrapperRef.current?.contains(e.target)) {
3153
- setIsOpen(false);
3154
- }
3155
- };
3156
- document.addEventListener("mousedown", handleOutsideClick);
3157
- return () => document.removeEventListener("mousedown", handleOutsideClick);
3158
- }, []);
3159
- const moveActive = useCallback(
3160
- (dir) => {
3161
- if (visibleOptions.length === 0) return;
3162
- if (!isOpen) {
3163
- setIsOpen(true);
3164
- return;
3165
- }
3166
- let i = activeIndex;
3167
- if (i === -1) {
3168
- i = dir === 1 ? -1 : 0;
3169
- }
3170
- const len = visibleOptions.length;
3171
- for (let step = 0; step < len; step++) {
3172
- i = (i + dir + len) % len;
3173
- if (!visibleOptions[i].disabled) {
3174
- setActiveIndex(i);
3175
- break;
3176
- }
3177
- }
3178
- },
3179
- [visibleOptions, isOpen, activeIndex]
3180
- );
3181
- const commitActive = useCallback(() => {
3182
- if (activeIndex < 0 || activeIndex >= visibleOptions.length) return;
3183
- selectOption(visibleOptions[activeIndex]);
3184
- }, [activeIndex, visibleOptions, selectOption]);
3185
- const onControlKeyDown = (e) => {
3186
- if (disabled) return;
3187
- switch (e.key) {
3188
- case " ":
3189
- case "Enter":
3190
- e.preventDefault();
3191
- if (!isOpen) setIsOpen(true);
3192
- else commitActive();
3193
- break;
3194
- case "ArrowDown":
3195
- e.preventDefault();
3196
- moveActive(1);
3197
- break;
3198
- case "ArrowUp":
3199
- e.preventDefault();
3200
- moveActive(-1);
3201
- break;
3202
- case "Home":
3203
- e.preventDefault();
3204
- setIsOpen(true);
3205
- setActiveIndex(visibleOptions.findIndex((o) => !o.disabled));
3206
- break;
3207
- case "End":
3208
- e.preventDefault();
3209
- setIsOpen(true);
3210
- for (let i = visibleOptions.length - 1; i >= 0; i--) {
3211
- if (!visibleOptions[i].disabled) {
3212
- setActiveIndex(i);
3213
- break;
3214
- }
3215
- }
3216
- break;
3217
- case "Escape":
3218
- e.preventDefault();
3219
- setIsOpen(false);
3220
- break;
3221
- case "Tab":
3222
- setIsOpen(false);
3223
- break;
3224
- }
3225
- };
3226
- const onSearchKeyDown = (e) => {
3227
- if (e.nativeEvent.isComposing) return;
3228
- switch (e.key) {
3229
- case "ArrowDown":
3230
- e.preventDefault();
3231
- moveActive(1);
3232
- break;
3233
- case "ArrowUp":
3234
- e.preventDefault();
3235
- moveActive(-1);
3236
- break;
3237
- case "Enter":
3238
- e.preventDefault();
3239
- commitActive();
3240
- break;
3241
- case "Escape":
3242
- e.preventDefault();
3243
- closePanel();
3244
- break;
3245
- case "Tab":
4249
+ (opt) => {
4250
+ if (opt.disabled) return;
4251
+ if (multiple) {
4252
+ toggleMultiple(opt);
4253
+ } else {
4254
+ selectSingle(opt.value);
3246
4255
  closePanel();
3247
- break;
3248
- }
3249
- };
3250
- useEffect(() => {
3251
- if (!isOpen) return;
3252
- const selectedIdx = visibleOptions.findIndex(
3253
- (o) => selectedValues.includes(o.value) && !o.disabled
3254
- );
3255
- setActiveIndex(selectedIdx >= 0 ? selectedIdx : visibleOptions.findIndex((o) => !o.disabled));
3256
- }, [isOpen, visibleOptions]);
4256
+ }
4257
+ },
4258
+ // closePanel 은 아래 훅에서 오므로 선언 순서상 참조만 한다 (렌더마다 동일 참조).
4259
+ // biome-ignore lint/correctness/useExhaustiveDependencies: closePanel 은 훅 결과라 아래에서 정의된다
4260
+ [multiple, toggleMultiple, selectSingle]
4261
+ );
4262
+ const {
4263
+ isOpen,
4264
+ setIsOpen,
4265
+ dropUp,
4266
+ activeIndex,
4267
+ setActiveIndex,
4268
+ wrapperRef,
4269
+ triggerRef: controlRef,
4270
+ listRef,
4271
+ close: closePanel,
4272
+ onTriggerKeyDown: onControlKeyDown,
4273
+ onInputKeyDown: onSearchKeyDown
4274
+ } = useListboxPopup({
4275
+ items: visibleOptions,
4276
+ onCommit: selectOption,
4277
+ disabled,
4278
+ // searchable 은 포커스가 검색 입력에 있으므로 닫을 때 트리거로 되돌린다.
4279
+ returnFocusOnClose: searchable,
4280
+ // 열릴 때는 선택된 항목을 활성으로. 없으면 훅이 첫 활성 항목을 고른다.
4281
+ initialActiveIndex: (opts) => opts.findIndex((o) => selectedValues.includes(o.value) && !o.disabled)
4282
+ });
3257
4283
  useEffect(() => {
3258
4284
  if (!isOpen) {
3259
4285
  setSearchText("");
@@ -3266,14 +4292,6 @@ var Dropdown = (props) => {
3266
4292
  searchRef.current?.focus();
3267
4293
  }
3268
4294
  }, [isOpen, searchable]);
3269
- useEffect(() => {
3270
- if (!isOpen || !controlRef.current) return;
3271
- const rect = controlRef.current.getBoundingClientRect();
3272
- const spaceBelow = window.innerHeight - rect.bottom;
3273
- const spaceAbove = rect.top;
3274
- const MIN_BELOW = 120;
3275
- setDropUp(spaceBelow < MIN_BELOW && spaceAbove > spaceBelow);
3276
- }, [isOpen]);
3277
4295
  const currentOption = useMemo(
3278
4296
  () => multiple ? null : options.find((o) => o.value === selectedValues[0]) ?? null,
3279
4297
  [multiple, options, selectedValues]
@@ -3304,6 +4322,8 @@ var Dropdown = (props) => {
3304
4322
  className: cn("dropdown_control", { is_disabled: disabled }),
3305
4323
  "aria-haspopup": "listbox",
3306
4324
  "aria-expanded": isOpen,
4325
+ "aria-describedby": field?.describedBy,
4326
+ "aria-invalid": field?.invalid || void 0,
3307
4327
  "aria-controls": isOpen ? `${dropdownId}_listbox` : void 0,
3308
4328
  onClick: () => !disabled && setIsOpen((o) => !o),
3309
4329
  onKeyDown: onControlKeyDown,
@@ -3354,6 +4374,7 @@ var Dropdown = (props) => {
3354
4374
  /* @__PURE__ */ jsx(
3355
4375
  "div",
3356
4376
  {
4377
+ ref: listRef,
3357
4378
  id: `${dropdownId}_listbox`,
3358
4379
  role: "listbox",
3359
4380
  className: "dropdown_options",
@@ -3399,7 +4420,7 @@ var Dropdown = (props) => {
3399
4420
  var pad = (n) => String(n).padStart(2, "0");
3400
4421
  var getDaysInMonth = (year, month) => new Date(year, month, 0).getDate();
3401
4422
  var normalizeWidth = (v) => typeof v === "number" ? `${v}px` : v;
3402
- var range = (start, end) => Array.from({ length: end - start + 1 }, (_, i) => start + i);
4423
+ var range2 = (start, end) => Array.from({ length: end - start + 1 }, (_, i) => start + i);
3403
4424
  var DatePicker = ({
3404
4425
  label,
3405
4426
  value,
@@ -3413,12 +4434,19 @@ var DatePicker = ({
3413
4434
  disabled,
3414
4435
  fullWidth = true,
3415
4436
  width,
3416
- yearLabel = "\uB144",
3417
- monthLabel = "\uC6D4",
3418
- dayLabel = "\uC77C",
3419
- minDateSrFormat = "\uCD5C\uC18C \uB0A0\uC9DC: {date}",
3420
- selectableRangeUntilTodaySrText = "\uC624\uB298\uAE4C\uC9C0 \uC120\uD0DD \uAC00\uB2A5"
4437
+ yearLabel: yearLabelProp,
4438
+ monthLabel: monthLabelProp,
4439
+ dayLabel: dayLabelProp,
4440
+ minDateSrFormat: minDateSrFormatProp,
4441
+ selectableRangeUntilTodaySrText: selectableRangeUntilTodaySrTextProp
3421
4442
  }) => {
4443
+ const t = useLocaleText();
4444
+ const yearLabel = yearLabelProp ?? t("datePicker.year");
4445
+ const monthLabel = monthLabelProp ?? t("datePicker.month");
4446
+ const dayLabel = dayLabelProp ?? t("datePicker.day");
4447
+ const minDateSrFormat = minDateSrFormatProp ?? t("datePicker.minDateSr");
4448
+ const selectableRangeUntilTodaySrText = selectableRangeUntilTodaySrTextProp ?? t("datePicker.rangeUntilTodaySr");
4449
+ const field = useFieldControl();
3422
4450
  const groupId = React11.useId();
3423
4451
  const constraintId = React11.useId();
3424
4452
  const { todayYear, todayMonth, todayDay } = React11.useMemo(() => {
@@ -3467,22 +4495,23 @@ var DatePicker = ({
3467
4495
  }
3468
4496
  return daysInMonth;
3469
4497
  }, [year, month, selectableRange, todayYear, todayMonth, todayDay]);
4498
+ const minYear = min.year > 0 ? Math.max(startYear, min.year) : startYear;
3470
4499
  const yearOptions = React11.useMemo(
3471
- () => range(startYear, maxYear).map((y) => ({
4500
+ () => range2(minYear, Math.max(minYear, maxYear)).map((y) => ({
3472
4501
  value: String(y),
3473
4502
  label: String(y)
3474
4503
  })),
3475
- [startYear, maxYear]
4504
+ [minYear, maxYear]
3476
4505
  );
3477
4506
  const monthOptions = React11.useMemo(
3478
- () => range(minMonth, Math.max(minMonth, maxMonth)).map((m) => ({
4507
+ () => range2(minMonth, Math.max(minMonth, maxMonth)).map((m) => ({
3479
4508
  value: String(m),
3480
4509
  label: pad(m)
3481
4510
  })),
3482
4511
  [minMonth, maxMonth]
3483
4512
  );
3484
4513
  const dayOptions = React11.useMemo(
3485
- () => range(minDay, Math.max(minDay, maxDay)).map((d) => ({
4514
+ () => range2(minDay, Math.max(minDay, maxDay)).map((d) => ({
3486
4515
  value: String(d),
3487
4516
  label: pad(d)
3488
4517
  })),
@@ -3548,8 +4577,9 @@ var DatePicker = ({
3548
4577
  {
3549
4578
  className: "date_picker_fields",
3550
4579
  role: "group",
3551
- "aria-labelledby": label ? groupId : void 0,
3552
- "aria-describedby": constraintDesc ? constraintId : void 0,
4580
+ "aria-labelledby": field?.labelId ?? (label ? groupId : void 0),
4581
+ "aria-describedby": [field?.describedBy, constraintDesc ? constraintId : void 0].filter(Boolean).join(" ") || void 0,
4582
+ "aria-invalid": field?.invalid || void 0,
3553
4583
  children: [
3554
4584
  /* @__PURE__ */ jsx(
3555
4585
  Dropdown,
@@ -3595,8 +4625,83 @@ var DatePicker = ({
3595
4625
  )
3596
4626
  ] });
3597
4627
  };
4628
+ var DateRangePicker = ({
4629
+ value,
4630
+ onValueChange,
4631
+ startLabel: startLabelProp,
4632
+ endLabel: endLabelProp,
4633
+ startYear,
4634
+ endYear,
4635
+ minDate,
4636
+ selectableRange = "all",
4637
+ disabled,
4638
+ fullWidth = true
4639
+ }) => {
4640
+ const t = useLocaleText();
4641
+ const startLabel = startLabelProp ?? t("dateRange.start");
4642
+ const endLabel = endLabelProp ?? t("dateRange.end");
4643
+ const field = useFieldControl();
4644
+ const start = value?.start;
4645
+ const end = value?.end;
4646
+ const handleStartChange = (next) => {
4647
+ onValueChange({ start: next, end: end && end < next ? void 0 : end });
4648
+ };
4649
+ const handleEndChange = (next) => {
4650
+ onValueChange({ start, end: next });
4651
+ };
4652
+ const endMinDate = start ?? minDate;
4653
+ return /* @__PURE__ */ jsx(
4654
+ "div",
4655
+ {
4656
+ className: cn("date_range_picker", {
4657
+ date_range_picker_full_width: fullWidth,
4658
+ date_range_picker_disabled: disabled
4659
+ }),
4660
+ children: /* @__PURE__ */ jsxs(
4661
+ "div",
4662
+ {
4663
+ className: "date_range_picker_fields",
4664
+ role: "group",
4665
+ "aria-labelledby": field?.labelId,
4666
+ "aria-describedby": field?.describedBy,
4667
+ "aria-invalid": field?.invalid || void 0,
4668
+ children: [
4669
+ /* @__PURE__ */ jsx(
4670
+ DatePicker,
4671
+ {
4672
+ label: startLabel,
4673
+ value: start,
4674
+ onValueChange: handleStartChange,
4675
+ startYear,
4676
+ endYear,
4677
+ minDate,
4678
+ selectableRange,
4679
+ disabled,
4680
+ fullWidth: true
4681
+ }
4682
+ ),
4683
+ /* @__PURE__ */ jsx(
4684
+ DatePicker,
4685
+ {
4686
+ label: endLabel,
4687
+ value: end,
4688
+ onValueChange: handleEndChange,
4689
+ startYear,
4690
+ endYear,
4691
+ minDate: endMinDate,
4692
+ selectableRange,
4693
+ disabled: disabled || !start,
4694
+ fullWidth: true
4695
+ }
4696
+ )
4697
+ ]
4698
+ }
4699
+ )
4700
+ }
4701
+ );
4702
+ };
3598
4703
  var FileInput = ({
3599
- label = "\uD30C\uC77C \uC120\uD0DD",
4704
+ label: labelProp,
3600
4705
  onFiles,
3601
4706
  supportingText,
3602
4707
  preview = false,
@@ -3608,8 +4713,12 @@ var FileInput = ({
3608
4713
  onChange,
3609
4714
  ...props
3610
4715
  }) => {
3611
- const inputId = React11.useId();
4716
+ const t = useLocaleText();
4717
+ const label = labelProp ?? t("fileInput.label");
4718
+ const generatedInputId = React11.useId();
3612
4719
  const helperId = React11.useId();
4720
+ const field = useFieldControl();
4721
+ const inputId = field?.inputId ?? generatedInputId;
3613
4722
  const inputRef = React11.useRef(null);
3614
4723
  const [previewUrls, setPreviewUrls] = React11.useState([]);
3615
4724
  const previewUrlsRef = React11.useRef([]);
@@ -3679,7 +4788,8 @@ var FileInput = ({
3679
4788
  className: "file_input_control",
3680
4789
  disabled,
3681
4790
  accept: isPreviewVariant ? accept ?? "image/*" : accept,
3682
- "aria-describedby": supportingText ? helperId : void 0,
4791
+ "aria-describedby": field?.describedBy ?? (supportingText ? helperId : void 0),
4792
+ "aria-invalid": field?.invalid || void 0,
3683
4793
  onChange: handleChange
3684
4794
  }
3685
4795
  ),
@@ -3704,7 +4814,7 @@ var FileInput = ({
3704
4814
  type: "button",
3705
4815
  className: "file_input_preview_remove",
3706
4816
  onClick: handleRemove,
3707
- "aria-label": "\uC774\uBBF8\uC9C0 \uC81C\uAC70",
4817
+ "aria-label": t("fileInput.removeImage"),
3708
4818
  children: /* @__PURE__ */ jsx(X, { size: iconSize.xs, "aria-hidden": "true" })
3709
4819
  }
3710
4820
  ),
@@ -3767,20 +4877,28 @@ function ImageCropper({
3767
4877
  onReady,
3768
4878
  onError,
3769
4879
  className,
3770
- label = "\uC774\uBBF8\uC9C0 \uC704\uCE58\uC640 \uBC30\uC728 \uC870\uC815",
3771
- hint = "\uB4DC\uB798\uADF8(\uB610\uB294 \uBC29\uD5A5\uD0A4)\uB85C \uC704\uCE58, \uD720\xB7\uC2AC\uB77C\uC774\uB354\uB85C \uBC30\uC728\uC744 \uB9DE\uCD94\uC138\uC694.",
3772
- zoomOutLabel = "\uCD95\uC18C",
3773
- zoomLabel = "\uBC30\uC728",
3774
- zoomInLabel = "\uD655\uB300",
3775
- noPanHint = "\uC774\uBBF8\uC9C0\uAC00 \uBDF0\uD3EC\uD2B8\uB97C \uB531 \uCC44\uC6CC \uC774\uB3D9 \uC5EC\uC720\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
4880
+ label: labelProp,
4881
+ hint: hintProp,
4882
+ zoomOutLabel: zoomOutLabelProp,
4883
+ zoomLabel: zoomLabelProp,
4884
+ zoomInLabel: zoomInLabelProp,
4885
+ noPanHint: noPanHintProp,
3776
4886
  ...rest
3777
4887
  }) {
4888
+ const t = useLocaleText();
4889
+ const hint = hintProp ?? t("imageCropper.hint");
4890
+ const noPanHint = noPanHintProp ?? t("imageCropper.noPanHint");
4891
+ const label = labelProp ?? t("imageCropper.label");
4892
+ const zoomOutLabel = zoomOutLabelProp ?? t("imageCropper.zoomOut");
4893
+ const zoomLabel = zoomLabelProp ?? t("imageCropper.zoom");
4894
+ const zoomInLabel = zoomInLabelProp ?? t("imageCropper.zoomIn");
3778
4895
  const imageRef = useRef(null);
3779
4896
  const viewportRef = useRef(null);
3780
4897
  const dragRef = useRef(
3781
4898
  null
3782
4899
  );
3783
4900
  const hintId = useId();
4901
+ const field = useFieldControl();
3784
4902
  const [previewUrl, setPreviewUrl] = useState("");
3785
4903
  const [srcType, setSrcType] = useState("");
3786
4904
  useEffect(() => {
@@ -3955,8 +5073,9 @@ function ImageCropper({
3955
5073
  className: cn("image_cropper_viewport", dragging && "image_cropper_viewport_dragging"),
3956
5074
  style: viewportStyle,
3957
5075
  role: "group",
3958
- "aria-label": label,
3959
- "aria-describedby": hintId,
5076
+ "aria-labelledby": field?.labelId,
5077
+ "aria-label": field?.labelId ? void 0 : label,
5078
+ "aria-describedby": [field?.describedBy, hintId].filter(Boolean).join(" "),
3960
5079
  tabIndex: imageSize ? 0 : -1,
3961
5080
  onPointerDown: handlePointerDown,
3962
5081
  onPointerMove: handlePointerMove,
@@ -4044,9 +5163,11 @@ var OtpInput = ({
4044
5163
  disabled = false,
4045
5164
  supportingText,
4046
5165
  autoFocus = false,
4047
- ariaLabel = "OTP \uC785\uB825",
5166
+ ariaLabel: ariaLabelProp,
4048
5167
  className
4049
5168
  }) => {
5169
+ const t = useLocaleText();
5170
+ const ariaLabel = ariaLabelProp ?? t("otpInput.label");
4050
5171
  const inputsRef = React11.useRef([]);
4051
5172
  const isTypingRef = React11.useRef(false);
4052
5173
  React11.useEffect(() => {
@@ -4130,46 +5251,56 @@ var OtpInput = ({
4130
5251
  };
4131
5252
  const rootClassName = cn("otp_input", className);
4132
5253
  const supportingId = React11.useId();
5254
+ const field = useFieldControl();
4133
5255
  return (
4134
5256
  // biome-ignore lint/a11y/useSemanticElements: <fieldset> would force border/legend styles; role=group is the WAI-ARIA equivalent for OTP grouping
4135
- /* @__PURE__ */ jsxs("div", { className: rootClassName, role: "group", "aria-label": ariaLabel, children: [
4136
- /* @__PURE__ */ jsx("div", { className: "otp_input_boxes", children: digits.map((digit, i) => /* @__PURE__ */ jsx(
4137
- "input",
4138
- {
4139
- ref: (el) => {
4140
- inputsRef.current[i] = el;
4141
- },
4142
- type: "text",
4143
- inputMode: "numeric",
4144
- pattern: "\\d*",
4145
- maxLength: 1,
4146
- autoComplete: i === 0 ? "one-time-code" : "off",
4147
- value: digit,
4148
- onChange: (e) => handleChange(i, e),
4149
- onFocus: () => handleFocus(i),
4150
- onKeyDown: (e) => handleKeyDown2(i, e),
4151
- onPaste: handlePaste,
4152
- disabled,
4153
- "aria-label": `${i + 1}\uBC88\uC9F8 \uC790\uB9AC`,
4154
- "aria-invalid": error || void 0,
4155
- "aria-describedby": supportingText ? supportingId : void 0,
4156
- className: cn(
4157
- "otp_input_box",
4158
- error && "otp_input_box_error",
4159
- disabled && "otp_input_box_disabled"
5257
+ /* @__PURE__ */ jsxs(
5258
+ "div",
5259
+ {
5260
+ className: rootClassName,
5261
+ role: "group",
5262
+ "aria-labelledby": field?.labelId,
5263
+ "aria-label": field?.labelId ? void 0 : ariaLabel,
5264
+ children: [
5265
+ /* @__PURE__ */ jsx("div", { className: "otp_input_boxes", children: digits.map((digit, i) => /* @__PURE__ */ jsx(
5266
+ "input",
5267
+ {
5268
+ ref: (el) => {
5269
+ inputsRef.current[i] = el;
5270
+ },
5271
+ type: "text",
5272
+ inputMode: "numeric",
5273
+ pattern: "\\d*",
5274
+ maxLength: 1,
5275
+ autoComplete: i === 0 ? "one-time-code" : "off",
5276
+ value: digit,
5277
+ onChange: (e) => handleChange(i, e),
5278
+ onFocus: () => handleFocus(i),
5279
+ onKeyDown: (e) => handleKeyDown2(i, e),
5280
+ onPaste: handlePaste,
5281
+ disabled,
5282
+ "aria-label": t("otpInput.digit", { index: i + 1 }),
5283
+ "aria-invalid": error || field?.invalid || void 0,
5284
+ "aria-describedby": field?.describedBy ?? (supportingText ? supportingId : void 0),
5285
+ className: cn(
5286
+ "otp_input_box",
5287
+ error && "otp_input_box_error",
5288
+ disabled && "otp_input_box_disabled"
5289
+ )
5290
+ },
5291
+ i
5292
+ )) }),
5293
+ supportingText && /* @__PURE__ */ jsx(
5294
+ "span",
5295
+ {
5296
+ id: supportingId,
5297
+ className: cn("otp_input_supporting", error && "otp_input_supporting_error"),
5298
+ children: supportingText
5299
+ }
4160
5300
  )
4161
- },
4162
- i
4163
- )) }),
4164
- supportingText && /* @__PURE__ */ jsx(
4165
- "span",
4166
- {
4167
- id: supportingId,
4168
- className: cn("otp_input_supporting", error && "otp_input_supporting_error"),
4169
- children: supportingText
4170
- }
4171
- )
4172
- ] })
5301
+ ]
5302
+ }
5303
+ )
4173
5304
  );
4174
5305
  };
4175
5306
  OtpInput.displayName = "OtpInput";
@@ -4198,6 +5329,7 @@ var RadioGroup = ({
4198
5329
  const generatedName = React11.useId();
4199
5330
  const name = nameProp ?? generatedName;
4200
5331
  const idPrefix = React11.useId();
5332
+ const field = useFieldControl();
4201
5333
  const labelId = label ? `${idPrefix}-label` : void 0;
4202
5334
  const helperId = supportingText ? `${idPrefix}-help` : void 0;
4203
5335
  const onChange = React11.useCallback(
@@ -4227,9 +5359,10 @@ var RadioGroup = ({
4227
5359
  "div",
4228
5360
  {
4229
5361
  role: "radiogroup",
4230
- "aria-labelledby": labelId,
4231
- "aria-describedby": helperId,
4232
- "aria-invalid": error || void 0,
5362
+ "aria-labelledby": field?.labelId ?? labelId,
5363
+ "aria-describedby": field?.describedBy ?? helperId,
5364
+ "aria-invalid": error || field?.invalid || void 0,
5365
+ "aria-required": field?.required || void 0,
4233
5366
  className: "radio_group_options",
4234
5367
  children
4235
5368
  }
@@ -4262,6 +5395,7 @@ var Radio = ({
4262
5395
  onChange?.(event);
4263
5396
  };
4264
5397
  const rootClassName = cn("radio", `radio_size_${size}`, disabled && "radio_disabled", className);
5398
+ const field = useFieldControl();
4265
5399
  return /* @__PURE__ */ jsxs("label", { className: rootClassName, children: [
4266
5400
  /* @__PURE__ */ jsx(
4267
5401
  "input",
@@ -4269,6 +5403,8 @@ var Radio = ({
4269
5403
  ref,
4270
5404
  type: "radio",
4271
5405
  className: "radio_input",
5406
+ id: field?.inputId ?? props.id,
5407
+ "aria-describedby": field?.describedBy ?? props["aria-describedby"],
4272
5408
  value,
4273
5409
  name,
4274
5410
  disabled,
@@ -4282,6 +5418,149 @@ var Radio = ({
4282
5418
  ] });
4283
5419
  };
4284
5420
  Radio.displayName = "Radio";
5421
+ var SEPARATORS = /[,\t\n\r]+/;
5422
+ var splitTags = (text) => text.split(SEPARATORS).map((part) => part.trim()).filter(Boolean);
5423
+ var TagInput = ({
5424
+ value,
5425
+ defaultValue = [],
5426
+ onValueChange,
5427
+ placeholder: placeholderProp,
5428
+ maxTags,
5429
+ allowDuplicates = false,
5430
+ size = "md",
5431
+ disabled = false,
5432
+ fullWidth = false,
5433
+ ariaLabel,
5434
+ className,
5435
+ ...props
5436
+ }) => {
5437
+ const t = useLocaleText();
5438
+ const placeholder = placeholderProp ?? t("tagInput.placeholder");
5439
+ const generatedId = useId();
5440
+ const field = useFieldControl();
5441
+ const inputId = field?.inputId ?? generatedId;
5442
+ const isControlled = value !== void 0;
5443
+ const [innerTags, setInnerTags] = useState(defaultValue);
5444
+ const tags = isControlled ? value : innerTags;
5445
+ const [draft, setDraft] = useState("");
5446
+ const [announcement, setAnnouncement] = useState("");
5447
+ const inputRef = useRef(null);
5448
+ const isFull = maxTags !== void 0 && tags.length >= maxTags;
5449
+ const setTags = (next) => {
5450
+ if (!isControlled) setInnerTags(next);
5451
+ onValueChange?.(next);
5452
+ };
5453
+ const addTags = (text) => {
5454
+ const candidates = splitTags(text);
5455
+ if (candidates.length === 0) return 0;
5456
+ const next = [...tags];
5457
+ const added = [];
5458
+ const duplicates = [];
5459
+ for (const candidate of candidates) {
5460
+ if (maxTags !== void 0 && next.length >= maxTags) break;
5461
+ if (!allowDuplicates && next.includes(candidate)) {
5462
+ duplicates.push(candidate);
5463
+ continue;
5464
+ }
5465
+ next.push(candidate);
5466
+ added.push(candidate);
5467
+ }
5468
+ const isAtCap = maxTags !== void 0 && next.length >= maxTags;
5469
+ if (added.length === 0) {
5470
+ if (isAtCap) {
5471
+ setAnnouncement(t("tagInput.atCap", { max: maxTags }));
5472
+ } else if (duplicates.length > 0) {
5473
+ setAnnouncement(t("tagInput.duplicate", { names: duplicates.join(", ") }));
5474
+ }
5475
+ return 0;
5476
+ }
5477
+ setTags(next);
5478
+ const notes = [
5479
+ duplicates.length > 0 ? t("tagInput.duplicate", { names: duplicates.join(", ") }) : "",
5480
+ isAtCap && maxTags !== void 0 ? t("tagInput.atCap", { max: maxTags }) : ""
5481
+ ].filter(Boolean);
5482
+ setAnnouncement(
5483
+ notes.length > 0 ? t("tagInput.addedWithNotes", { names: added.join(", "), notes: notes.join(", ") }) : t("tagInput.added", { names: added.join(", ") })
5484
+ );
5485
+ return added.length;
5486
+ };
5487
+ const removeAt = (index) => {
5488
+ const removed = tags[index];
5489
+ setTags(tags.filter((_, i) => i !== index));
5490
+ setAnnouncement(t("tagInput.removed", { name: removed }));
5491
+ };
5492
+ const onKeyDown = (event) => {
5493
+ if (event.nativeEvent.isComposing) return;
5494
+ if (event.key === "Enter" || event.key === ",") {
5495
+ event.preventDefault();
5496
+ if (addTags(draft) > 0) setDraft("");
5497
+ return;
5498
+ }
5499
+ if (event.key === "Backspace" && draft === "" && tags.length > 0) {
5500
+ event.preventDefault();
5501
+ removeAt(tags.length - 1);
5502
+ }
5503
+ };
5504
+ const onPaste = (event) => {
5505
+ const text = event.clipboardData.getData("text");
5506
+ if (!SEPARATORS.test(text)) return;
5507
+ event.preventDefault();
5508
+ const input = event.currentTarget;
5509
+ const start = input.selectionStart ?? draft.length;
5510
+ const end = input.selectionEnd ?? draft.length;
5511
+ const merged = `${draft.slice(0, start)}${text}${draft.slice(end)}`;
5512
+ if (addTags(merged) > 0) setDraft("");
5513
+ };
5514
+ const rootClassName = cn(
5515
+ "tag_input",
5516
+ `tag_input_size_${size}`,
5517
+ { tag_input_full_width: fullWidth, tag_input_disabled: disabled },
5518
+ className
5519
+ );
5520
+ return /* @__PURE__ */ jsxs("div", { className: rootClassName, ...props, children: [
5521
+ /* @__PURE__ */ jsxs("div", { className: "tag_input_control", onClick: () => inputRef.current?.focus(), children: [
5522
+ tags.length > 0 && /* @__PURE__ */ jsx("ul", { className: "tag_input_tags", children: tags.map((tag, index) => (
5523
+ /* biome-ignore lint/suspicious/noArrayIndexKey: allowDuplicates 면 같은 라벨이 여러 개라 값만으로는 구분되지 않는다 */
5524
+ /* @__PURE__ */ jsx("li", { className: "tag_input_tag", children: /* @__PURE__ */ jsx(
5525
+ Chip,
5526
+ {
5527
+ type: "static",
5528
+ size: "sm",
5529
+ label: tag,
5530
+ removable: !disabled,
5531
+ onRemove: () => removeAt(index)
5532
+ }
5533
+ ) }, `${tag}-${index}`)
5534
+ )) }),
5535
+ /* @__PURE__ */ jsx(
5536
+ "input",
5537
+ {
5538
+ ref: inputRef,
5539
+ id: inputId,
5540
+ className: "tag_input_field",
5541
+ type: "text",
5542
+ autoComplete: "off",
5543
+ value: draft,
5544
+ disabled,
5545
+ readOnly: isFull,
5546
+ placeholder: isFull ? "" : placeholder,
5547
+ "aria-labelledby": field?.labelId,
5548
+ "aria-label": field?.labelId ? void 0 : ariaLabel,
5549
+ "aria-describedby": field?.describedBy,
5550
+ "aria-invalid": field?.invalid || void 0,
5551
+ "aria-required": field?.required || void 0,
5552
+ onChange: (event) => setDraft(event.target.value),
5553
+ onKeyDown,
5554
+ onPaste,
5555
+ onBlur: () => {
5556
+ if (addTags(draft) > 0) setDraft("");
5557
+ }
5558
+ }
5559
+ )
5560
+ ] }),
5561
+ /* @__PURE__ */ jsx("span", { className: "tag_input_live", role: "status", children: announcement })
5562
+ ] });
5563
+ };
4285
5564
  var LINE_HEIGHT_PX = {
4286
5565
  sm: 20,
4287
5566
  md: 20,
@@ -4313,8 +5592,10 @@ var Textarea = ({
4313
5592
  ...props
4314
5593
  }) => {
4315
5594
  const generatedId = useId();
4316
- const inputId = id ?? generatedId;
5595
+ const field = useFieldControl();
5596
+ const inputId = id ?? field?.inputId ?? generatedId;
4317
5597
  const helperId = supportingText ? `${inputId}-help` : void 0;
5598
+ const describedBy = field?.describedBy ?? helperId;
4318
5599
  const isControlled = value !== void 0;
4319
5600
  const applyTransform = (nextValue) => transformValue ? transformValue(nextValue) : nextValue;
4320
5601
  const [innerValue, setInnerValue] = useState(() => applyTransform(value ?? defaultValue ?? ""));
@@ -4346,228 +5627,187 @@ var Textarea = ({
4346
5627
  const maxH = maxRows ? maxRows * lh2 : Number.POSITIVE_INFINITY;
4347
5628
  el.style.height = "auto";
4348
5629
  const next = Math.min(Math.max(el.scrollHeight, minH), maxH);
4349
- el.style.height = `${next}px`;
4350
- el.style.overflowY = el.scrollHeight > maxH ? "auto" : "hidden";
4351
- }, [innerValue, autoGrow, size, minRows, maxRows]);
4352
- const rootClassName = cn(
4353
- "textarea",
4354
- size === "sm" && "textarea_size_sm",
4355
- size === "lg" && "textarea_size_lg",
4356
- fullWidth && "textarea_full_width",
4357
- error && "textarea_error",
4358
- props.disabled && "textarea_disabled",
4359
- className
4360
- );
4361
- const counterText = showCounter && maxLength !== void 0 ? `${innerValue.length}/${maxLength}` : showCounter ? String(innerValue.length) : null;
4362
- const emit = (nextValue) => {
4363
- setInnerValue(nextValue);
4364
- if (nextValue !== lastEmittedValueRef.current) {
4365
- lastEmittedValueRef.current = nextValue;
4366
- (onValueChange ?? onChangeAction)?.(nextValue);
4367
- }
4368
- };
4369
- return /* @__PURE__ */ jsxs("div", { className: rootClassName, children: [
4370
- label && showLabel && /* @__PURE__ */ jsx("label", { htmlFor: inputId, className: "textarea_label", children: label }),
4371
- /* @__PURE__ */ jsxs("div", { className: "textarea_container", children: [
4372
- toolbar && /* @__PURE__ */ jsx("div", { className: "textarea_toolbar", inert: props.disabled || void 0, children: toolbar }),
4373
- /* @__PURE__ */ jsx("div", { className: "textarea_input_wrap", children: /* @__PURE__ */ jsx(
4374
- "textarea",
4375
- {
4376
- id: inputId,
4377
- ref: setRefs,
4378
- className: "textarea_input",
4379
- style: { resize: autoGrow ? "none" : resize },
4380
- rows: autoGrow ? minRows ?? rows : rows,
4381
- maxLength,
4382
- "aria-invalid": !!error,
4383
- "aria-describedby": helperId,
4384
- "aria-label": !showLabel ? label : void 0,
4385
- ...props,
4386
- value: innerValue,
4387
- onCompositionStart: () => {
4388
- isComposingRef.current = true;
4389
- },
4390
- onCompositionEnd: (event) => {
4391
- isComposingRef.current = false;
4392
- emit(applyTransform(event.currentTarget.value));
4393
- },
4394
- onChange: (event) => {
4395
- const rawValue = event.target.value;
4396
- if (isComposingRef.current) {
4397
- setInnerValue(rawValue);
4398
- if (imeStrategy === "immediate" && rawValue !== lastEmittedValueRef.current) {
4399
- lastEmittedValueRef.current = rawValue;
4400
- (onValueChange ?? onChangeAction)?.(rawValue);
4401
- }
4402
- return;
4403
- }
4404
- emit(applyTransform(rawValue));
4405
- }
4406
- }
4407
- ) })
4408
- ] }),
4409
- (supportingText || counterText) && /* @__PURE__ */ jsxs("div", { className: "textarea_footer", children: [
4410
- supportingText ? /* @__PURE__ */ jsx("div", { id: helperId, className: "textarea_helper", children: supportingText }) : /* @__PURE__ */ jsx("span", {}),
4411
- counterText && /* @__PURE__ */ jsx("div", { className: "textarea_counter", "aria-hidden": "true", children: counterText })
4412
- ] })
4413
- ] });
4414
- };
4415
- Textarea.displayName = "Textarea";
4416
- var ClearIcon = () => /* @__PURE__ */ jsx(X, { size: iconSize.lg, "aria-hidden": "true" });
4417
- var DEFAULT_PASSWORD_TOGGLE_LABELS = { show: "\uBE44\uBC00\uBC88\uD638 \uD45C\uC2DC", hide: "\uBE44\uBC00\uBC88\uD638 \uC228\uAE30\uAE30" };
4418
- var TextField = ({
4419
- id,
4420
- label,
4421
- showLabel = true,
4422
- supportingText,
4423
- error,
4424
- success,
4425
- identifier,
4426
- leadingIcon,
4427
- trailingIcon,
4428
- leadingAction,
4429
- trailingAction,
4430
- showPasswordToggle,
4431
- passwordToggleLabels,
4432
- clearable,
4433
- clearLabel = "\uC9C0\uC6B0\uAE30",
4434
- type,
4435
- fullWidth,
4436
- size = "md",
4437
- variant = "outline",
4438
- className,
4439
- onValueChange,
4440
- onChangeAction,
4441
- imeStrategy = "delayed",
4442
- value,
4443
- defaultValue,
4444
- transformValue,
4445
- ref,
4446
- ...props
4447
- }) => {
4448
- const generatedId = useId();
4449
- const inputId = id ?? generatedId;
4450
- const helperId = supportingText ? `${inputId}-help` : void 0;
4451
- const isControlled = value !== void 0;
4452
- const applyTransform = (nextValue) => transformValue ? transformValue(nextValue) : nextValue;
4453
- const [innerValue, setInnerValue] = useState(() => applyTransform(value ?? defaultValue ?? ""));
4454
- const isComposingRef = useRef(false);
4455
- const lastEmittedValueRef = useRef(innerValue);
4456
- const [prevValue, setPrevValue] = useState(value);
4457
- if (isControlled && value !== prevValue && !isComposingRef.current) {
4458
- setPrevValue(value);
4459
- const nextValue = applyTransform(value ?? "");
4460
- setInnerValue(nextValue);
4461
- lastEmittedValueRef.current = nextValue;
4462
- }
4463
- const emit = useCallback(
4464
- (nextValue) => {
4465
- setInnerValue(nextValue);
4466
- if (nextValue !== lastEmittedValueRef.current) {
4467
- lastEmittedValueRef.current = nextValue;
4468
- (onValueChange ?? onChangeAction)?.(nextValue);
4469
- }
4470
- },
4471
- [onValueChange, onChangeAction]
4472
- );
4473
- const handleClear = useCallback(() => {
4474
- emit("");
4475
- }, [emit]);
4476
- const [passwordRevealed, setPasswordRevealed] = useState(false);
4477
- const togglePassword = useCallback(() => {
4478
- setPasswordRevealed((revealed) => !revealed);
4479
- }, []);
4480
- let resolvedType = type;
4481
- if (showPasswordToggle) {
4482
- resolvedType = passwordRevealed ? "text" : type ?? "password";
4483
- }
4484
- const isError = !!error;
4485
- const isSuccess = !!success && !isError;
5630
+ el.style.height = `${next}px`;
5631
+ el.style.overflowY = el.scrollHeight > maxH ? "auto" : "hidden";
5632
+ }, [innerValue, autoGrow, size, minRows, maxRows]);
4486
5633
  const rootClassName = cn(
4487
- "text_field",
4488
- `text_field_variant_${variant}`,
4489
- size === "sm" && "text_field_size_sm",
4490
- size === "lg" && "text_field_size_lg",
4491
- fullWidth && "text_field_full_width",
4492
- isError && "text_field_error",
4493
- isSuccess && "text_field_success",
4494
- props.disabled && "text_field_disabled",
5634
+ "textarea",
5635
+ size === "sm" && "textarea_size_sm",
5636
+ size === "lg" && "textarea_size_lg",
5637
+ fullWidth && "textarea_full_width",
5638
+ error && "textarea_error",
5639
+ props.disabled && "textarea_disabled",
4495
5640
  className
4496
5641
  );
4497
- const passwordToggleLabel = passwordRevealed ? passwordToggleLabels?.hide ?? DEFAULT_PASSWORD_TOGGLE_LABELS.hide : passwordToggleLabels?.show ?? DEFAULT_PASSWORD_TOGGLE_LABELS.show;
4498
- const resolvedTrailing = showPasswordToggle ? /* @__PURE__ */ jsx("span", { className: "text_field_icon text_field_action", children: /* @__PURE__ */ jsx(
4499
- "button",
4500
- {
4501
- type: "button",
4502
- onClick: togglePassword,
4503
- "aria-label": passwordToggleLabel,
4504
- disabled: props.disabled,
4505
- children: passwordRevealed ? /* @__PURE__ */ jsx(EyeOff, { size: iconSize.lg, "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Eye, { size: iconSize.lg, "aria-hidden": "true" })
4506
- }
4507
- ) }) : clearable && innerValue ? /* @__PURE__ */ jsx(
4508
- "button",
4509
- {
4510
- type: "button",
4511
- className: "text_field_clear",
4512
- onClick: handleClear,
4513
- "aria-label": clearLabel,
4514
- disabled: props.disabled,
4515
- children: /* @__PURE__ */ jsx(ClearIcon, {})
5642
+ const counterText = showCounter && maxLength !== void 0 ? `${innerValue.length}/${maxLength}` : showCounter ? String(innerValue.length) : null;
5643
+ const emit = (nextValue) => {
5644
+ setInnerValue(nextValue);
5645
+ if (nextValue !== lastEmittedValueRef.current) {
5646
+ lastEmittedValueRef.current = nextValue;
5647
+ (onValueChange ?? onChangeAction)?.(nextValue);
4516
5648
  }
4517
- ) : trailingAction ? /* @__PURE__ */ jsx("span", { className: "text_field_icon text_field_action", children: trailingAction }) : trailingIcon ? /* @__PURE__ */ jsx("span", { className: "text_field_icon", "aria-hidden": "true", children: trailingIcon }) : null;
4518
- const resolvedLeading = leadingAction ? /* @__PURE__ */ jsx("span", { className: "text_field_icon text_field_action", children: leadingAction }) : leadingIcon ? /* @__PURE__ */ jsx("span", { className: "text_field_icon", "aria-hidden": "true", children: leadingIcon }) : null;
5649
+ };
4519
5650
  return /* @__PURE__ */ jsxs("div", { className: rootClassName, children: [
4520
- label && showLabel && /* @__PURE__ */ jsx("label", { htmlFor: inputId, className: "text_field_label", children: label }),
4521
- /* @__PURE__ */ jsx("div", { className: "text_field_container", children: /* @__PURE__ */ jsxs("div", { className: "text_field_inner", children: [
4522
- resolvedLeading,
4523
- /* @__PURE__ */ jsx(
4524
- "div",
5651
+ label && showLabel && /* @__PURE__ */ jsx("label", { htmlFor: inputId, className: "textarea_label", children: label }),
5652
+ /* @__PURE__ */ jsxs("div", { className: "textarea_container", children: [
5653
+ toolbar && /* @__PURE__ */ jsx("div", { className: "textarea_toolbar", inert: props.disabled || void 0, children: toolbar }),
5654
+ /* @__PURE__ */ jsx("div", { className: "textarea_input_wrap", children: /* @__PURE__ */ jsx(
5655
+ "textarea",
4525
5656
  {
4526
- className: cn(
4527
- "text_field_input_wrap",
4528
- resolvedTrailing && "text_field_input_wrap_no_pad_right"
4529
- ),
4530
- children: /* @__PURE__ */ jsx(
4531
- "input",
4532
- {
4533
- id: inputId,
4534
- ref,
4535
- className: cn("text_field_input", identifier && "text_field_input_identifier"),
4536
- "aria-invalid": isError,
4537
- "aria-describedby": helperId,
4538
- "aria-label": !showLabel ? label : void 0,
4539
- ...props,
4540
- type: resolvedType,
4541
- value: innerValue,
4542
- onCompositionStart: () => {
4543
- isComposingRef.current = true;
4544
- },
4545
- onCompositionEnd: (event) => {
4546
- isComposingRef.current = false;
4547
- emit(applyTransform(event.currentTarget.value));
4548
- },
4549
- onChange: (event) => {
4550
- const rawValue = event.target.value;
4551
- if (isComposingRef.current) {
4552
- setInnerValue(rawValue);
4553
- if (imeStrategy === "immediate" && rawValue !== lastEmittedValueRef.current) {
4554
- lastEmittedValueRef.current = rawValue;
4555
- (onValueChange ?? onChangeAction)?.(rawValue);
4556
- }
4557
- return;
4558
- }
4559
- emit(applyTransform(rawValue));
5657
+ id: inputId,
5658
+ ref: setRefs,
5659
+ className: "textarea_input",
5660
+ style: { resize: autoGrow ? "none" : resize },
5661
+ rows: autoGrow ? minRows ?? rows : rows,
5662
+ maxLength,
5663
+ "aria-invalid": !!error || !!field?.invalid,
5664
+ "aria-describedby": describedBy,
5665
+ "aria-required": field?.required || void 0,
5666
+ "aria-label": !showLabel ? label : void 0,
5667
+ ...props,
5668
+ value: innerValue,
5669
+ onCompositionStart: () => {
5670
+ isComposingRef.current = true;
5671
+ },
5672
+ onCompositionEnd: (event) => {
5673
+ isComposingRef.current = false;
5674
+ emit(applyTransform(event.currentTarget.value));
5675
+ },
5676
+ onChange: (event) => {
5677
+ const rawValue = event.target.value;
5678
+ if (isComposingRef.current) {
5679
+ setInnerValue(rawValue);
5680
+ if (imeStrategy === "immediate" && rawValue !== lastEmittedValueRef.current) {
5681
+ lastEmittedValueRef.current = rawValue;
5682
+ (onValueChange ?? onChangeAction)?.(rawValue);
4560
5683
  }
5684
+ return;
4561
5685
  }
4562
- )
5686
+ emit(applyTransform(rawValue));
5687
+ }
4563
5688
  }
4564
- ),
4565
- resolvedTrailing
4566
- ] }) }),
4567
- supportingText && /* @__PURE__ */ jsx("div", { id: helperId, className: "text_field_helper", children: supportingText })
5689
+ ) })
5690
+ ] }),
5691
+ (supportingText || counterText) && /* @__PURE__ */ jsxs("div", { className: "textarea_footer", children: [
5692
+ supportingText ? /* @__PURE__ */ jsx("div", { id: helperId, className: "textarea_helper", children: supportingText }) : /* @__PURE__ */ jsx("span", {}),
5693
+ counterText && /* @__PURE__ */ jsx("div", { className: "textarea_counter", "aria-hidden": "true", children: counterText })
5694
+ ] })
4568
5695
  ] });
4569
5696
  };
4570
- TextField.displayName = "TextField";
5697
+ Textarea.displayName = "Textarea";
5698
+ var toMinutes = (value) => {
5699
+ if (!value) return null;
5700
+ const [h, m] = value.split(":").map(Number);
5701
+ if (!Number.isInteger(h) || !Number.isInteger(m)) return null;
5702
+ if (h < 0 || h > 23 || m < 0 || m > 59) return null;
5703
+ return h * 60 + m;
5704
+ };
5705
+ var pad2 = (n) => String(n).padStart(2, "0");
5706
+ var TimePicker = ({
5707
+ label,
5708
+ value,
5709
+ onValueChange,
5710
+ minuteStep = 5,
5711
+ minTime,
5712
+ maxTime,
5713
+ disabled,
5714
+ fullWidth = true,
5715
+ hourLabel: hourLabelProp,
5716
+ minuteLabel: minuteLabelProp
5717
+ }) => {
5718
+ const t = useLocaleText();
5719
+ const hourLabel = hourLabelProp ?? t("timePicker.hour");
5720
+ const minuteLabel = minuteLabelProp ?? t("timePicker.minute");
5721
+ const field = useFieldControl();
5722
+ const groupId = React11.useId();
5723
+ const constraintId = React11.useId();
5724
+ const min = toMinutes(minTime) ?? 0;
5725
+ const max = toMinutes(maxTime) ?? 23 * 60 + 59;
5726
+ const parsed = toMinutes(value);
5727
+ const hour = parsed === null ? null : Math.floor(parsed / 60);
5728
+ const minute = parsed === null ? null : parsed % 60;
5729
+ const hourOptions = React11.useMemo(() => {
5730
+ const first = Math.floor(min / 60);
5731
+ const last = Math.floor(max / 60);
5732
+ return Array.from({ length: Math.max(0, last - first + 1) }, (_, i) => {
5733
+ const h = first + i;
5734
+ return { value: String(h), label: pad2(h) };
5735
+ });
5736
+ }, [min, max]);
5737
+ const minuteOptions = React11.useMemo(() => {
5738
+ if (hour === null) return [];
5739
+ const step = Math.max(1, Math.floor(minuteStep));
5740
+ return Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step).filter((m) => m < 60).filter((m) => hour * 60 + m >= min && hour * 60 + m <= max).map((m) => ({ value: String(m), label: pad2(m) }));
5741
+ }, [hour, minuteStep, min, max]);
5742
+ const emit = (h, m) => onValueChange(`${pad2(h)}:${pad2(m)}`);
5743
+ const handleHourChange = (raw) => {
5744
+ if (!raw) return;
5745
+ const h = Number(raw);
5746
+ const step = Math.max(1, Math.floor(minuteStep));
5747
+ const candidates = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step).filter(
5748
+ (m) => m < 60 && h * 60 + m >= min && h * 60 + m <= max
5749
+ );
5750
+ if (candidates.length === 0) return;
5751
+ const keep = minute !== null && candidates.includes(minute) ? minute : candidates[0];
5752
+ emit(h, keep);
5753
+ };
5754
+ const handleMinuteChange = (raw) => {
5755
+ if (!raw || hour === null) return;
5756
+ emit(hour, Number(raw));
5757
+ };
5758
+ const constraint = minTime || maxTime ? t("timePicker.rangeSr", { min: minTime ?? "00:00", max: maxTime ?? "23:59" }) : "";
5759
+ return /* @__PURE__ */ jsxs(
5760
+ "div",
5761
+ {
5762
+ className: cn("time_picker", {
5763
+ time_picker_full_width: fullWidth,
5764
+ time_picker_disabled: disabled
5765
+ }),
5766
+ children: [
5767
+ label && /* @__PURE__ */ jsx("span", { className: "time_picker_label", id: groupId, children: label }),
5768
+ constraint && /* @__PURE__ */ jsx("span", { id: constraintId, className: "time_picker_sr_only", children: constraint }),
5769
+ /* @__PURE__ */ jsxs(
5770
+ "div",
5771
+ {
5772
+ className: "time_picker_fields",
5773
+ role: "group",
5774
+ "aria-labelledby": field?.labelId ?? (label ? groupId : void 0),
5775
+ "aria-describedby": [field?.describedBy, constraint ? constraintId : void 0].filter(Boolean).join(" ") || void 0,
5776
+ "aria-invalid": field?.invalid || void 0,
5777
+ children: [
5778
+ /* @__PURE__ */ jsx(
5779
+ Dropdown,
5780
+ {
5781
+ size: "sm",
5782
+ fullWidth: true,
5783
+ label: hourLabel,
5784
+ placeholder: hourLabel,
5785
+ options: hourOptions,
5786
+ value: hour === null ? null : String(hour),
5787
+ onValueChange: handleHourChange,
5788
+ disabled
5789
+ }
5790
+ ),
5791
+ /* @__PURE__ */ jsx(
5792
+ Dropdown,
5793
+ {
5794
+ size: "sm",
5795
+ fullWidth: true,
5796
+ label: minuteLabel,
5797
+ placeholder: minuteLabel,
5798
+ options: minuteOptions,
5799
+ value: minute === null ? null : String(minute),
5800
+ onValueChange: handleMinuteChange,
5801
+ disabled: disabled || hour === null
5802
+ }
5803
+ )
5804
+ ]
5805
+ }
5806
+ )
5807
+ ]
5808
+ }
5809
+ );
5810
+ };
4571
5811
  var Toggle = ({
4572
5812
  checked,
4573
5813
  defaultChecked,
@@ -4590,6 +5830,7 @@ var Toggle = ({
4590
5830
  if (!isControlled) setInnerChecked(next);
4591
5831
  (onCheckedChange ?? onChange)?.(next);
4592
5832
  };
5833
+ const field = useFieldControl();
4593
5834
  const rootClassName = cn(
4594
5835
  "toggle",
4595
5836
  `toggle_size_${size}`,
@@ -4604,7 +5845,10 @@ var Toggle = ({
4604
5845
  type: "button",
4605
5846
  role: "switch",
4606
5847
  "aria-checked": isOn,
4607
- "aria-label": ariaLabel,
5848
+ id: field?.inputId ?? props.id,
5849
+ "aria-describedby": field?.describedBy ?? props["aria-describedby"],
5850
+ "aria-labelledby": field?.labelId,
5851
+ "aria-label": field?.labelId ? void 0 : ariaLabel,
4608
5852
  disabled,
4609
5853
  onClick: handleToggle,
4610
5854
  className: rootClassName,
@@ -4630,92 +5874,6 @@ var IconButton = ({
4630
5874
  );
4631
5875
  return /* @__PURE__ */ jsx("button", { ref, type, className: buttonClassName, ...props, children: /* @__PURE__ */ jsx("span", { className: "icon_button_icon", "aria-hidden": "true", children: icon }) });
4632
5876
  };
4633
- var range2 = (start, end) => {
4634
- const out = [];
4635
- for (let i = start; i <= end; i += 1) out.push(i);
4636
- return out;
4637
- };
4638
- var getPaginationItems = (page, totalPages) => {
4639
- if (totalPages <= 7) return range2(1, totalPages);
4640
- const items = [];
4641
- const last = totalPages;
4642
- const sibling = 2;
4643
- if (page <= sibling + 2) {
4644
- for (const p of range2(1, sibling + 3)) items.push(p);
4645
- items.push("ellipsis");
4646
- items.push(last);
4647
- return items;
4648
- }
4649
- if (page >= last - sibling - 1) {
4650
- items.push(1);
4651
- items.push("ellipsis");
4652
- for (const p of range2(last - sibling - 2, last)) items.push(p);
4653
- return items;
4654
- }
4655
- items.push(1);
4656
- items.push("ellipsis");
4657
- for (const p of range2(page - sibling, page + sibling)) items.push(p);
4658
- items.push("ellipsis");
4659
- items.push(last);
4660
- return items;
4661
- };
4662
- var Pagination = ({
4663
- page,
4664
- totalPages,
4665
- onPageChange,
4666
- onChange,
4667
- prevLabel = "\uC774\uC804 \uD398\uC774\uC9C0",
4668
- nextLabel = "\uB2E4\uC74C \uD398\uC774\uC9C0",
4669
- navLabel = "\uD398\uC774\uC9C0 \uC774\uB3D9"
4670
- }) => {
4671
- const emit = onPageChange ?? onChange;
4672
- const prevDisabled = page <= 1;
4673
- const nextDisabled = page >= totalPages;
4674
- const items = React11.useMemo(() => getPaginationItems(page, totalPages), [page, totalPages]);
4675
- return /* @__PURE__ */ jsxs("nav", { className: "pagination", "aria-label": navLabel, children: [
4676
- /* @__PURE__ */ jsx(
4677
- "button",
4678
- {
4679
- type: "button",
4680
- className: "pagination_item",
4681
- onClick: () => emit?.(page - 1),
4682
- disabled: prevDisabled,
4683
- "aria-label": prevLabel,
4684
- children: "\u2039"
4685
- }
4686
- ),
4687
- /* @__PURE__ */ jsx("ul", { className: "pagination_pages", children: items.map((it, idx) => {
4688
- if (it === "ellipsis") {
4689
- const prev = items[idx - 1];
4690
- const next = items[idx + 1];
4691
- return /* @__PURE__ */ jsx("li", { className: "pagination_ellipsis", "aria-hidden": "true", children: "\u2026" }, `e-${prev}-${next}`);
4692
- }
4693
- const isActive = it === page;
4694
- const buttonClassName = cn("pagination_page_button", { pagination_active: isActive });
4695
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
4696
- "button",
4697
- {
4698
- type: "button",
4699
- className: buttonClassName,
4700
- onClick: () => emit?.(it),
4701
- "aria-current": isActive ? "page" : void 0,
4702
- children: it
4703
- }
4704
- ) }, it);
4705
- }) }),
4706
- /* @__PURE__ */ jsx(
4707
- "button",
4708
- {
4709
- type: "button",
4710
- className: "pagination_item",
4711
- onClick: () => emit?.(page + 1),
4712
- disabled: nextDisabled,
4713
- "aria-label": nextLabel,
4714
- children: "\u203A"
4715
- }
4716
- )
4717
- ] });
4718
- };
4719
5877
  var SLIDE_FROM = {
4720
5878
  left: "translateX(-100%)",
4721
5879
  right: "translateX(100%)",
@@ -4731,13 +5889,15 @@ var Drawer = ({
4731
5889
  closeOnOverlay = true,
4732
5890
  dismissible,
4733
5891
  showCloseIcon = true,
4734
- closeLabel = "\uB2EB\uAE30",
5892
+ closeLabel: closeLabelProp,
4735
5893
  ariaLabel,
4736
5894
  onExited,
4737
5895
  children,
4738
5896
  className,
4739
5897
  ...props
4740
5898
  }) => {
5899
+ const t = useLocaleText();
5900
+ const closeLabel = closeLabelProp ?? t("drawer.close");
4741
5901
  const lastContentRef = React11.useRef({ children, title, footer });
4742
5902
  if (open) lastContentRef.current = { children, title, footer };
4743
5903
  const content = open ? { children, title, footer } : lastContentRef.current;
@@ -4837,13 +5997,15 @@ var Modal = ({
4837
5997
  footer,
4838
5998
  footerAlign = "end",
4839
5999
  showCloseIcon = true,
4840
- closeLabel = "\uB2EB\uAE30",
6000
+ closeLabel: closeLabelProp,
4841
6001
  children,
4842
6002
  className,
4843
6003
  ariaLabel,
4844
6004
  onExited,
4845
6005
  ...props
4846
6006
  }) => {
6007
+ const t = useLocaleText();
6008
+ const closeLabel = closeLabelProp ?? t("modal.close");
4847
6009
  const lastContentRef = React11.useRef({ children, title, description, footer });
4848
6010
  if (open) lastContentRef.current = { children, title, description, footer };
4849
6011
  const content = open ? { children, title, description, footer } : lastContentRef.current;
@@ -5010,15 +6172,39 @@ var ThemeProvider = ({
5010
6172
  );
5011
6173
  return /* @__PURE__ */ jsx(ThemeContext.Provider, { value, children });
5012
6174
  };
6175
+ var AppShell = ({
6176
+ sidebar,
6177
+ header,
6178
+ padded = true,
6179
+ className,
6180
+ children,
6181
+ ref,
6182
+ ...props
6183
+ }) => /* @__PURE__ */ jsxs(
6184
+ "div",
6185
+ {
6186
+ ref,
6187
+ className: cn("app_shell", { app_shell_with_sidebar: !!sidebar }, className),
6188
+ ...props,
6189
+ children: [
6190
+ sidebar && /* @__PURE__ */ jsx("div", { className: "app_shell_sidebar", children: sidebar }),
6191
+ /* @__PURE__ */ jsxs("div", { className: "app_shell_body", children: [
6192
+ header && /* @__PURE__ */ jsx("div", { className: "app_shell_header", children: header }),
6193
+ /* @__PURE__ */ jsx("main", { className: cn("app_shell_main", { app_shell_main_padded: padded }), children })
6194
+ ] })
6195
+ ]
6196
+ }
6197
+ );
5013
6198
  var Container = ({
5014
6199
  size = "xl",
5015
6200
  center = true,
5016
- as: Tag = "div",
6201
+ as,
5017
6202
  ref,
5018
6203
  className,
5019
6204
  children,
5020
6205
  ...props
5021
6206
  }) => {
6207
+ const Tag = as ?? "div";
5022
6208
  return /* @__PURE__ */ jsx(
5023
6209
  Tag,
5024
6210
  {
@@ -5036,7 +6222,7 @@ var Grid = ({
5036
6222
  rowGap,
5037
6223
  colGap,
5038
6224
  singleColOnMobile = true,
5039
- as: Tag = "div",
6225
+ as,
5040
6226
  ref,
5041
6227
  className,
5042
6228
  children,
@@ -5044,6 +6230,7 @@ var Grid = ({
5044
6230
  ...props
5045
6231
  }) => {
5046
6232
  const gridTemplateColumns = cols === "auto" ? `repeat(auto-fill, minmax(${minColWidth}, 1fr))` : `repeat(${cols}, 1fr)`;
6233
+ const Tag = as ?? "div";
5047
6234
  return /* @__PURE__ */ jsx(
5048
6235
  Tag,
5049
6236
  {
@@ -5061,15 +6248,36 @@ var Grid = ({
5061
6248
  }
5062
6249
  );
5063
6250
  };
6251
+ var PageHeader = ({
6252
+ title,
6253
+ description,
6254
+ breadcrumb,
6255
+ actions,
6256
+ tabs,
6257
+ className,
6258
+ ref,
6259
+ ...props
6260
+ }) => /* @__PURE__ */ jsxs("div", { ref, className: cn("page_header", className), ...props, children: [
6261
+ breadcrumb && /* @__PURE__ */ jsx("div", { className: "page_header_breadcrumb", children: breadcrumb }),
6262
+ /* @__PURE__ */ jsxs("div", { className: "page_header_bar", children: [
6263
+ /* @__PURE__ */ jsxs("div", { className: "page_header_titles", children: [
6264
+ /* @__PURE__ */ jsx("h1", { className: "page_header_title", children: title }),
6265
+ description && /* @__PURE__ */ jsx("p", { className: "page_header_description", children: description })
6266
+ ] }),
6267
+ actions && /* @__PURE__ */ jsx("div", { className: "page_header_actions", children: actions })
6268
+ ] }),
6269
+ tabs && /* @__PURE__ */ jsx("div", { className: "page_header_tabs", children: tabs })
6270
+ ] });
5064
6271
  var Section = ({
5065
6272
  spacing: spacing2 = "md",
5066
6273
  bg = "default",
5067
- as: Tag = "section",
6274
+ as,
5068
6275
  ref,
5069
6276
  className,
5070
6277
  children,
5071
6278
  ...props
5072
6279
  }) => {
6280
+ const Tag = as ?? "section";
5073
6281
  return /* @__PURE__ */ jsx(
5074
6282
  Tag,
5075
6283
  {
@@ -5086,13 +6294,14 @@ var Stack = ({
5086
6294
  align,
5087
6295
  justify,
5088
6296
  wrap,
5089
- as: Tag = "div",
6297
+ as,
5090
6298
  ref,
5091
6299
  className,
5092
6300
  children,
5093
6301
  style,
5094
6302
  ...props
5095
6303
  }) => {
6304
+ const Tag = as ?? "div";
5096
6305
  return /* @__PURE__ */ jsx(
5097
6306
  Tag,
5098
6307
  {
@@ -5112,4 +6321,4 @@ var Stack = ({
5112
6321
  );
5113
6322
  };
5114
6323
 
5115
- 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 };
6324
+ export { Accordion, AlertProvider, AppShell, Avatar, Badge, BottomNav, BottomNavItem, BottomNavSpacer, Breadcrumb, Button, Card, Checkbox, Chip, Combobox, Container, DataView, DatePicker, DateRangePicker, DescriptionList, Divider, Drawer, Dropdown, EmptyState, ErrorState, Field, FileInput, Form, Grid, Hero, Icon, IconButton, ImageCropper, LinearProgress, ListItem, LocaleProvider, MediaCard, Menu, Modal, NavBar, NavLink, OtpInput, PageHeader, Pagination, Popover, Prose, Radio, RadioGroup, Section, Sidebar, SidebarItem, SidebarSection, Skeleton, Spinner, Stack, Stat, Tab, TabList, TabPanel, Table, Tabs, TagInput, TextField, Textarea, ThemeProvider, TimePicker, Timeline, ToastProvider, Toggle, Tooltip, TopLoading, a11y, baseBorderWidth, baseColors, baseTypography, borderWidth, breakpoints, catalogs, cn, colors, elevation, en, iconSize, ko, motion, opacity, radius, skeleton, spacing, typography, useAlert, useFieldControl, useFocusTrap, useListboxPopup, useLocaleName, useLocaleText, useRadioGroupContext, useReducedMotion, useSpringHover, useSpringPresence, useTheme, useToast, zIndex };