@bigtablet/design-system 3.15.2 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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
 
@@ -409,6 +409,168 @@ function useSpringPresence({
409
409
  }
410
410
  });
411
411
  }
412
+ var MIN_SPACE_BELOW = 120;
413
+ function useListboxPopup({
414
+ items,
415
+ onCommit,
416
+ disabled = false,
417
+ returnFocusOnClose = false,
418
+ initialActiveIndex
419
+ }) {
420
+ const [isOpen, setIsOpen] = useState(false);
421
+ const [activeIndex, setActiveIndex] = useState(-1);
422
+ const [dropUp, setDropUp] = useState(false);
423
+ const wrapperRef = useRef(null);
424
+ const triggerRef = useRef(null);
425
+ const listRef = useRef(null);
426
+ const close = useCallback(() => {
427
+ setIsOpen(false);
428
+ if (returnFocusOnClose) triggerRef.current?.focus();
429
+ }, [returnFocusOnClose]);
430
+ useEffect(() => {
431
+ const handleOutsideClick = (event) => {
432
+ if (!wrapperRef.current?.contains(event.target)) setIsOpen(false);
433
+ };
434
+ document.addEventListener("mousedown", handleOutsideClick);
435
+ return () => document.removeEventListener("mousedown", handleOutsideClick);
436
+ }, []);
437
+ const moveActive = useCallback(
438
+ (dir) => {
439
+ if (items.length === 0) return;
440
+ if (!isOpen) {
441
+ setIsOpen(true);
442
+ return;
443
+ }
444
+ let i = activeIndex;
445
+ if (i === -1) {
446
+ i = dir === 1 ? -1 : 0;
447
+ }
448
+ const len = items.length;
449
+ for (let step = 0; step < len; step++) {
450
+ i = (i + dir + len) % len;
451
+ if (!items[i].disabled) {
452
+ setActiveIndex(i);
453
+ break;
454
+ }
455
+ }
456
+ },
457
+ [items, isOpen, activeIndex]
458
+ );
459
+ const commitActive = useCallback(() => {
460
+ if (activeIndex < 0 || activeIndex >= items.length) return;
461
+ const item = items[activeIndex];
462
+ if (item.disabled) return;
463
+ onCommit(item);
464
+ }, [activeIndex, items, onCommit]);
465
+ const firstEnabled = useCallback(() => items.findIndex((o) => !o.disabled), [items]);
466
+ const lastEnabled = useCallback(() => {
467
+ for (let i = items.length - 1; i >= 0; i--) {
468
+ if (!items[i].disabled) return i;
469
+ }
470
+ return -1;
471
+ }, [items]);
472
+ const onTriggerKeyDown = useCallback(
473
+ (event) => {
474
+ if (disabled) return;
475
+ switch (event.key) {
476
+ case " ":
477
+ case "Enter":
478
+ event.preventDefault();
479
+ if (!isOpen) setIsOpen(true);
480
+ else commitActive();
481
+ break;
482
+ case "ArrowDown":
483
+ event.preventDefault();
484
+ moveActive(1);
485
+ break;
486
+ case "ArrowUp":
487
+ event.preventDefault();
488
+ moveActive(-1);
489
+ break;
490
+ case "Home":
491
+ event.preventDefault();
492
+ setIsOpen(true);
493
+ setActiveIndex(firstEnabled());
494
+ break;
495
+ case "End":
496
+ event.preventDefault();
497
+ setIsOpen(true);
498
+ setActiveIndex(lastEnabled());
499
+ break;
500
+ case "Escape":
501
+ event.preventDefault();
502
+ setIsOpen(false);
503
+ break;
504
+ case "Tab":
505
+ setIsOpen(false);
506
+ break;
507
+ }
508
+ },
509
+ [disabled, isOpen, commitActive, moveActive, firstEnabled, lastEnabled]
510
+ );
511
+ const onInputKeyDown = useCallback(
512
+ (event) => {
513
+ if (disabled) return;
514
+ if (event.nativeEvent.isComposing) return;
515
+ switch (event.key) {
516
+ case "ArrowDown":
517
+ event.preventDefault();
518
+ moveActive(1);
519
+ break;
520
+ case "ArrowUp":
521
+ event.preventDefault();
522
+ moveActive(-1);
523
+ break;
524
+ case "Enter":
525
+ event.preventDefault();
526
+ commitActive();
527
+ break;
528
+ case "Escape":
529
+ event.preventDefault();
530
+ close();
531
+ break;
532
+ case "Tab":
533
+ close();
534
+ break;
535
+ }
536
+ },
537
+ [disabled, moveActive, commitActive, close]
538
+ );
539
+ useEffect(() => {
540
+ if (!isOpen) return;
541
+ const preferred = initialActiveIndex?.(items) ?? -1;
542
+ setActiveIndex(preferred >= 0 ? preferred : items.findIndex((o) => !o.disabled));
543
+ }, [isOpen, items]);
544
+ useEffect(() => {
545
+ if (!isOpen || activeIndex < 0) return;
546
+ const list = listRef.current;
547
+ if (!list) return;
548
+ const option = list.querySelectorAll('[role="option"]')[activeIndex];
549
+ option?.scrollIntoView?.({ block: "nearest" });
550
+ }, [isOpen, activeIndex, items]);
551
+ useEffect(() => {
552
+ if (!isOpen || !triggerRef.current) return;
553
+ const rect = triggerRef.current.getBoundingClientRect();
554
+ const spaceBelow = window.innerHeight - rect.bottom;
555
+ const spaceAbove = rect.top;
556
+ setDropUp(spaceBelow < MIN_SPACE_BELOW && spaceAbove > spaceBelow);
557
+ }, [isOpen]);
558
+ return {
559
+ isOpen,
560
+ setIsOpen,
561
+ dropUp,
562
+ activeIndex,
563
+ setActiveIndex,
564
+ wrapperRef,
565
+ triggerRef,
566
+ listRef,
567
+ close,
568
+ moveActive,
569
+ commitActive,
570
+ onTriggerKeyDown,
571
+ onInputKeyDown
572
+ };
573
+ }
412
574
 
413
575
  // src/styles/icon/index.ts
414
576
  var iconSize = {
@@ -550,6 +712,90 @@ var Badge = ({
550
712
  }
551
713
  );
552
714
  };
715
+ var DescriptionList = ({
716
+ items,
717
+ layout = "row",
718
+ divided = false,
719
+ className,
720
+ ref,
721
+ ...props
722
+ }) => /* @__PURE__ */ jsx(
723
+ "dl",
724
+ {
725
+ ref,
726
+ className: cn(
727
+ "description_list",
728
+ `description_list_layout_${layout}`,
729
+ { description_list_divided: divided },
730
+ className
731
+ ),
732
+ ...props,
733
+ children: items.map((item, index) => /* @__PURE__ */ jsxs(
734
+ "div",
735
+ {
736
+ className: cn("description_list_item", { description_list_item_full: item.full }),
737
+ children: [
738
+ /* @__PURE__ */ jsx("dt", { className: "description_list_label", children: item.label }),
739
+ /* @__PURE__ */ jsx("dd", { className: "description_list_value", children: item.value })
740
+ ]
741
+ },
742
+ index
743
+ ))
744
+ }
745
+ );
746
+ var isPresent = (value) => value !== void 0 && value !== null && value !== "";
747
+ var Stat = ({
748
+ label,
749
+ value,
750
+ delta,
751
+ deltaTone = "neutral",
752
+ icon,
753
+ className,
754
+ ref,
755
+ ...props
756
+ }) => /* @__PURE__ */ jsxs("div", { ref, className: cn("stat", className), ...props, children: [
757
+ /* @__PURE__ */ jsxs("div", { className: "stat_label", children: [
758
+ icon && /* @__PURE__ */ jsx("span", { className: "stat_icon", "aria-hidden": "true", children: icon }),
759
+ label
760
+ ] }),
761
+ /* @__PURE__ */ jsx("div", { className: "stat_value", children: value }),
762
+ isPresent(delta) && /* @__PURE__ */ jsx("div", { className: cn("stat_delta", `stat_delta_${deltaTone}`), children: delta })
763
+ ] });
764
+ var CheckGlyph = () => /* @__PURE__ */ jsx(
765
+ "svg",
766
+ {
767
+ viewBox: "0 0 20 20",
768
+ fill: "none",
769
+ stroke: "currentColor",
770
+ strokeWidth: 2,
771
+ strokeLinecap: "round",
772
+ strokeLinejoin: "round",
773
+ "aria-hidden": "true",
774
+ focusable: "false",
775
+ className: "timeline_glyph",
776
+ children: /* @__PURE__ */ jsx("polyline", { points: "4 10 8 14 16 6" })
777
+ }
778
+ );
779
+ var isPresent2 = (value) => value !== void 0 && value !== null && value !== "";
780
+ var Timeline = ({ items, className, ref, ...props }) => /* @__PURE__ */ jsx("ol", { ref, className: cn("timeline", className), ...props, children: items.map((item) => {
781
+ const status = item.status ?? "pending";
782
+ return /* @__PURE__ */ jsxs("li", { className: cn("timeline_item", `timeline_item_${status}`), children: [
783
+ /* @__PURE__ */ jsx("span", { className: "timeline_indicator", "aria-hidden": "true", children: item.icon ?? (status === "done" ? /* @__PURE__ */ jsx(CheckGlyph, {}) : /* @__PURE__ */ jsx(
784
+ "span",
785
+ {
786
+ className: cn("timeline_dot", { timeline_dot_hollow: status === "pending" })
787
+ }
788
+ )) }),
789
+ /* @__PURE__ */ jsxs("div", { className: "timeline_body", children: [
790
+ /* @__PURE__ */ jsxs("div", { className: "timeline_head", children: [
791
+ /* @__PURE__ */ jsx("div", { className: "timeline_title", children: item.title }),
792
+ isPresent2(item.time) && /* @__PURE__ */ jsx("div", { className: "timeline_time", children: item.time })
793
+ ] }),
794
+ isPresent2(item.description) && /* @__PURE__ */ jsx("div", { className: "timeline_description", children: item.description }),
795
+ item.children
796
+ ] })
797
+ ] }, item.id);
798
+ }) });
553
799
  var EmptyState = ({
554
800
  illustration,
555
801
  title,
@@ -579,12 +825,181 @@ var EmptyState = ({
579
825
  }
580
826
  );
581
827
  };
828
+
829
+ // src/ui/system/locale-provider/messages.ts
830
+ var ko = {
831
+ "chip.remove": "{label} \uC81C\uAC70",
832
+ "dataView.clearSelection": "\uC120\uD0DD \uD574\uC81C",
833
+ "dataView.empty": "\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
834
+ "dataView.errorTitle": "\uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4",
835
+ "dataView.retry": "\uB2E4\uC2DC \uC2DC\uB3C4",
836
+ "dataView.search": "\uAC80\uC0C9",
837
+ "dataView.selectionSummary": "{count}\uAC1C \uC120\uD0DD\uB428",
838
+ "table.empty": "\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
839
+ "table.rowClickHint": "\uD074\uB9AD \uAC00\uB2A5\uD55C \uD589",
840
+ "table.selectAll": "\uC804\uCCB4 \uC120\uD0DD",
841
+ "table.selectRow": "{index}\uBC88\uC9F8 \uD589 \uC120\uD0DD",
842
+ "alert.cancel": "\uCDE8\uC18C",
843
+ "alert.confirm": "\uD655\uC778",
844
+ "errorState.title": "\uBB38\uC81C\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4",
845
+ "spinner.label": "\uB85C\uB529 \uC911",
846
+ "toast.close": "\uB2EB\uAE30",
847
+ "toast.region": "\uC54C\uB9BC",
848
+ "topLoading.label": "\uD398\uC774\uC9C0 \uB85C\uB529 \uC911",
849
+ "combobox.empty": "\uC77C\uCE58\uD558\uB294 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4",
850
+ "combobox.idle": "\uAC80\uC0C9\uC5B4\uB97C \uC785\uB825\uD558\uC138\uC694",
851
+ "combobox.loading": "\uAC80\uC0C9 \uC911",
852
+ "combobox.placeholder": "\uAC80\uC0C9\uD574\uC11C \uC120\uD0DD",
853
+ "datePicker.day": "\uC77C",
854
+ "datePicker.minDateSr": "\uCD5C\uC18C \uB0A0\uC9DC: {date}",
855
+ "datePicker.month": "\uC6D4",
856
+ "datePicker.rangeUntilTodaySr": "\uC624\uB298\uAE4C\uC9C0 \uC120\uD0DD \uAC00\uB2A5",
857
+ "datePicker.year": "\uB144",
858
+ "dateRange.end": "\uC885\uB8CC\uC77C",
859
+ "dateRange.start": "\uC2DC\uC791\uC77C",
860
+ "dropdown.empty": "\uACB0\uACFC \uC5C6\uC74C",
861
+ "dropdown.placeholder": "\uC120\uD0DD\u2026",
862
+ "dropdown.searchPlaceholder": "\uAC80\uC0C9\u2026",
863
+ "dropdown.selectedSummary": "{count}\uAC1C \uC120\uD0DD",
864
+ "fileInput.label": "\uD30C\uC77C \uC120\uD0DD",
865
+ "fileInput.removeImage": "\uC774\uBBF8\uC9C0 \uC81C\uAC70",
866
+ "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.",
867
+ "imageCropper.label": "\uC774\uBBF8\uC9C0 \uC704\uCE58\uC640 \uBC30\uC728 \uC870\uC815",
868
+ "imageCropper.noPanHint": "\uC774\uBBF8\uC9C0\uAC00 \uBDF0\uD3EC\uD2B8\uB97C \uB531 \uCC44\uC6CC \uC774\uB3D9 \uC5EC\uC720\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
869
+ "imageCropper.zoom": "\uBC30\uC728",
870
+ "imageCropper.zoomIn": "\uD655\uB300",
871
+ "imageCropper.zoomOut": "\uCD95\uC18C",
872
+ "otpInput.digit": "{index}\uBC88\uC9F8 \uC790\uB9AC",
873
+ "otpInput.label": "OTP \uC785\uB825",
874
+ "tagInput.added": "{names} \uCD94\uAC00\uB428",
875
+ "tagInput.addedWithNotes": "{names} \uCD94\uAC00\uB428 ({notes})",
876
+ "tagInput.atCap": "\uCD5C\uB300 {max}\uAC1C\uAE4C\uC9C0 \uCD94\uAC00\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4",
877
+ "tagInput.duplicate": "{names} \uC774\uBBF8 \uC788\uC74C",
878
+ "tagInput.placeholder": "\uC785\uB825 \uD6C4 Enter",
879
+ "tagInput.removed": "{name} \uC81C\uAC70\uB428",
880
+ "textField.clear": "\uC9C0\uC6B0\uAE30",
881
+ "textField.passwordHide": "\uBE44\uBC00\uBC88\uD638 \uC228\uAE30\uAE30",
882
+ "textField.passwordShow": "\uBE44\uBC00\uBC88\uD638 \uD45C\uC2DC",
883
+ "timePicker.hour": "\uC2DC",
884
+ "timePicker.minute": "\uBD84",
885
+ "timePicker.rangeSr": "{min} \uBD80\uD130 {max} \uAE4C\uC9C0 \uC120\uD0DD \uAC00\uB2A5",
886
+ "bottomNav.label": "\uC8FC\uC694 \uBA54\uB274",
887
+ "breadcrumb.label": "\uD604\uC7AC \uC704\uCE58",
888
+ "pagination.label": "\uD398\uC774\uC9C0 \uC774\uB3D9",
889
+ "pagination.next": "\uB2E4\uC74C \uD398\uC774\uC9C0",
890
+ "pagination.prev": "\uC774\uC804 \uD398\uC774\uC9C0",
891
+ "sidebar.toggle": "\uC0AC\uC774\uB4DC\uBC14 \uD1A0\uAE00",
892
+ "drawer.close": "\uB2EB\uAE30",
893
+ "modal.close": "\uB2EB\uAE30"
894
+ };
895
+ var en = {
896
+ "chip.remove": "Remove {label}",
897
+ "dataView.clearSelection": "Clear selection",
898
+ "dataView.empty": "No data",
899
+ "dataView.errorTitle": "Could not load",
900
+ "dataView.retry": "Try again",
901
+ "dataView.search": "Search",
902
+ "dataView.selectionSummary": "{count} selected",
903
+ "table.empty": "No data",
904
+ "table.rowClickHint": "Clickable row",
905
+ "table.selectAll": "Select all",
906
+ "table.selectRow": "Select row {index}",
907
+ "alert.cancel": "Cancel",
908
+ "alert.confirm": "OK",
909
+ "errorState.title": "Something went wrong",
910
+ "spinner.label": "Loading",
911
+ "toast.close": "Close",
912
+ "toast.region": "Notifications",
913
+ "topLoading.label": "Loading page",
914
+ "combobox.empty": "No matches",
915
+ "combobox.idle": "Type to search",
916
+ "combobox.loading": "Searching",
917
+ "combobox.placeholder": "Search to select",
918
+ "datePicker.day": "Day",
919
+ "datePicker.minDateSr": "Earliest date: {date}",
920
+ "datePicker.month": "Month",
921
+ "datePicker.rangeUntilTodaySr": "Selectable up to today",
922
+ "datePicker.year": "Year",
923
+ "dateRange.end": "End date",
924
+ "dateRange.start": "Start date",
925
+ "dropdown.empty": "No results",
926
+ "dropdown.placeholder": "Select\u2026",
927
+ "dropdown.searchPlaceholder": "Search\u2026",
928
+ "dropdown.selectedSummary": "{count} selected",
929
+ "fileInput.label": "Choose file",
930
+ "fileInput.removeImage": "Remove image",
931
+ "imageCropper.hint": "Drag (or use arrow keys) to move, wheel or slider to zoom.",
932
+ "imageCropper.label": "Adjust image position and zoom",
933
+ "imageCropper.noPanHint": "The image fills the viewport exactly, so there is no room to move it.",
934
+ "imageCropper.zoom": "Zoom",
935
+ "imageCropper.zoomIn": "Zoom in",
936
+ "imageCropper.zoomOut": "Zoom out",
937
+ "otpInput.digit": "Digit {index}",
938
+ "otpInput.label": "One-time code",
939
+ "tagInput.added": "{names} added",
940
+ "tagInput.addedWithNotes": "{names} added ({notes})",
941
+ "tagInput.atCap": "You can add up to {max}",
942
+ "tagInput.duplicate": "{names} already added",
943
+ "tagInput.placeholder": "Type and press Enter",
944
+ "tagInput.removed": "{name} removed",
945
+ "textField.clear": "Clear",
946
+ "textField.passwordHide": "Hide password",
947
+ "textField.passwordShow": "Show password",
948
+ "timePicker.hour": "Hour",
949
+ "timePicker.minute": "Minute",
950
+ "timePicker.rangeSr": "Selectable from {min} to {max}",
951
+ "bottomNav.label": "Main menu",
952
+ "breadcrumb.label": "Breadcrumb",
953
+ "pagination.label": "Pagination",
954
+ "pagination.next": "Next page",
955
+ "pagination.prev": "Previous page",
956
+ "sidebar.toggle": "Toggle sidebar",
957
+ "drawer.close": "Close",
958
+ "modal.close": "Close"
959
+ };
960
+ var catalogs = { ko, en };
961
+ function format(template, vars) {
962
+ if (!vars) return template;
963
+ return template.replace(
964
+ /\{(\w+)\}/g,
965
+ (whole, name) => name in vars ? String(vars[name]) : whole
966
+ );
967
+ }
968
+ function makeText(messages) {
969
+ return (key, vars) => format(messages[key], vars);
970
+ }
971
+ var FALLBACK = { locale: "ko", t: makeText(ko) };
972
+ var LocaleContext = createContext(void 0);
973
+ var LocaleProvider = ({ locale = "ko", messages, children }) => {
974
+ const stableMessages = useStableMessages(messages);
975
+ const value = useMemo(() => {
976
+ const base = catalogs[locale];
977
+ const merged = stableMessages ? { ...base, ...stableMessages } : base;
978
+ return { locale, t: makeText(merged) };
979
+ }, [locale, stableMessages]);
980
+ return /* @__PURE__ */ jsx(LocaleContext.Provider, { value, children });
981
+ };
982
+ function useStableMessages(messages) {
983
+ const ref = useRef(messages);
984
+ const previous = ref.current;
985
+ const same = previous === messages || !!previous && !!messages && Object.keys(previous).length === Object.keys(messages).length && Object.keys(messages).every((key) => previous[key] === messages[key]);
986
+ useEffect(() => {
987
+ if (!same) ref.current = messages;
988
+ }, [same, messages]);
989
+ return same ? previous : messages;
990
+ }
991
+ function useLocaleText() {
992
+ return (useContext(LocaleContext) ?? FALLBACK).t;
993
+ }
994
+ function useLocaleName() {
995
+ return (useContext(LocaleContext) ?? FALLBACK).locale;
996
+ }
582
997
  var DEFAULT_ICON_SIZE = {
583
998
  page: 48,
584
999
  widget: 28
585
1000
  };
586
1001
  var ErrorState = ({
587
- title = "\uBB38\uC81C\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4",
1002
+ title: titleProp,
588
1003
  description,
589
1004
  icon,
590
1005
  action,
@@ -592,6 +1007,8 @@ var ErrorState = ({
592
1007
  className,
593
1008
  ...props
594
1009
  }) => {
1010
+ const t = useLocaleText();
1011
+ const title = titleProp === void 0 ? t("errorState.title") : titleProp;
595
1012
  const resolvedIcon = icon === null ? null : icon ?? /* @__PURE__ */ jsx(TriangleAlert, { size: DEFAULT_ICON_SIZE[variant], strokeWidth: 1.5 });
596
1013
  return /* @__PURE__ */ jsxs(
597
1014
  "div",
@@ -609,15 +1026,17 @@ var ErrorState = ({
609
1026
  );
610
1027
  };
611
1028
  var BottomNav = ({
612
- ariaLabel = "\uC8FC\uC694 \uBA54\uB274",
1029
+ ariaLabel: ariaLabelProp,
613
1030
  className,
614
1031
  children,
615
1032
  ...props
616
1033
  }) => {
1034
+ const t = useLocaleText();
1035
+ const ariaLabel = ariaLabelProp ?? t("bottomNav.label");
617
1036
  return /* @__PURE__ */ jsx("nav", { className: cn("bottom_nav", className), "aria-label": ariaLabel, ...props, children });
618
1037
  };
619
1038
  var BottomNavItem = (props) => {
620
- const { icon, label, active, badge, as = "button", className, disabled, ...rest } = props;
1039
+ const { icon, label, active, badge, as, className, disabled, ref, ...rest } = props;
621
1040
  const classes = cn(
622
1041
  "bottom_nav_item",
623
1042
  active && "bottom_nav_item_active",
@@ -625,6 +1044,8 @@ var BottomNavItem = (props) => {
625
1044
  className
626
1045
  );
627
1046
  const ariaCurrent = active ? "page" : void 0;
1047
+ const anchorRest = rest;
1048
+ const Tag = as ?? (anchorRest.href != null ? "a" : "button");
628
1049
  const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
629
1050
  /* @__PURE__ */ jsxs("span", { className: "bottom_nav_item_icon", "aria-hidden": "true", children: [
630
1051
  icon,
@@ -632,38 +1053,45 @@ var BottomNavItem = (props) => {
632
1053
  ] }),
633
1054
  /* @__PURE__ */ jsx("span", { className: "bottom_nav_item_label", children: label })
634
1055
  ] });
635
- if (as === "a") {
636
- const { href, onClick: onClick2, ...anchorRest } = rest;
1056
+ if (Tag === "button") {
1057
+ const {
1058
+ type,
1059
+ onClick: onClick2,
1060
+ href: _href,
1061
+ ...buttonRest
1062
+ } = rest;
637
1063
  return /* @__PURE__ */ jsx(
638
- "a",
1064
+ "button",
639
1065
  {
1066
+ ref,
1067
+ type: type ?? "button",
640
1068
  className: classes,
641
- href,
1069
+ disabled,
642
1070
  "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,
1071
+ onClick: onClick2,
1072
+ ...buttonRest,
653
1073
  children: content
654
1074
  }
655
1075
  );
656
1076
  }
657
- const { type, onClick, ...buttonRest } = rest;
1077
+ const { onClick, tabIndex, ...tagProps } = anchorRest;
658
1078
  return /* @__PURE__ */ jsx(
659
- "button",
1079
+ Tag,
660
1080
  {
661
- type: type ?? "button",
1081
+ ...tagProps,
1082
+ ref,
662
1083
  className: classes,
663
- disabled,
664
1084
  "aria-current": ariaCurrent,
665
- onClick,
666
- ...buttonRest,
1085
+ "aria-disabled": disabled ? "true" : void 0,
1086
+ tabIndex: disabled ? -1 : tabIndex,
1087
+ onClick: (event) => {
1088
+ if (disabled) {
1089
+ event.preventDefault();
1090
+ event.stopPropagation();
1091
+ return;
1092
+ }
1093
+ onClick?.(event);
1094
+ },
667
1095
  children: content
668
1096
  }
669
1097
  );
@@ -674,10 +1102,12 @@ var BottomNavSpacer = ({ className, ...props }) => {
674
1102
  var Breadcrumb = ({
675
1103
  items,
676
1104
  separator,
677
- navLabel = "\uD604\uC7AC \uC704\uCE58",
1105
+ navLabel: navLabelProp,
678
1106
  className,
679
1107
  ...props
680
1108
  }) => {
1109
+ const t = useLocaleText();
1110
+ const navLabel = navLabelProp ?? t("breadcrumb.label");
681
1111
  const sep = separator ?? /* @__PURE__ */ jsx(ChevronRight, { size: iconSize.xs, "aria-hidden": "true" });
682
1112
  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
1113
  const isLast = idx === items.length - 1;
@@ -1018,7 +1448,7 @@ var Sidebar = ({
1018
1448
  defaultCollapsed = false,
1019
1449
  onCollapsedChange,
1020
1450
  collapsible = true,
1021
- toggleLabel = "\uC0AC\uC774\uB4DC\uBC14 \uD1A0\uAE00",
1451
+ toggleLabel: toggleLabelProp,
1022
1452
  width = 240,
1023
1453
  collapsedWidth = 64,
1024
1454
  mode = "auto",
@@ -1027,6 +1457,8 @@ var Sidebar = ({
1027
1457
  style,
1028
1458
  ...props
1029
1459
  }) => {
1460
+ const t = useLocaleText();
1461
+ const toggleLabel = toggleLabelProp ?? t("sidebar.toggle");
1030
1462
  const isControlled = collapsedProp !== void 0;
1031
1463
  const [internalCollapsed, setInternalCollapsed] = React11.useState(defaultCollapsed);
1032
1464
  const collapsed = isControlled ? collapsedProp : internalCollapsed;
@@ -1070,20 +1502,61 @@ var Sidebar = ({
1070
1502
  );
1071
1503
  };
1072
1504
  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);
1505
+ const { icon, active, trailing, as, className, children, disabled, ref, ...rest } = props;
1506
+ const classes = cn(
1507
+ "sidebar_item",
1508
+ active && "sidebar_item_active",
1509
+ disabled && "sidebar_item_disabled",
1510
+ className
1511
+ );
1075
1512
  const ariaCurrent = active ? "page" : void 0;
1513
+ const anchorRest = rest;
1514
+ const Tag = as ?? (anchorRest.href != null ? "a" : "button");
1076
1515
  const inner = /* @__PURE__ */ jsxs(Fragment$1, { children: [
1077
1516
  icon && /* @__PURE__ */ jsx("span", { className: "sidebar_item_icon", "aria-hidden": "true", children: icon }),
1078
1517
  /* @__PURE__ */ jsx("span", { className: "sidebar_item_label", children }),
1079
1518
  trailing && /* @__PURE__ */ jsx("span", { className: "sidebar_item_trailing", children: trailing })
1080
1519
  ] });
1081
- if (as === "a") {
1082
- const { href, ...anchorRest } = rest;
1083
- return /* @__PURE__ */ jsx("a", { className: classes, href, "aria-current": ariaCurrent, ...anchorRest, children: inner });
1520
+ if (Tag === "button") {
1521
+ const {
1522
+ type,
1523
+ href: _href,
1524
+ ...buttonRest
1525
+ } = rest;
1526
+ return /* @__PURE__ */ jsx(
1527
+ "button",
1528
+ {
1529
+ ref,
1530
+ type: type ?? "button",
1531
+ className: classes,
1532
+ disabled,
1533
+ "aria-current": ariaCurrent,
1534
+ ...buttonRest,
1535
+ children: inner
1536
+ }
1537
+ );
1084
1538
  }
1085
- const { type, ...buttonRest } = rest;
1086
- return /* @__PURE__ */ jsx("button", { type: type ?? "button", className: classes, "aria-current": ariaCurrent, ...buttonRest, children: inner });
1539
+ const { onClick, tabIndex, ...tagProps } = anchorRest;
1540
+ return /* @__PURE__ */ jsx(
1541
+ Tag,
1542
+ {
1543
+ ...tagProps,
1544
+ ref,
1545
+ className: classes,
1546
+ "aria-current": ariaCurrent,
1547
+ "aria-disabled": disabled || void 0,
1548
+ tabIndex: disabled ? -1 : tabIndex,
1549
+ onClick: (event) => {
1550
+ if (disabled) {
1551
+ event.preventDefault();
1552
+ event.stopPropagation();
1553
+ return;
1554
+ }
1555
+ onClick?.(event);
1556
+ },
1557
+ children: inner
1558
+ }
1559
+ );
1087
1560
  };
1088
1561
  var SidebarSection = ({ label, className, children, ...props }) => {
1089
1562
  return /* @__PURE__ */ jsxs("div", { className: cn("sidebar_section", className), ...props, children: [
@@ -2103,7 +2576,8 @@ var Chip = ({
2103
2576
  className,
2104
2577
  ...props
2105
2578
  }) => {
2106
- const removeAriaLabel = removeLabel ?? `${label} \uC81C\uAC70`;
2579
+ const t = useLocaleText();
2580
+ const removeAriaLabel = removeLabel ?? t("chip.remove", { label: String(label) });
2107
2581
  const [iconHovered, setIconHovered] = useState(false);
2108
2582
  const isStatic = type === "static";
2109
2583
  const hasLeading = !isStatic && selected;
@@ -2200,252 +2674,382 @@ var Chip = ({
2200
2674
  )
2201
2675
  ] });
2202
2676
  };
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
- );
2677
+ var FormContext = createContext(void 0);
2678
+ function useFormError(name) {
2679
+ return useContext(FormContext)?.errors?.[name];
2680
+ }
2681
+ var Form = ({ errors, onSubmit, children, className, ...props }) => /* @__PURE__ */ jsx(FormContext.Provider, { value: { errors }, children: /* @__PURE__ */ jsx(
2682
+ "form",
2683
+ {
2684
+ className: cn("form", className),
2685
+ onSubmit: (event) => {
2686
+ event.preventDefault();
2687
+ onSubmit?.(event);
2688
+ },
2689
+ ...props,
2690
+ children
2255
2691
  }
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,
2692
+ ) });
2693
+ var FormActions = ({ align = "end", children, className, ...props }) => /* @__PURE__ */ jsx("div", { className: cn("form_actions", `form_actions_${align}`, className), ...props, children });
2694
+ FormActions.displayName = "Form.Actions";
2695
+ Form.Actions = FormActions;
2696
+ var FieldContext = createContext(void 0);
2697
+ function useFieldControl() {
2698
+ return useContext(FieldContext);
2699
+ }
2700
+ var Field = ({
2701
+ name,
2702
+ label,
2703
+ required = false,
2704
+ help,
2705
+ error: errorProp,
2285
2706
  children,
2286
2707
  className,
2287
- style,
2288
2708
  ...props
2289
2709
  }) => {
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
- ] })
2710
+ const generatedId = useId();
2711
+ const inputId = `${name}-${generatedId}`;
2712
+ const formError = useFormError(name);
2713
+ const error = errorProp ?? formError;
2714
+ const labelId = label ? `${inputId}-label` : void 0;
2715
+ const helpId = help ? `${inputId}-help` : void 0;
2716
+ const errorId = error ? `${inputId}-error` : void 0;
2717
+ const showHelp = !error && !!help;
2718
+ const control = {
2719
+ inputId,
2720
+ labelId,
2721
+ describedBy: [errorId, showHelp ? helpId : void 0].filter(Boolean).join(" ") || void 0,
2722
+ invalid: !!error,
2723
+ required
2724
+ };
2725
+ return /* @__PURE__ */ jsxs("div", { className: cn("field", !!error && "field_error", className), ...props, children: [
2726
+ label && /* @__PURE__ */ jsxs("label", { id: labelId, htmlFor: inputId, className: "field_label", children: [
2727
+ label,
2728
+ required && /* @__PURE__ */ jsx("span", { className: "field_required", "aria-hidden": "true", children: "*" })
2729
+ ] }),
2730
+ /* @__PURE__ */ jsx(FieldContext.Provider, { value: control, children }),
2731
+ showHelp && /* @__PURE__ */ jsx("div", { id: helpId, className: "field_help", children: help }),
2732
+ error && /* @__PURE__ */ jsx("div", { id: errorId, className: "field_message", children: error })
2316
2733
  ] });
2317
2734
  };
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,
2735
+ var ClearIcon = () => /* @__PURE__ */ jsx(X, { size: iconSize.lg, "aria-hidden": "true" });
2736
+ var TextField = ({
2737
+ id,
2332
2738
  label,
2739
+ showLabel = true,
2333
2740
  supportingText,
2334
- metadata,
2335
- leadingElement,
2336
- trailingElement,
2337
- alignment,
2338
- disabled,
2339
- selected,
2340
- onClick,
2741
+ error,
2742
+ success,
2743
+ identifier,
2744
+ leadingIcon,
2745
+ trailingIcon,
2746
+ leadingAction,
2747
+ trailingAction,
2748
+ showPasswordToggle,
2749
+ passwordToggleLabels,
2750
+ clearable,
2751
+ clearLabel: clearLabelProp,
2752
+ type,
2753
+ fullWidth,
2754
+ size = "md",
2755
+ variant = "outline",
2341
2756
  className,
2757
+ onValueChange,
2758
+ onChangeAction,
2759
+ imeStrategy = "delayed",
2760
+ value,
2761
+ defaultValue,
2762
+ transformValue,
2763
+ ref,
2342
2764
  ...props
2343
2765
  }) => {
2344
- const isOneLine = !overline && !supportingText && !metadata;
2345
- const effectiveAlignment = alignment ?? (isOneLine ? "middle" : "top");
2766
+ const t = useLocaleText();
2767
+ const clearLabel = clearLabelProp ?? t("textField.clear");
2768
+ const generatedId = useId();
2769
+ const field = useFieldControl();
2770
+ const inputId = id ?? field?.inputId ?? generatedId;
2771
+ const helperId = supportingText ? `${inputId}-help` : void 0;
2772
+ const describedBy = field?.describedBy ?? helperId;
2773
+ const isControlled = value !== void 0;
2774
+ const applyTransform = (nextValue) => transformValue ? transformValue(nextValue) : nextValue;
2775
+ const [innerValue, setInnerValue] = useState(() => applyTransform(value ?? defaultValue ?? ""));
2776
+ const isComposingRef = useRef(false);
2777
+ const lastEmittedValueRef = useRef(innerValue);
2778
+ const [prevValue, setPrevValue] = useState(value);
2779
+ if (isControlled && value !== prevValue && !isComposingRef.current) {
2780
+ setPrevValue(value);
2781
+ const nextValue = applyTransform(value ?? "");
2782
+ setInnerValue(nextValue);
2783
+ lastEmittedValueRef.current = nextValue;
2784
+ }
2785
+ const emit = useCallback(
2786
+ (nextValue) => {
2787
+ setInnerValue(nextValue);
2788
+ if (nextValue !== lastEmittedValueRef.current) {
2789
+ lastEmittedValueRef.current = nextValue;
2790
+ (onValueChange ?? onChangeAction)?.(nextValue);
2791
+ }
2792
+ },
2793
+ [onValueChange, onChangeAction]
2794
+ );
2795
+ const handleClear = useCallback(() => {
2796
+ emit("");
2797
+ }, [emit]);
2798
+ const [passwordRevealed, setPasswordRevealed] = useState(false);
2799
+ const togglePassword = useCallback(() => {
2800
+ setPasswordRevealed((revealed) => !revealed);
2801
+ }, []);
2802
+ let resolvedType = type;
2803
+ if (showPasswordToggle) {
2804
+ resolvedType = passwordRevealed ? "text" : type ?? "password";
2805
+ }
2806
+ const isError = !!error || !!field?.invalid;
2807
+ const isSuccess = !!success && !isError;
2346
2808
  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",
2809
+ "text_field",
2810
+ `text_field_variant_${variant}`,
2811
+ size === "sm" && "text_field_size_sm",
2812
+ size === "lg" && "text_field_size_lg",
2813
+ fullWidth && "text_field_full_width",
2814
+ isError && "text_field_error",
2815
+ isSuccess && "text_field_success",
2816
+ props.disabled && "text_field_disabled",
2352
2817
  className
2353
2818
  );
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
- );
2819
+ const passwordToggleLabel = passwordRevealed ? passwordToggleLabels?.hide ?? t("textField.passwordHide") : passwordToggleLabels?.show ?? t("textField.passwordShow");
2820
+ const resolvedTrailing = showPasswordToggle ? /* @__PURE__ */ jsx("span", { className: "text_field_icon text_field_action", children: /* @__PURE__ */ jsx(
2821
+ "button",
2822
+ {
2823
+ type: "button",
2824
+ onClick: togglePassword,
2825
+ "aria-label": passwordToggleLabel,
2826
+ disabled: props.disabled,
2827
+ children: passwordRevealed ? /* @__PURE__ */ jsx(EyeOff, { size: iconSize.lg, "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Eye, { size: iconSize.lg, "aria-hidden": "true" })
2828
+ }
2829
+ ) }) : clearable && innerValue ? /* @__PURE__ */ jsx(
2830
+ "button",
2831
+ {
2832
+ type: "button",
2833
+ className: "text_field_clear",
2834
+ onClick: handleClear,
2835
+ "aria-label": clearLabel,
2836
+ disabled: props.disabled,
2837
+ children: /* @__PURE__ */ jsx(ClearIcon, {})
2838
+ }
2839
+ ) : 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;
2840
+ 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;
2841
+ return /* @__PURE__ */ jsxs("div", { className: rootClassName, children: [
2842
+ label && showLabel && /* @__PURE__ */ jsx("label", { htmlFor: inputId, className: "text_field_label", children: label }),
2843
+ /* @__PURE__ */ jsx("div", { className: "text_field_container", children: /* @__PURE__ */ jsxs("div", { className: "text_field_inner", children: [
2844
+ resolvedLeading,
2845
+ /* @__PURE__ */ jsx(
2846
+ "div",
2847
+ {
2848
+ className: cn(
2849
+ "text_field_input_wrap",
2850
+ resolvedTrailing && "text_field_input_wrap_no_pad_right"
2851
+ ),
2852
+ children: /* @__PURE__ */ jsx(
2853
+ "input",
2854
+ {
2855
+ id: inputId,
2856
+ ref,
2857
+ className: cn("text_field_input", identifier && "text_field_input_identifier"),
2858
+ "aria-invalid": isError,
2859
+ "aria-describedby": describedBy,
2860
+ "aria-required": field?.required || void 0,
2861
+ "aria-label": !showLabel ? label : void 0,
2862
+ ...props,
2863
+ type: resolvedType,
2864
+ value: innerValue,
2865
+ onCompositionStart: () => {
2866
+ isComposingRef.current = true;
2867
+ },
2868
+ onCompositionEnd: (event) => {
2869
+ isComposingRef.current = false;
2870
+ emit(applyTransform(event.currentTarget.value));
2871
+ },
2872
+ onChange: (event) => {
2873
+ const rawValue = event.target.value;
2874
+ if (isComposingRef.current) {
2875
+ setInnerValue(rawValue);
2876
+ if (imeStrategy === "immediate" && rawValue !== lastEmittedValueRef.current) {
2877
+ lastEmittedValueRef.current = rawValue;
2878
+ (onValueChange ?? onChangeAction)?.(rawValue);
2879
+ }
2880
+ return;
2881
+ }
2882
+ emit(applyTransform(rawValue));
2883
+ }
2884
+ }
2885
+ )
2886
+ }
2887
+ ),
2888
+ resolvedTrailing
2889
+ ] }) }),
2890
+ supportingText && /* @__PURE__ */ jsx("div", { id: helperId, className: "text_field_helper", children: supportingText })
2891
+ ] });
2386
2892
  };
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",
2893
+ TextField.displayName = "TextField";
2894
+ var Button = (props) => {
2895
+ const {
2896
+ variant = "filled",
2897
+ size = "md",
2898
+ leadingIcon,
2899
+ trailingIcon,
2900
+ fullWidth = false,
2901
+ radius: radius2,
2902
+ danger = false,
2903
+ disabled = false,
2904
+ as,
2905
+ className,
2906
+ children,
2907
+ ref,
2908
+ ...rest
2909
+ } = props;
2910
+ const buttonClassName = cn(
2911
+ "button",
2912
+ `button_variant_${variant}`,
2913
+ `button_size_${size}`,
2914
+ fullWidth && "button_full_width",
2915
+ radius2 && `button_radius_${radius2}`,
2916
+ danger && "button_danger",
2917
+ // anchor 엔 native :disabled 가 안 먹으므로 클래스로 비활성 스타일 적용 (button 도 무해)
2918
+ disabled && "button_disabled",
2408
2919
  className
2409
2920
  );
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
- ] })
2921
+ const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
2922
+ leadingIcon && /* @__PURE__ */ jsx("span", { className: "button_icon", "aria-hidden": "true", children: leadingIcon }),
2923
+ children && /* @__PURE__ */ jsx("span", { className: "button_label", children }),
2924
+ trailingIcon && /* @__PURE__ */ jsx("span", { className: "button_icon", "aria-hidden": "true", children: trailingIcon })
2424
2925
  ] });
2926
+ const anchorRest = rest;
2927
+ const Tag = as ?? (anchorRest.href != null ? "a" : "button");
2928
+ if (Tag === "button") {
2929
+ const {
2930
+ type = "button",
2931
+ href: _href,
2932
+ ...buttonRest
2933
+ } = rest;
2934
+ return /* @__PURE__ */ jsx(
2935
+ "button",
2936
+ {
2937
+ ref,
2938
+ type,
2939
+ disabled,
2940
+ className: buttonClassName,
2941
+ ...buttonRest,
2942
+ children: content
2943
+ }
2944
+ );
2945
+ }
2946
+ const { onClick, tabIndex, ...tagProps } = anchorRest;
2947
+ return /* @__PURE__ */ jsx(
2948
+ Tag,
2949
+ {
2950
+ ...tagProps,
2951
+ ref,
2952
+ className: buttonClassName,
2953
+ "aria-disabled": disabled || void 0,
2954
+ tabIndex: disabled ? -1 : tabIndex,
2955
+ onClick: disabled ? (event) => {
2956
+ event.preventDefault();
2957
+ event.stopPropagation();
2958
+ } : onClick,
2959
+ children: content
2960
+ }
2961
+ );
2425
2962
  };
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");
2963
+ var range = (start, end) => {
2964
+ const out = [];
2965
+ for (let i = start; i <= end; i += 1) out.push(i);
2966
+ return out;
2967
+ };
2968
+ var getPaginationItems = (page, totalPages) => {
2969
+ if (totalPages <= 7) return range(1, totalPages);
2970
+ const items = [];
2971
+ const last = totalPages;
2972
+ const sibling = 2;
2973
+ if (page <= sibling + 2) {
2974
+ for (const p of range(1, sibling + 3)) items.push(p);
2975
+ items.push("ellipsis");
2976
+ items.push(last);
2977
+ return items;
2978
+ }
2979
+ if (page >= last - sibling - 1) {
2980
+ items.push(1);
2981
+ items.push("ellipsis");
2982
+ for (const p of range(last - sibling - 2, last)) items.push(p);
2983
+ return items;
2984
+ }
2985
+ items.push(1);
2986
+ items.push("ellipsis");
2987
+ for (const p of range(page - sibling, page + sibling)) items.push(p);
2988
+ items.push("ellipsis");
2989
+ items.push(last);
2990
+ return items;
2991
+ };
2992
+ var Pagination = ({
2993
+ page,
2994
+ totalPages,
2995
+ onPageChange,
2996
+ onChange,
2997
+ prevLabel: prevLabelProp,
2998
+ nextLabel: nextLabelProp,
2999
+ navLabel: navLabelProp
3000
+ }) => {
3001
+ const t = useLocaleText();
3002
+ const prevLabel = prevLabelProp ?? t("pagination.prev");
3003
+ const nextLabel = nextLabelProp ?? t("pagination.next");
3004
+ const navLabel = navLabelProp ?? t("pagination.label");
3005
+ const emit = onPageChange ?? onChange;
3006
+ const prevDisabled = page <= 1;
3007
+ const nextDisabled = page >= totalPages;
3008
+ const items = React11.useMemo(() => getPaginationItems(page, totalPages), [page, totalPages]);
3009
+ return /* @__PURE__ */ jsxs("nav", { className: "pagination", "aria-label": navLabel, children: [
3010
+ /* @__PURE__ */ jsx(
3011
+ "button",
3012
+ {
3013
+ type: "button",
3014
+ className: "pagination_item",
3015
+ onClick: () => emit?.(page - 1),
3016
+ disabled: prevDisabled,
3017
+ "aria-label": prevLabel,
3018
+ children: "\u2039"
2437
3019
  }
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 });
3020
+ ),
3021
+ /* @__PURE__ */ jsx("ul", { className: "pagination_pages", children: items.map((it, idx) => {
3022
+ if (it === "ellipsis") {
3023
+ const prev = items[idx - 1];
3024
+ const next = items[idx + 1];
3025
+ return /* @__PURE__ */ jsx("li", { className: "pagination_ellipsis", "aria-hidden": "true", children: "\u2026" }, `e-${prev}-${next}`);
3026
+ }
3027
+ const isActive = it === page;
3028
+ const buttonClassName = cn("pagination_page_button", { pagination_active: isActive });
3029
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
3030
+ "button",
3031
+ {
3032
+ type: "button",
3033
+ className: buttonClassName,
3034
+ onClick: () => emit?.(it),
3035
+ "aria-current": isActive ? "page" : void 0,
3036
+ children: it
3037
+ }
3038
+ ) }, it);
3039
+ }) }),
3040
+ /* @__PURE__ */ jsx(
3041
+ "button",
3042
+ {
3043
+ type: "button",
3044
+ className: "pagination_item",
3045
+ onClick: () => emit?.(page + 1),
3046
+ disabled: nextDisabled,
3047
+ "aria-label": nextLabel,
3048
+ children: "\u203A"
3049
+ }
3050
+ )
3051
+ ] });
2447
3052
  };
2448
- Prose.displayName = "Prose";
2449
3053
  var Skeleton = ({
2450
3054
  variant = "text",
2451
3055
  width,
@@ -2496,6 +3100,7 @@ var Checkbox = ({
2496
3100
  props.disabled && "checkbox_disabled",
2497
3101
  className
2498
3102
  );
3103
+ const field = useFieldControl();
2499
3104
  return /* @__PURE__ */ jsxs("label", { className: rootClassName, children: [
2500
3105
  /* @__PURE__ */ jsx(
2501
3106
  "input",
@@ -2504,6 +3109,9 @@ var Checkbox = ({
2504
3109
  ref: inputRef,
2505
3110
  type: "checkbox",
2506
3111
  className: "checkbox_input",
3112
+ id: field?.inputId ?? props.id,
3113
+ "aria-describedby": field?.describedBy ?? props["aria-describedby"],
3114
+ "aria-required": field?.required || void 0,
2507
3115
  "aria-invalid": error || void 0
2508
3116
  }
2509
3117
  ),
@@ -2516,7 +3124,7 @@ var Table = ({
2516
3124
  columns,
2517
3125
  data,
2518
3126
  keyExtractor,
2519
- emptyMessage = "\uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
3127
+ emptyMessage: emptyMessageProp,
2520
3128
  isLoading = false,
2521
3129
  skeletonRows = 5,
2522
3130
  size = "md",
@@ -2525,16 +3133,21 @@ var Table = ({
2525
3133
  ariaLabel,
2526
3134
  className,
2527
3135
  onRowClick,
2528
- rowClickHint = "\uD074\uB9AD \uAC00\uB2A5\uD55C \uD589",
3136
+ rowClickHint: rowClickHintProp,
2529
3137
  sort,
2530
3138
  onSortChange,
2531
- selectAllAriaLabel = "\uC804\uCCB4 \uC120\uD0DD",
2532
- selectRowAriaLabel = (index) => `${index + 1}\uBC88\uC9F8 \uD589 \uC120\uD0DD`,
3139
+ selectAllAriaLabel: selectAllAriaLabelProp,
3140
+ selectRowAriaLabel: selectRowAriaLabelProp,
2533
3141
  selectable = false,
2534
3142
  rowKey,
2535
3143
  selectedKeys,
2536
3144
  onSelectionChange
2537
3145
  }) => {
3146
+ const t = useLocaleText();
3147
+ const rowClickHint = rowClickHintProp ?? t("table.rowClickHint");
3148
+ const emptyMessage = emptyMessageProp === void 0 ? t("table.empty") : emptyMessageProp;
3149
+ const selectAllAriaLabel = selectAllAriaLabelProp ?? t("table.selectAll");
3150
+ const selectRowAriaLabel = selectRowAriaLabelProp ?? ((index) => t("table.selectRow", { index: index + 1 }));
2538
3151
  const wrapperClassName = cn(
2539
3152
  "table_wrapper",
2540
3153
  `table_size_${size}`,
@@ -2702,10 +3315,312 @@ var Table = ({
2702
3315
  );
2703
3316
  }) })
2704
3317
  ] }),
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 })
3318
+ onRowClick && rowClickHint && /* @__PURE__ */ jsx("span", { id: rowClickHintId, className: "table_sr_only", children: rowClickHint }),
3319
+ isEmpty && /* @__PURE__ */ jsx("div", { className: "table_empty", role: "status", children: emptyMessage })
3320
+ ] });
3321
+ };
3322
+ var DataView = ({
3323
+ query,
3324
+ columns,
3325
+ rowKey,
3326
+ toolbar,
3327
+ selectionActions,
3328
+ pagination,
3329
+ empty,
3330
+ sort,
3331
+ onSortChange,
3332
+ onRowClick,
3333
+ ariaLabel,
3334
+ selectionSummary: selectionSummaryProp,
3335
+ clearSelectionLabel: clearSelectionLabelProp,
3336
+ errorTitle: errorTitleProp,
3337
+ retryLabel: retryLabelProp,
3338
+ className,
3339
+ ...props
3340
+ }) => {
3341
+ const t = useLocaleText();
3342
+ const selectionSummary = selectionSummaryProp ?? ((count) => t("dataView.selectionSummary", { count }));
3343
+ const clearSelectionLabel = clearSelectionLabelProp ?? t("dataView.clearSelection");
3344
+ const errorTitle = errorTitleProp ?? t("dataView.errorTitle");
3345
+ const retryLabel = retryLabelProp ?? t("dataView.retry");
3346
+ const searchLabel = toolbar?.searchPlaceholder ?? t("dataView.search");
3347
+ const [selectedKeys, setSelectedKeys] = useState([]);
3348
+ const selectionBarId = useId();
3349
+ const selectable = !!selectionActions?.length;
3350
+ const rows = query.data ?? [];
3351
+ const showEmpty = !query.isLoading && !query.error && rows.length === 0;
3352
+ if (query.error) {
3353
+ return /* @__PURE__ */ jsx("div", { className: cn("data_view", className), ...props, children: /* @__PURE__ */ jsx(
3354
+ ErrorState,
3355
+ {
3356
+ variant: "widget",
3357
+ title: errorTitle,
3358
+ action: query.refetch ? /* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", onClick: query.refetch, children: retryLabel }) : void 0
3359
+ }
3360
+ ) });
3361
+ }
3362
+ return /* @__PURE__ */ jsxs("div", { className: cn("data_view", className), ...props, children: [
3363
+ toolbar && /* @__PURE__ */ jsxs("div", { className: "data_view_toolbar", children: [
3364
+ toolbar.search && /* @__PURE__ */ jsx("div", { className: "data_view_search", children: /* @__PURE__ */ jsx(
3365
+ TextField,
3366
+ {
3367
+ fullWidth: true,
3368
+ size: "sm",
3369
+ type: "search",
3370
+ value: toolbar.searchValue,
3371
+ onValueChange: toolbar.onSearchChange,
3372
+ placeholder: searchLabel,
3373
+ "aria-label": searchLabel,
3374
+ leadingIcon: /* @__PURE__ */ jsx(Search, { size: iconSize.sm })
3375
+ }
3376
+ ) }),
3377
+ toolbar.filters && /* @__PURE__ */ jsx("div", { className: "data_view_filters", children: toolbar.filters })
3378
+ ] }),
3379
+ selectable && selectedKeys.length > 0 && // `role="status"` - 선택이 바뀔 때마다 스크린리더가 개수를 읽는다. 액션 줄이
3380
+ // 시각적으로만 나타나면 키보드 사용자는 무엇이 가능해졌는지 알 수 없다.
3381
+ /* @__PURE__ */ jsxs("div", { id: selectionBarId, className: "data_view_selection", role: "status", children: [
3382
+ /* @__PURE__ */ jsx("span", { className: "data_view_selection_count", children: selectionSummary(selectedKeys.length) }),
3383
+ /* @__PURE__ */ jsxs("div", { className: "data_view_selection_actions", children: [
3384
+ selectionActions?.map((action) => /* @__PURE__ */ jsx(
3385
+ Button,
3386
+ {
3387
+ size: "sm",
3388
+ variant: "outline",
3389
+ danger: action.danger,
3390
+ onClick: () => action.onRun(selectedKeys),
3391
+ children: action.label
3392
+ },
3393
+ action.label
3394
+ )),
3395
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "text", onClick: () => setSelectedKeys([]), children: clearSelectionLabel })
3396
+ ] })
3397
+ ] }),
3398
+ showEmpty ? empty ?? /* @__PURE__ */ jsx(EmptyState, { title: t("dataView.empty") }) : selectable ? (
3399
+ // 판별 union 이라 조건부 스프레드로는 좁혀지지 않는다 - 분기를 명시한다.
3400
+ /* @__PURE__ */ jsx(
3401
+ Table,
3402
+ {
3403
+ columns,
3404
+ data: rows,
3405
+ keyExtractor: rowKey,
3406
+ isLoading: query.isLoading,
3407
+ sort,
3408
+ onSortChange,
3409
+ onRowClick,
3410
+ ariaLabel,
3411
+ selectable: true,
3412
+ rowKey,
3413
+ selectedKeys,
3414
+ onSelectionChange: setSelectedKeys
3415
+ }
3416
+ )
3417
+ ) : /* @__PURE__ */ jsx(
3418
+ Table,
3419
+ {
3420
+ columns,
3421
+ data: rows,
3422
+ keyExtractor: rowKey,
3423
+ isLoading: query.isLoading,
3424
+ sort,
3425
+ onSortChange,
3426
+ onRowClick,
3427
+ ariaLabel
3428
+ }
3429
+ ),
3430
+ pagination && pagination.totalPages > 1 && /* @__PURE__ */ jsx("div", { className: "data_view_pagination", children: /* @__PURE__ */ jsx(
3431
+ Pagination,
3432
+ {
3433
+ page: pagination.page,
3434
+ totalPages: pagination.totalPages,
3435
+ onPageChange: pagination.onPageChange
3436
+ }
3437
+ ) })
3438
+ ] });
3439
+ };
3440
+ var Divider = ({ weight = "standard", className, ...props }) => {
3441
+ const dividerClassName = cn("divider", `divider_weight_${weight}`, className);
3442
+ return /* @__PURE__ */ jsx("hr", { className: dividerClassName, ...props });
3443
+ };
3444
+ var HeroActionButton = ({
3445
+ action,
3446
+ variant
3447
+ }) => 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 });
3448
+ var Hero = ({
3449
+ height = "md",
3450
+ align = "left",
3451
+ backgroundImage,
3452
+ backgroundColor,
3453
+ overlay,
3454
+ title,
3455
+ subtitle,
3456
+ eyebrow,
3457
+ textColor = "auto",
3458
+ primaryAction,
3459
+ secondaryAction,
3460
+ children,
3461
+ className,
3462
+ style,
3463
+ ...props
3464
+ }) => {
3465
+ const resolvedOverlay = overlay === true ? "dark" : overlay;
3466
+ const isDarkOverlay = resolvedOverlay === "dark";
3467
+ const resolvedTextColor = textColor === "auto" ? isDarkOverlay || backgroundImage && !resolvedOverlay ? "inverse" : "default" : textColor;
3468
+ const heroClassName = cn(
3469
+ "hero",
3470
+ `hero_height_${height}`,
3471
+ `hero_align_${align}`,
3472
+ resolvedOverlay && `hero_overlay_${resolvedOverlay}`,
3473
+ `hero_text_${resolvedTextColor}`,
3474
+ className
3475
+ );
3476
+ const inlineStyle = { ...style };
3477
+ if (backgroundImage) inlineStyle.backgroundImage = `url("${backgroundImage}")`;
3478
+ if (backgroundColor) inlineStyle.backgroundColor = backgroundColor;
3479
+ return /* @__PURE__ */ jsxs("section", { className: heroClassName, style: inlineStyle, ...props, children: [
3480
+ resolvedOverlay && /* @__PURE__ */ jsx("div", { className: "hero_overlay", "aria-hidden": "true" }),
3481
+ /* @__PURE__ */ jsxs("div", { className: "hero_content", children: [
3482
+ eyebrow && /* @__PURE__ */ jsx("div", { className: "hero_eyebrow", children: eyebrow }),
3483
+ title && /* @__PURE__ */ jsx("h1", { className: "hero_title", children: title }),
3484
+ subtitle && /* @__PURE__ */ jsx("p", { className: "hero_subtitle", children: subtitle }),
3485
+ (primaryAction || secondaryAction || children) && /* @__PURE__ */ jsxs("div", { className: "hero_actions", children: [
3486
+ primaryAction && /* @__PURE__ */ jsx(HeroActionButton, { action: primaryAction, variant: "filled" }),
3487
+ secondaryAction && /* @__PURE__ */ jsx(HeroActionButton, { action: secondaryAction, variant: "outline" }),
3488
+ children
3489
+ ] })
3490
+ ] })
3491
+ ] });
3492
+ };
3493
+ var Icon = ({ icon: IconComponent, ...props }) => {
3494
+ const hasLabel = !!props["aria-label"];
3495
+ return /* @__PURE__ */ jsx(
3496
+ IconComponent,
3497
+ {
3498
+ "aria-hidden": hasLabel ? void 0 : true,
3499
+ focusable: hasLabel ? void 0 : false,
3500
+ ...props
3501
+ }
3502
+ );
3503
+ };
3504
+ Icon.displayName = "Icon";
3505
+ var ListItem = ({
3506
+ overline,
3507
+ label,
3508
+ supportingText,
3509
+ metadata,
3510
+ leadingElement,
3511
+ trailingElement,
3512
+ alignment,
3513
+ disabled,
3514
+ selected,
3515
+ onClick,
3516
+ className,
3517
+ ...props
3518
+ }) => {
3519
+ const isOneLine = !overline && !supportingText && !metadata;
3520
+ const effectiveAlignment = alignment ?? (isOneLine ? "middle" : "top");
3521
+ const rootClassName = cn(
3522
+ "list_item",
3523
+ `list_item_align_${effectiveAlignment}`,
3524
+ disabled && "list_item_disabled",
3525
+ selected && "list_item_selected",
3526
+ onClick && "list_item_interactive",
3527
+ className
3528
+ );
3529
+ return (
3530
+ // biome-ignore lint/a11y/noStaticElementInteractions: optional interactive list item - role=button + tabIndex set conditionally based on onClick
3531
+ /* @__PURE__ */ jsx(
3532
+ "div",
3533
+ {
3534
+ className: rootClassName,
3535
+ onClick: disabled ? void 0 : onClick,
3536
+ onKeyDown: (e) => {
3537
+ if (disabled || !onClick) return;
3538
+ if (e.key === "Enter" || e.key === " ") {
3539
+ e.preventDefault();
3540
+ e.currentTarget.click();
3541
+ }
3542
+ },
3543
+ role: onClick ? "button" : void 0,
3544
+ tabIndex: onClick && !disabled ? 0 : void 0,
3545
+ "aria-disabled": disabled || void 0,
3546
+ "aria-pressed": onClick && selected !== void 0 ? selected : void 0,
3547
+ ...props,
3548
+ children: /* @__PURE__ */ jsxs("div", { className: "list_item_state_layer", children: [
3549
+ leadingElement && /* @__PURE__ */ jsx("div", { className: "list_item_leading", children: leadingElement }),
3550
+ /* @__PURE__ */ jsxs("div", { className: "list_item_content", children: [
3551
+ overline && /* @__PURE__ */ jsx("div", { className: "list_item_overline", children: overline }),
3552
+ /* @__PURE__ */ jsx("div", { className: "list_item_label", children: label }),
3553
+ supportingText && /* @__PURE__ */ jsx("div", { className: "list_item_supporting", children: supportingText }),
3554
+ metadata && /* @__PURE__ */ jsx("div", { className: "list_item_metadata", children: metadata })
3555
+ ] }),
3556
+ trailingElement && /* @__PURE__ */ jsx("div", { className: "list_item_trailing", children: trailingElement })
3557
+ ] })
3558
+ }
3559
+ )
3560
+ );
3561
+ };
3562
+ var MediaCard = ({
3563
+ image,
3564
+ imagePosition = "top",
3565
+ aspectRatio,
3566
+ heading,
3567
+ headingAs: HeadingTag = "h3",
3568
+ eyebrow,
3569
+ shadow = "sm",
3570
+ bordered = false,
3571
+ clickable = false,
3572
+ meta,
3573
+ children,
3574
+ className,
3575
+ ...props
3576
+ }) => {
3577
+ const cardClassName = cn(
3578
+ "media_card",
3579
+ `media_card_image_${imagePosition}`,
3580
+ `media_card_shadow_${shadow}`,
3581
+ bordered && "media_card_bordered",
3582
+ clickable && "media_card_clickable",
3583
+ className
3584
+ );
3585
+ const isOverlay = imagePosition === "overlay";
3586
+ const cardStyle = isOverlay && aspectRatio ? { aspectRatio } : void 0;
3587
+ const wrapStyle = !isOverlay && aspectRatio ? { aspectRatio } : void 0;
3588
+ return /* @__PURE__ */ jsxs("div", { className: cardClassName, style: cardStyle, ...props, children: [
3589
+ /* @__PURE__ */ jsxs("div", { className: "media_card_image_wrap", style: wrapStyle, children: [
3590
+ /* @__PURE__ */ jsx("img", { className: "media_card_image", src: image.src, alt: image.alt, loading: "lazy" }),
3591
+ isOverlay && /* @__PURE__ */ jsx("div", { className: "media_card_overlay", "aria-hidden": "true" })
3592
+ ] }),
3593
+ /* @__PURE__ */ jsxs("div", { className: "media_card_body", children: [
3594
+ eyebrow && /* @__PURE__ */ jsx("div", { className: "media_card_eyebrow", children: eyebrow }),
3595
+ heading && /* @__PURE__ */ jsx(HeadingTag, { className: "media_card_heading", children: heading }),
3596
+ children && /* @__PURE__ */ jsx("div", { className: "media_card_content", children }),
3597
+ meta && /* @__PURE__ */ jsx("div", { className: "media_card_meta", children: meta })
3598
+ ] })
2707
3599
  ] });
2708
3600
  };
3601
+ var Prose = ({ size = "md", className, children, ref, ...props }) => {
3602
+ const rootRef = React11.useRef(null);
3603
+ React11.useImperativeHandle(ref, () => rootRef.current, []);
3604
+ useSafeLayoutEffect(() => {
3605
+ const root = rootRef.current;
3606
+ if (!root) return;
3607
+ const targets = Array.from(root.querySelectorAll("pre, table"));
3608
+ const sync = () => {
3609
+ for (const el of targets) {
3610
+ if (el.scrollWidth > el.clientWidth) el.setAttribute("tabindex", "0");
3611
+ else el.removeAttribute("tabindex");
3612
+ }
3613
+ };
3614
+ sync();
3615
+ if (typeof ResizeObserver === "undefined") return;
3616
+ const observer = new ResizeObserver(sync);
3617
+ observer.observe(root);
3618
+ for (const el of targets) observer.observe(el);
3619
+ return () => observer.disconnect();
3620
+ }, [children]);
3621
+ return /* @__PURE__ */ jsx("div", { ref: rootRef, className: cn("prose", `prose_size_${size}`, className), ...props, children });
3622
+ };
3623
+ Prose.displayName = "Prose";
2709
3624
  var ICONS = {
2710
3625
  info: /* @__PURE__ */ jsx(Info, { size: iconSize.lg, "aria-hidden": "true" }),
2711
3626
  success: /* @__PURE__ */ jsx(CheckCircle2, { size: iconSize.lg, "aria-hidden": "true" }),
@@ -2756,8 +3671,8 @@ var AlertModal = ({
2756
3671
  variant = "info",
2757
3672
  title,
2758
3673
  message,
2759
- confirmText = "\uD655\uC778",
2760
- cancelText = "\uCDE8\uC18C",
3674
+ confirmText: confirmTextProp,
3675
+ cancelText: cancelTextProp,
2761
3676
  showCancel = false,
2762
3677
  destructive = false,
2763
3678
  actionsAlign = "right",
@@ -2767,6 +3682,9 @@ var AlertModal = ({
2767
3682
  onCancel,
2768
3683
  onClose
2769
3684
  }) => {
3685
+ const t = useLocaleText();
3686
+ const confirmText = confirmTextProp ?? t("alert.confirm");
3687
+ const cancelText = cancelTextProp ?? t("alert.cancel");
2770
3688
  const dismiss = onCancel ?? onClose;
2771
3689
  const panelRef = React11.useRef(null);
2772
3690
  const titleId = React11.useId();
@@ -2886,7 +3804,9 @@ var LinearProgress = ({
2886
3804
  }
2887
3805
  );
2888
3806
  };
2889
- var Spinner = ({ size = 24, ariaLabel = "\uB85C\uB529 \uC911" }) => {
3807
+ var Spinner = ({ size = 24, ariaLabel: ariaLabelProp }) => {
3808
+ const t = useLocaleText();
3809
+ const ariaLabel = ariaLabelProp ?? t("spinner.label");
2890
3810
  return /* @__PURE__ */ jsx(
2891
3811
  "span",
2892
3812
  {
@@ -2964,9 +3884,12 @@ var ToastItemComponent = ({ item, onRemove, closeAriaLabel }) => {
2964
3884
  var ToastProvider = ({
2965
3885
  children,
2966
3886
  maxCount = 5,
2967
- closeAriaLabel = "\uB2EB\uAE30",
2968
- regionLabel = "\uC54C\uB9BC"
3887
+ closeAriaLabel: closeAriaLabelProp,
3888
+ regionLabel: regionLabelProp
2969
3889
  }) => {
3890
+ const t = useLocaleText();
3891
+ const closeAriaLabel = closeAriaLabelProp ?? t("toast.close");
3892
+ const regionLabel = regionLabelProp ?? t("toast.region");
2970
3893
  const [toasts, setToasts] = React11.useState([]);
2971
3894
  const isMounted = useIsMounted();
2972
3895
  const addToast = React11.useCallback(
@@ -2977,7 +3900,7 @@ var ToastProvider = ({
2977
3900
  [maxCount]
2978
3901
  );
2979
3902
  const removeToast = React11.useCallback((id) => {
2980
- setToasts((prev) => prev.filter((t) => t.id !== id));
3903
+ setToasts((prev) => prev.filter((t2) => t2.id !== id));
2981
3904
  }, []);
2982
3905
  const contextValue = React11.useMemo(() => ({ addToast }), [addToast]);
2983
3906
  return /* @__PURE__ */ jsxs(ToastContext.Provider, { value: contextValue, children: [
@@ -3032,8 +3955,10 @@ var TopLoading = ({
3032
3955
  color,
3033
3956
  height = 3,
3034
3957
  isLoading = true,
3035
- ariaLabel = "\uD398\uC774\uC9C0 \uB85C\uB529 \uC911"
3958
+ ariaLabel: ariaLabelProp
3036
3959
  }) => {
3960
+ const t = useLocaleText();
3961
+ const ariaLabel = ariaLabelProp ?? t("topLoading.label");
3037
3962
  if (!isLoading) return null;
3038
3963
  const isIndeterminate = progress === void 0;
3039
3964
  return /* @__PURE__ */ jsx(
@@ -3059,26 +3984,212 @@ var TopLoading = ({
3059
3984
  }
3060
3985
  );
3061
3986
  };
3987
+ var Combobox = ({
3988
+ value = null,
3989
+ onValueChange,
3990
+ onSearch,
3991
+ defaultOptions = [],
3992
+ debounceMs = 250,
3993
+ placeholder: placeholderProp,
3994
+ emptyMessage: emptyMessageProp,
3995
+ idleMessage: idleMessageProp,
3996
+ size = "md",
3997
+ disabled = false,
3998
+ fullWidth = false,
3999
+ renderOption,
4000
+ ariaLabel,
4001
+ loadingLabel: loadingLabelProp,
4002
+ className,
4003
+ ...props
4004
+ }) => {
4005
+ const t = useLocaleText();
4006
+ const placeholder = placeholderProp ?? t("combobox.placeholder");
4007
+ const emptyMessage = emptyMessageProp ?? t("combobox.empty");
4008
+ const idleMessage = idleMessageProp ?? t("combobox.idle");
4009
+ const loadingLabel = loadingLabelProp ?? t("combobox.loading");
4010
+ const generatedId = useId();
4011
+ const field = useFieldControl();
4012
+ const inputId = field?.inputId ?? generatedId;
4013
+ const listId = `${inputId}-listbox`;
4014
+ const [query, setQuery] = useState("");
4015
+ const [options, setOptions] = useState(defaultOptions);
4016
+ const [isLoading, setIsLoading] = useState(false);
4017
+ const [hasSearched, setHasSearched] = useState(false);
4018
+ const requestSeq = useRef(0);
4019
+ const defaultOptionsRef = useRef(defaultOptions);
4020
+ useEffect(() => {
4021
+ defaultOptionsRef.current = defaultOptions;
4022
+ }, [defaultOptions]);
4023
+ const closeRef = useRef(() => {
4024
+ });
4025
+ const commit = useCallback(
4026
+ (option) => {
4027
+ onValueChange?.(option);
4028
+ setQuery("");
4029
+ closeRef.current();
4030
+ },
4031
+ [onValueChange]
4032
+ );
4033
+ const popup = useListboxPopup({
4034
+ items: options,
4035
+ onCommit: commit,
4036
+ disabled,
4037
+ // 상시 컨트롤이 입력창이라 포커스를 되돌릴 필요가 없다. triggerRef 는 장식용
4038
+ // chevron 버튼(tabIndex=-1)에 붙어 있어, 켜면 Escape 가 포커스를 그 숨은 버튼으로 던진다.
4039
+ returnFocusOnClose: false
4040
+ });
4041
+ const { isOpen, setIsOpen, close, activeIndex, setActiveIndex } = popup;
4042
+ closeRef.current = close;
4043
+ useEffect(() => {
4044
+ if (!isOpen) return;
4045
+ if (query === "") {
4046
+ requestSeq.current++;
4047
+ setOptions(defaultOptionsRef.current);
4048
+ setHasSearched(false);
4049
+ setIsLoading(false);
4050
+ return;
4051
+ }
4052
+ const seq = ++requestSeq.current;
4053
+ setIsLoading(true);
4054
+ const timer = setTimeout(() => {
4055
+ onSearch(query).then((result) => {
4056
+ if (seq !== requestSeq.current) return;
4057
+ setOptions(result);
4058
+ setHasSearched(true);
4059
+ }).catch(() => {
4060
+ if (seq !== requestSeq.current) return;
4061
+ setOptions([]);
4062
+ setHasSearched(true);
4063
+ }).finally(() => {
4064
+ if (seq !== requestSeq.current) return;
4065
+ setIsLoading(false);
4066
+ });
4067
+ }, debounceMs);
4068
+ return () => clearTimeout(timer);
4069
+ }, [query, isOpen, debounceMs, onSearch]);
4070
+ const rootClassName = cn(
4071
+ "combobox",
4072
+ `combobox_size_${size}`,
4073
+ { combobox_full_width: fullWidth, combobox_disabled: disabled },
4074
+ className
4075
+ );
4076
+ const showIdle = !isLoading && !hasSearched && options.length === 0;
4077
+ const showEmpty = !isLoading && hasSearched && options.length === 0;
4078
+ const hasList = !showIdle && !showEmpty;
4079
+ const panelStyle = useSpringPresence({
4080
+ visible: isOpen,
4081
+ from: popup.dropUp ? "translateY(4px)" : "translateY(-4px)"
4082
+ });
4083
+ return /* @__PURE__ */ jsxs("div", { ref: popup.wrapperRef, className: rootClassName, ...props, children: [
4084
+ /* @__PURE__ */ jsxs("div", { className: "combobox_control", children: [
4085
+ /* @__PURE__ */ jsx(
4086
+ "input",
4087
+ {
4088
+ id: inputId,
4089
+ className: "combobox_input",
4090
+ role: "combobox",
4091
+ type: "text",
4092
+ autoComplete: "off",
4093
+ disabled,
4094
+ value: isOpen ? query : value?.label ?? "",
4095
+ placeholder: value ? value.label : placeholder,
4096
+ "aria-expanded": isOpen,
4097
+ "aria-controls": isOpen && hasList ? listId : void 0,
4098
+ "aria-autocomplete": "list",
4099
+ "aria-activedescendant": isOpen && activeIndex >= 0 && options[activeIndex] ? `${listId}-${options[activeIndex].value}` : void 0,
4100
+ "aria-labelledby": field?.labelId,
4101
+ "aria-label": field?.labelId ? void 0 : ariaLabel,
4102
+ "aria-describedby": field?.describedBy,
4103
+ "aria-invalid": field?.invalid || void 0,
4104
+ "aria-required": field?.required || void 0,
4105
+ onChange: (event) => {
4106
+ setQuery(event.target.value);
4107
+ if (!isOpen) setIsOpen(true);
4108
+ },
4109
+ onFocus: () => !disabled && setIsOpen(true),
4110
+ onKeyDown: popup.onInputKeyDown
4111
+ }
4112
+ ),
4113
+ isLoading && /* @__PURE__ */ jsx("span", { className: "combobox_spinner", children: /* @__PURE__ */ jsx(Spinner, { size: iconSize.sm, ariaLabel: loadingLabel }) }),
4114
+ /* @__PURE__ */ jsx(
4115
+ "button",
4116
+ {
4117
+ type: "button",
4118
+ ref: popup.triggerRef,
4119
+ className: "combobox_toggle",
4120
+ tabIndex: -1,
4121
+ disabled,
4122
+ "aria-hidden": "true",
4123
+ onClick: () => isOpen ? close() : setIsOpen(true),
4124
+ children: /* @__PURE__ */ jsx(ChevronDown, { size: iconSize.lg })
4125
+ }
4126
+ )
4127
+ ] }),
4128
+ isOpen && /* @__PURE__ */ jsx(
4129
+ animated.div,
4130
+ {
4131
+ className: cn("combobox_panel", { combobox_panel_up: popup.dropUp }),
4132
+ style: panelStyle,
4133
+ children: !hasList ? /* @__PURE__ */ jsx("p", { className: "combobox_message", role: "status", children: showIdle ? idleMessage : emptyMessage }) : /* @__PURE__ */ jsx(
4134
+ "div",
4135
+ {
4136
+ ref: popup.listRef,
4137
+ id: listId,
4138
+ className: "combobox_list",
4139
+ role: "listbox",
4140
+ children: options.map((option, index) => (
4141
+ /* biome-ignore lint/a11y/useKeyWithClickEvents: 키보드는 입력의 onKeyDown 이 담당한다 - option 은 aria-activedescendant 로 가리키는 비포커스 요소다 (APG Combobox) */
4142
+ /* @__PURE__ */ jsx(
4143
+ "div",
4144
+ {
4145
+ id: `${listId}-${option.value}`,
4146
+ role: "option",
4147
+ tabIndex: -1,
4148
+ "aria-selected": value?.value === option.value,
4149
+ "aria-disabled": option.disabled || void 0,
4150
+ className: cn("combobox_option", {
4151
+ is_active: index === activeIndex,
4152
+ is_disabled: option.disabled
4153
+ }),
4154
+ onMouseEnter: () => !option.disabled && setActiveIndex(index),
4155
+ onClick: () => !option.disabled && commit(option),
4156
+ children: renderOption ? renderOption(option) : option.label
4157
+ },
4158
+ option.value
4159
+ )
4160
+ ))
4161
+ }
4162
+ )
4163
+ }
4164
+ )
4165
+ ] });
4166
+ };
3062
4167
  var normalizeForSearch = (s) => s.toLowerCase().replace(/\s+/g, "");
3063
4168
  var Dropdown = (props) => {
4169
+ const t = useLocaleText();
3064
4170
  const {
3065
4171
  id,
3066
4172
  label,
3067
- placeholder = "\uC120\uD0DD\u2026",
4173
+ placeholder: placeholderProp,
3068
4174
  options,
3069
4175
  disabled,
3070
4176
  size = "md",
3071
4177
  variant = "outline",
3072
4178
  className,
3073
4179
  searchable = false,
3074
- searchPlaceholder = "\uAC80\uC0C9\u2026",
3075
- emptyText = "\uACB0\uACFC \uC5C6\uC74C",
3076
- selectedSummary = (count) => `${count}\uAC1C \uC120\uD0DD`,
4180
+ searchPlaceholder: searchPlaceholderProp,
4181
+ emptyText: emptyTextProp,
4182
+ selectedSummary: selectedSummaryProp,
3077
4183
  name
3078
4184
  } = props;
4185
+ const placeholder = placeholderProp ?? t("dropdown.placeholder");
4186
+ const searchPlaceholder = searchPlaceholderProp ?? t("dropdown.searchPlaceholder");
4187
+ const emptyText = emptyTextProp ?? t("dropdown.empty");
4188
+ const selectedSummary = selectedSummaryProp ?? ((count) => t("dropdown.selectedSummary", { count }));
3079
4189
  const multiple = props.multiple === true;
3080
4190
  const internalId = useId();
3081
- const dropdownId = id ?? internalId;
4191
+ const field = useFieldControl();
4192
+ const dropdownId = id ?? field?.inputId ?? internalId;
3082
4193
  const isControlled = props.value !== void 0;
3083
4194
  const [internalSingle, setInternalSingle] = useState(
3084
4195
  () => props.multiple === true ? null : props.defaultValue ?? null
@@ -3086,14 +4197,9 @@ var Dropdown = (props) => {
3086
4197
  const [internalMulti, setInternalMulti] = useState(
3087
4198
  () => props.multiple === true ? props.defaultValue ?? [] : []
3088
4199
  );
3089
- const [isOpen, setIsOpen] = useState(false);
3090
- const [activeIndex, setActiveIndex] = useState(-1);
3091
- const [dropUp, setDropUp] = useState(false);
3092
4200
  const [searchText, setSearchText] = useState("");
3093
4201
  const [committedQuery, setCommittedQuery] = useState("");
3094
4202
  const isComposingRef = useRef(false);
3095
- const wrapperRef = useRef(null);
3096
- const controlRef = useRef(null);
3097
4203
  const searchRef = useRef(null);
3098
4204
  const selectedValues = useMemo(() => {
3099
4205
  if (multiple) {
@@ -3106,154 +4212,66 @@ var Dropdown = (props) => {
3106
4212
  const visibleOptions = useMemo(() => {
3107
4213
  if (!searchable) return options;
3108
4214
  const q = normalizeForSearch(committedQuery);
3109
- if (q === "") return options;
3110
- return options.filter((o) => normalizeForSearch(o.label).includes(q));
3111
- }, [options, searchable, committedQuery]);
3112
- const selectSingle = useCallback(
3113
- (next) => {
3114
- const option = options.find((o) => o.value === next) ?? null;
3115
- if (!isControlled) setInternalSingle(next);
3116
- if (props.multiple !== true) {
3117
- (props.onValueChange ?? props.onChange)?.(next, option);
3118
- }
3119
- },
3120
- [isControlled, options, props.multiple, props.onValueChange, props.onChange]
3121
- );
3122
- const toggleMultiple = useCallback(
3123
- (opt) => {
3124
- const exists = selectedValues.includes(opt.value);
3125
- const nextValues = exists ? selectedValues.filter((v) => v !== opt.value) : [...selectedValues, opt.value];
3126
- const nextOptions = nextValues.map((v) => options.find((o) => o.value === v)).filter((o) => Boolean(o));
3127
- if (!isControlled) setInternalMulti(nextValues);
3128
- if (props.multiple === true) {
3129
- (props.onValueChange ?? props.onChange)?.(nextValues, nextOptions);
3130
- }
3131
- },
3132
- [selectedValues, options, isControlled, props.multiple, props.onValueChange, props.onChange]
3133
- );
3134
- const closePanel = useCallback(() => {
3135
- setIsOpen(false);
3136
- if (searchable) controlRef.current?.focus();
3137
- }, [searchable]);
3138
- 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":
4215
+ if (q === "") return options;
4216
+ return options.filter((o) => normalizeForSearch(o.label).includes(q));
4217
+ }, [options, searchable, committedQuery]);
4218
+ const selectSingle = useCallback(
4219
+ (next) => {
4220
+ const option = options.find((o) => o.value === next) ?? null;
4221
+ if (!isControlled) setInternalSingle(next);
4222
+ if (props.multiple !== true) {
4223
+ (props.onValueChange ?? props.onChange)?.(next, option);
4224
+ }
4225
+ },
4226
+ [isControlled, options, props.multiple, props.onValueChange, props.onChange]
4227
+ );
4228
+ const toggleMultiple = useCallback(
4229
+ (opt) => {
4230
+ const exists = selectedValues.includes(opt.value);
4231
+ const nextValues = exists ? selectedValues.filter((v) => v !== opt.value) : [...selectedValues, opt.value];
4232
+ const nextOptions = nextValues.map((v) => options.find((o) => o.value === v)).filter((o) => Boolean(o));
4233
+ if (!isControlled) setInternalMulti(nextValues);
4234
+ if (props.multiple === true) {
4235
+ (props.onValueChange ?? props.onChange)?.(nextValues, nextOptions);
4236
+ }
4237
+ },
4238
+ [selectedValues, options, isControlled, props.multiple, props.onValueChange, props.onChange]
4239
+ );
4240
+ const selectOption = useCallback(
4241
+ (opt) => {
4242
+ if (opt.disabled) return;
4243
+ if (multiple) {
4244
+ toggleMultiple(opt);
4245
+ } else {
4246
+ selectSingle(opt.value);
3246
4247
  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]);
4248
+ }
4249
+ },
4250
+ // closePanel 은 아래 훅에서 오므로 선언 순서상 참조만 한다 (렌더마다 동일 참조).
4251
+ // biome-ignore lint/correctness/useExhaustiveDependencies: closePanel 은 훅 결과라 아래에서 정의된다
4252
+ [multiple, toggleMultiple, selectSingle]
4253
+ );
4254
+ const {
4255
+ isOpen,
4256
+ setIsOpen,
4257
+ dropUp,
4258
+ activeIndex,
4259
+ setActiveIndex,
4260
+ wrapperRef,
4261
+ triggerRef: controlRef,
4262
+ listRef,
4263
+ close: closePanel,
4264
+ onTriggerKeyDown: onControlKeyDown,
4265
+ onInputKeyDown: onSearchKeyDown
4266
+ } = useListboxPopup({
4267
+ items: visibleOptions,
4268
+ onCommit: selectOption,
4269
+ disabled,
4270
+ // searchable 은 포커스가 검색 입력에 있으므로 닫을 때 트리거로 되돌린다.
4271
+ returnFocusOnClose: searchable,
4272
+ // 열릴 때는 선택된 항목을 활성으로. 없으면 훅이 첫 활성 항목을 고른다.
4273
+ initialActiveIndex: (opts) => opts.findIndex((o) => selectedValues.includes(o.value) && !o.disabled)
4274
+ });
3257
4275
  useEffect(() => {
3258
4276
  if (!isOpen) {
3259
4277
  setSearchText("");
@@ -3266,14 +4284,6 @@ var Dropdown = (props) => {
3266
4284
  searchRef.current?.focus();
3267
4285
  }
3268
4286
  }, [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
4287
  const currentOption = useMemo(
3278
4288
  () => multiple ? null : options.find((o) => o.value === selectedValues[0]) ?? null,
3279
4289
  [multiple, options, selectedValues]
@@ -3304,6 +4314,8 @@ var Dropdown = (props) => {
3304
4314
  className: cn("dropdown_control", { is_disabled: disabled }),
3305
4315
  "aria-haspopup": "listbox",
3306
4316
  "aria-expanded": isOpen,
4317
+ "aria-describedby": field?.describedBy,
4318
+ "aria-invalid": field?.invalid || void 0,
3307
4319
  "aria-controls": isOpen ? `${dropdownId}_listbox` : void 0,
3308
4320
  onClick: () => !disabled && setIsOpen((o) => !o),
3309
4321
  onKeyDown: onControlKeyDown,
@@ -3354,6 +4366,7 @@ var Dropdown = (props) => {
3354
4366
  /* @__PURE__ */ jsx(
3355
4367
  "div",
3356
4368
  {
4369
+ ref: listRef,
3357
4370
  id: `${dropdownId}_listbox`,
3358
4371
  role: "listbox",
3359
4372
  className: "dropdown_options",
@@ -3399,7 +4412,7 @@ var Dropdown = (props) => {
3399
4412
  var pad = (n) => String(n).padStart(2, "0");
3400
4413
  var getDaysInMonth = (year, month) => new Date(year, month, 0).getDate();
3401
4414
  var normalizeWidth = (v) => typeof v === "number" ? `${v}px` : v;
3402
- var range = (start, end) => Array.from({ length: end - start + 1 }, (_, i) => start + i);
4415
+ var range2 = (start, end) => Array.from({ length: end - start + 1 }, (_, i) => start + i);
3403
4416
  var DatePicker = ({
3404
4417
  label,
3405
4418
  value,
@@ -3413,12 +4426,19 @@ var DatePicker = ({
3413
4426
  disabled,
3414
4427
  fullWidth = true,
3415
4428
  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"
4429
+ yearLabel: yearLabelProp,
4430
+ monthLabel: monthLabelProp,
4431
+ dayLabel: dayLabelProp,
4432
+ minDateSrFormat: minDateSrFormatProp,
4433
+ selectableRangeUntilTodaySrText: selectableRangeUntilTodaySrTextProp
3421
4434
  }) => {
4435
+ const t = useLocaleText();
4436
+ const yearLabel = yearLabelProp ?? t("datePicker.year");
4437
+ const monthLabel = monthLabelProp ?? t("datePicker.month");
4438
+ const dayLabel = dayLabelProp ?? t("datePicker.day");
4439
+ const minDateSrFormat = minDateSrFormatProp ?? t("datePicker.minDateSr");
4440
+ const selectableRangeUntilTodaySrText = selectableRangeUntilTodaySrTextProp ?? t("datePicker.rangeUntilTodaySr");
4441
+ const field = useFieldControl();
3422
4442
  const groupId = React11.useId();
3423
4443
  const constraintId = React11.useId();
3424
4444
  const { todayYear, todayMonth, todayDay } = React11.useMemo(() => {
@@ -3467,22 +4487,23 @@ var DatePicker = ({
3467
4487
  }
3468
4488
  return daysInMonth;
3469
4489
  }, [year, month, selectableRange, todayYear, todayMonth, todayDay]);
4490
+ const minYear = min.year > 0 ? Math.max(startYear, min.year) : startYear;
3470
4491
  const yearOptions = React11.useMemo(
3471
- () => range(startYear, maxYear).map((y) => ({
4492
+ () => range2(minYear, Math.max(minYear, maxYear)).map((y) => ({
3472
4493
  value: String(y),
3473
4494
  label: String(y)
3474
4495
  })),
3475
- [startYear, maxYear]
4496
+ [minYear, maxYear]
3476
4497
  );
3477
4498
  const monthOptions = React11.useMemo(
3478
- () => range(minMonth, Math.max(minMonth, maxMonth)).map((m) => ({
4499
+ () => range2(minMonth, Math.max(minMonth, maxMonth)).map((m) => ({
3479
4500
  value: String(m),
3480
4501
  label: pad(m)
3481
4502
  })),
3482
4503
  [minMonth, maxMonth]
3483
4504
  );
3484
4505
  const dayOptions = React11.useMemo(
3485
- () => range(minDay, Math.max(minDay, maxDay)).map((d) => ({
4506
+ () => range2(minDay, Math.max(minDay, maxDay)).map((d) => ({
3486
4507
  value: String(d),
3487
4508
  label: pad(d)
3488
4509
  })),
@@ -3548,8 +4569,9 @@ var DatePicker = ({
3548
4569
  {
3549
4570
  className: "date_picker_fields",
3550
4571
  role: "group",
3551
- "aria-labelledby": label ? groupId : void 0,
3552
- "aria-describedby": constraintDesc ? constraintId : void 0,
4572
+ "aria-labelledby": field?.labelId ?? (label ? groupId : void 0),
4573
+ "aria-describedby": [field?.describedBy, constraintDesc ? constraintId : void 0].filter(Boolean).join(" ") || void 0,
4574
+ "aria-invalid": field?.invalid || void 0,
3553
4575
  children: [
3554
4576
  /* @__PURE__ */ jsx(
3555
4577
  Dropdown,
@@ -3595,8 +4617,83 @@ var DatePicker = ({
3595
4617
  )
3596
4618
  ] });
3597
4619
  };
4620
+ var DateRangePicker = ({
4621
+ value,
4622
+ onValueChange,
4623
+ startLabel: startLabelProp,
4624
+ endLabel: endLabelProp,
4625
+ startYear,
4626
+ endYear,
4627
+ minDate,
4628
+ selectableRange = "all",
4629
+ disabled,
4630
+ fullWidth = true
4631
+ }) => {
4632
+ const t = useLocaleText();
4633
+ const startLabel = startLabelProp ?? t("dateRange.start");
4634
+ const endLabel = endLabelProp ?? t("dateRange.end");
4635
+ const field = useFieldControl();
4636
+ const start = value?.start;
4637
+ const end = value?.end;
4638
+ const handleStartChange = (next) => {
4639
+ onValueChange({ start: next, end: end && end < next ? void 0 : end });
4640
+ };
4641
+ const handleEndChange = (next) => {
4642
+ onValueChange({ start, end: next });
4643
+ };
4644
+ const endMinDate = start ?? minDate;
4645
+ return /* @__PURE__ */ jsx(
4646
+ "div",
4647
+ {
4648
+ className: cn("date_range_picker", {
4649
+ date_range_picker_full_width: fullWidth,
4650
+ date_range_picker_disabled: disabled
4651
+ }),
4652
+ children: /* @__PURE__ */ jsxs(
4653
+ "div",
4654
+ {
4655
+ className: "date_range_picker_fields",
4656
+ role: "group",
4657
+ "aria-labelledby": field?.labelId,
4658
+ "aria-describedby": field?.describedBy,
4659
+ "aria-invalid": field?.invalid || void 0,
4660
+ children: [
4661
+ /* @__PURE__ */ jsx(
4662
+ DatePicker,
4663
+ {
4664
+ label: startLabel,
4665
+ value: start,
4666
+ onValueChange: handleStartChange,
4667
+ startYear,
4668
+ endYear,
4669
+ minDate,
4670
+ selectableRange,
4671
+ disabled,
4672
+ fullWidth: true
4673
+ }
4674
+ ),
4675
+ /* @__PURE__ */ jsx(
4676
+ DatePicker,
4677
+ {
4678
+ label: endLabel,
4679
+ value: end,
4680
+ onValueChange: handleEndChange,
4681
+ startYear,
4682
+ endYear,
4683
+ minDate: endMinDate,
4684
+ selectableRange,
4685
+ disabled: disabled || !start,
4686
+ fullWidth: true
4687
+ }
4688
+ )
4689
+ ]
4690
+ }
4691
+ )
4692
+ }
4693
+ );
4694
+ };
3598
4695
  var FileInput = ({
3599
- label = "\uD30C\uC77C \uC120\uD0DD",
4696
+ label: labelProp,
3600
4697
  onFiles,
3601
4698
  supportingText,
3602
4699
  preview = false,
@@ -3608,8 +4705,12 @@ var FileInput = ({
3608
4705
  onChange,
3609
4706
  ...props
3610
4707
  }) => {
3611
- const inputId = React11.useId();
4708
+ const t = useLocaleText();
4709
+ const label = labelProp ?? t("fileInput.label");
4710
+ const generatedInputId = React11.useId();
3612
4711
  const helperId = React11.useId();
4712
+ const field = useFieldControl();
4713
+ const inputId = field?.inputId ?? generatedInputId;
3613
4714
  const inputRef = React11.useRef(null);
3614
4715
  const [previewUrls, setPreviewUrls] = React11.useState([]);
3615
4716
  const previewUrlsRef = React11.useRef([]);
@@ -3679,7 +4780,8 @@ var FileInput = ({
3679
4780
  className: "file_input_control",
3680
4781
  disabled,
3681
4782
  accept: isPreviewVariant ? accept ?? "image/*" : accept,
3682
- "aria-describedby": supportingText ? helperId : void 0,
4783
+ "aria-describedby": field?.describedBy ?? (supportingText ? helperId : void 0),
4784
+ "aria-invalid": field?.invalid || void 0,
3683
4785
  onChange: handleChange
3684
4786
  }
3685
4787
  ),
@@ -3704,7 +4806,7 @@ var FileInput = ({
3704
4806
  type: "button",
3705
4807
  className: "file_input_preview_remove",
3706
4808
  onClick: handleRemove,
3707
- "aria-label": "\uC774\uBBF8\uC9C0 \uC81C\uAC70",
4809
+ "aria-label": t("fileInput.removeImage"),
3708
4810
  children: /* @__PURE__ */ jsx(X, { size: iconSize.xs, "aria-hidden": "true" })
3709
4811
  }
3710
4812
  ),
@@ -3767,20 +4869,28 @@ function ImageCropper({
3767
4869
  onReady,
3768
4870
  onError,
3769
4871
  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.",
4872
+ label: labelProp,
4873
+ hint: hintProp,
4874
+ zoomOutLabel: zoomOutLabelProp,
4875
+ zoomLabel: zoomLabelProp,
4876
+ zoomInLabel: zoomInLabelProp,
4877
+ noPanHint: noPanHintProp,
3776
4878
  ...rest
3777
4879
  }) {
4880
+ const t = useLocaleText();
4881
+ const hint = hintProp ?? t("imageCropper.hint");
4882
+ const noPanHint = noPanHintProp ?? t("imageCropper.noPanHint");
4883
+ const label = labelProp ?? t("imageCropper.label");
4884
+ const zoomOutLabel = zoomOutLabelProp ?? t("imageCropper.zoomOut");
4885
+ const zoomLabel = zoomLabelProp ?? t("imageCropper.zoom");
4886
+ const zoomInLabel = zoomInLabelProp ?? t("imageCropper.zoomIn");
3778
4887
  const imageRef = useRef(null);
3779
4888
  const viewportRef = useRef(null);
3780
4889
  const dragRef = useRef(
3781
4890
  null
3782
4891
  );
3783
4892
  const hintId = useId();
4893
+ const field = useFieldControl();
3784
4894
  const [previewUrl, setPreviewUrl] = useState("");
3785
4895
  const [srcType, setSrcType] = useState("");
3786
4896
  useEffect(() => {
@@ -3955,8 +5065,9 @@ function ImageCropper({
3955
5065
  className: cn("image_cropper_viewport", dragging && "image_cropper_viewport_dragging"),
3956
5066
  style: viewportStyle,
3957
5067
  role: "group",
3958
- "aria-label": label,
3959
- "aria-describedby": hintId,
5068
+ "aria-labelledby": field?.labelId,
5069
+ "aria-label": field?.labelId ? void 0 : label,
5070
+ "aria-describedby": [field?.describedBy, hintId].filter(Boolean).join(" "),
3960
5071
  tabIndex: imageSize ? 0 : -1,
3961
5072
  onPointerDown: handlePointerDown,
3962
5073
  onPointerMove: handlePointerMove,
@@ -4044,9 +5155,11 @@ var OtpInput = ({
4044
5155
  disabled = false,
4045
5156
  supportingText,
4046
5157
  autoFocus = false,
4047
- ariaLabel = "OTP \uC785\uB825",
5158
+ ariaLabel: ariaLabelProp,
4048
5159
  className
4049
5160
  }) => {
5161
+ const t = useLocaleText();
5162
+ const ariaLabel = ariaLabelProp ?? t("otpInput.label");
4050
5163
  const inputsRef = React11.useRef([]);
4051
5164
  const isTypingRef = React11.useRef(false);
4052
5165
  React11.useEffect(() => {
@@ -4130,46 +5243,56 @@ var OtpInput = ({
4130
5243
  };
4131
5244
  const rootClassName = cn("otp_input", className);
4132
5245
  const supportingId = React11.useId();
5246
+ const field = useFieldControl();
4133
5247
  return (
4134
5248
  // 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"
5249
+ /* @__PURE__ */ jsxs(
5250
+ "div",
5251
+ {
5252
+ className: rootClassName,
5253
+ role: "group",
5254
+ "aria-labelledby": field?.labelId,
5255
+ "aria-label": field?.labelId ? void 0 : ariaLabel,
5256
+ children: [
5257
+ /* @__PURE__ */ jsx("div", { className: "otp_input_boxes", children: digits.map((digit, i) => /* @__PURE__ */ jsx(
5258
+ "input",
5259
+ {
5260
+ ref: (el) => {
5261
+ inputsRef.current[i] = el;
5262
+ },
5263
+ type: "text",
5264
+ inputMode: "numeric",
5265
+ pattern: "\\d*",
5266
+ maxLength: 1,
5267
+ autoComplete: i === 0 ? "one-time-code" : "off",
5268
+ value: digit,
5269
+ onChange: (e) => handleChange(i, e),
5270
+ onFocus: () => handleFocus(i),
5271
+ onKeyDown: (e) => handleKeyDown2(i, e),
5272
+ onPaste: handlePaste,
5273
+ disabled,
5274
+ "aria-label": t("otpInput.digit", { index: i + 1 }),
5275
+ "aria-invalid": error || field?.invalid || void 0,
5276
+ "aria-describedby": field?.describedBy ?? (supportingText ? supportingId : void 0),
5277
+ className: cn(
5278
+ "otp_input_box",
5279
+ error && "otp_input_box_error",
5280
+ disabled && "otp_input_box_disabled"
5281
+ )
5282
+ },
5283
+ i
5284
+ )) }),
5285
+ supportingText && /* @__PURE__ */ jsx(
5286
+ "span",
5287
+ {
5288
+ id: supportingId,
5289
+ className: cn("otp_input_supporting", error && "otp_input_supporting_error"),
5290
+ children: supportingText
5291
+ }
4160
5292
  )
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
- ] })
5293
+ ]
5294
+ }
5295
+ )
4173
5296
  );
4174
5297
  };
4175
5298
  OtpInput.displayName = "OtpInput";
@@ -4198,6 +5321,7 @@ var RadioGroup = ({
4198
5321
  const generatedName = React11.useId();
4199
5322
  const name = nameProp ?? generatedName;
4200
5323
  const idPrefix = React11.useId();
5324
+ const field = useFieldControl();
4201
5325
  const labelId = label ? `${idPrefix}-label` : void 0;
4202
5326
  const helperId = supportingText ? `${idPrefix}-help` : void 0;
4203
5327
  const onChange = React11.useCallback(
@@ -4227,9 +5351,10 @@ var RadioGroup = ({
4227
5351
  "div",
4228
5352
  {
4229
5353
  role: "radiogroup",
4230
- "aria-labelledby": labelId,
4231
- "aria-describedby": helperId,
4232
- "aria-invalid": error || void 0,
5354
+ "aria-labelledby": field?.labelId ?? labelId,
5355
+ "aria-describedby": field?.describedBy ?? helperId,
5356
+ "aria-invalid": error || field?.invalid || void 0,
5357
+ "aria-required": field?.required || void 0,
4233
5358
  className: "radio_group_options",
4234
5359
  children
4235
5360
  }
@@ -4262,6 +5387,7 @@ var Radio = ({
4262
5387
  onChange?.(event);
4263
5388
  };
4264
5389
  const rootClassName = cn("radio", `radio_size_${size}`, disabled && "radio_disabled", className);
5390
+ const field = useFieldControl();
4265
5391
  return /* @__PURE__ */ jsxs("label", { className: rootClassName, children: [
4266
5392
  /* @__PURE__ */ jsx(
4267
5393
  "input",
@@ -4269,6 +5395,8 @@ var Radio = ({
4269
5395
  ref,
4270
5396
  type: "radio",
4271
5397
  className: "radio_input",
5398
+ id: field?.inputId ?? props.id,
5399
+ "aria-describedby": field?.describedBy ?? props["aria-describedby"],
4272
5400
  value,
4273
5401
  name,
4274
5402
  disabled,
@@ -4282,6 +5410,149 @@ var Radio = ({
4282
5410
  ] });
4283
5411
  };
4284
5412
  Radio.displayName = "Radio";
5413
+ var SEPARATORS = /[,\t\n\r]+/;
5414
+ var splitTags = (text) => text.split(SEPARATORS).map((part) => part.trim()).filter(Boolean);
5415
+ var TagInput = ({
5416
+ value,
5417
+ defaultValue = [],
5418
+ onValueChange,
5419
+ placeholder: placeholderProp,
5420
+ maxTags,
5421
+ allowDuplicates = false,
5422
+ size = "md",
5423
+ disabled = false,
5424
+ fullWidth = false,
5425
+ ariaLabel,
5426
+ className,
5427
+ ...props
5428
+ }) => {
5429
+ const t = useLocaleText();
5430
+ const placeholder = placeholderProp ?? t("tagInput.placeholder");
5431
+ const generatedId = useId();
5432
+ const field = useFieldControl();
5433
+ const inputId = field?.inputId ?? generatedId;
5434
+ const isControlled = value !== void 0;
5435
+ const [innerTags, setInnerTags] = useState(defaultValue);
5436
+ const tags = isControlled ? value : innerTags;
5437
+ const [draft, setDraft] = useState("");
5438
+ const [announcement, setAnnouncement] = useState("");
5439
+ const inputRef = useRef(null);
5440
+ const isFull = maxTags !== void 0 && tags.length >= maxTags;
5441
+ const setTags = (next) => {
5442
+ if (!isControlled) setInnerTags(next);
5443
+ onValueChange?.(next);
5444
+ };
5445
+ const addTags = (text) => {
5446
+ const candidates = splitTags(text);
5447
+ if (candidates.length === 0) return 0;
5448
+ const next = [...tags];
5449
+ const added = [];
5450
+ const duplicates = [];
5451
+ for (const candidate of candidates) {
5452
+ if (maxTags !== void 0 && next.length >= maxTags) break;
5453
+ if (!allowDuplicates && next.includes(candidate)) {
5454
+ duplicates.push(candidate);
5455
+ continue;
5456
+ }
5457
+ next.push(candidate);
5458
+ added.push(candidate);
5459
+ }
5460
+ const isAtCap = maxTags !== void 0 && next.length >= maxTags;
5461
+ if (added.length === 0) {
5462
+ if (isAtCap) {
5463
+ setAnnouncement(t("tagInput.atCap", { max: maxTags }));
5464
+ } else if (duplicates.length > 0) {
5465
+ setAnnouncement(t("tagInput.duplicate", { names: duplicates.join(", ") }));
5466
+ }
5467
+ return 0;
5468
+ }
5469
+ setTags(next);
5470
+ const notes = [
5471
+ duplicates.length > 0 ? t("tagInput.duplicate", { names: duplicates.join(", ") }) : "",
5472
+ isAtCap && maxTags !== void 0 ? t("tagInput.atCap", { max: maxTags }) : ""
5473
+ ].filter(Boolean);
5474
+ setAnnouncement(
5475
+ notes.length > 0 ? t("tagInput.addedWithNotes", { names: added.join(", "), notes: notes.join(", ") }) : t("tagInput.added", { names: added.join(", ") })
5476
+ );
5477
+ return added.length;
5478
+ };
5479
+ const removeAt = (index) => {
5480
+ const removed = tags[index];
5481
+ setTags(tags.filter((_, i) => i !== index));
5482
+ setAnnouncement(t("tagInput.removed", { name: removed }));
5483
+ };
5484
+ const onKeyDown = (event) => {
5485
+ if (event.nativeEvent.isComposing) return;
5486
+ if (event.key === "Enter" || event.key === ",") {
5487
+ event.preventDefault();
5488
+ if (addTags(draft) > 0) setDraft("");
5489
+ return;
5490
+ }
5491
+ if (event.key === "Backspace" && draft === "" && tags.length > 0) {
5492
+ event.preventDefault();
5493
+ removeAt(tags.length - 1);
5494
+ }
5495
+ };
5496
+ const onPaste = (event) => {
5497
+ const text = event.clipboardData.getData("text");
5498
+ if (!SEPARATORS.test(text)) return;
5499
+ event.preventDefault();
5500
+ const input = event.currentTarget;
5501
+ const start = input.selectionStart ?? draft.length;
5502
+ const end = input.selectionEnd ?? draft.length;
5503
+ const merged = `${draft.slice(0, start)}${text}${draft.slice(end)}`;
5504
+ if (addTags(merged) > 0) setDraft("");
5505
+ };
5506
+ const rootClassName = cn(
5507
+ "tag_input",
5508
+ `tag_input_size_${size}`,
5509
+ { tag_input_full_width: fullWidth, tag_input_disabled: disabled },
5510
+ className
5511
+ );
5512
+ return /* @__PURE__ */ jsxs("div", { className: rootClassName, ...props, children: [
5513
+ /* @__PURE__ */ jsxs("div", { className: "tag_input_control", onClick: () => inputRef.current?.focus(), children: [
5514
+ tags.length > 0 && /* @__PURE__ */ jsx("ul", { className: "tag_input_tags", children: tags.map((tag, index) => (
5515
+ /* biome-ignore lint/suspicious/noArrayIndexKey: allowDuplicates 면 같은 라벨이 여러 개라 값만으로는 구분되지 않는다 */
5516
+ /* @__PURE__ */ jsx("li", { className: "tag_input_tag", children: /* @__PURE__ */ jsx(
5517
+ Chip,
5518
+ {
5519
+ type: "static",
5520
+ size: "sm",
5521
+ label: tag,
5522
+ removable: !disabled,
5523
+ onRemove: () => removeAt(index)
5524
+ }
5525
+ ) }, `${tag}-${index}`)
5526
+ )) }),
5527
+ /* @__PURE__ */ jsx(
5528
+ "input",
5529
+ {
5530
+ ref: inputRef,
5531
+ id: inputId,
5532
+ className: "tag_input_field",
5533
+ type: "text",
5534
+ autoComplete: "off",
5535
+ value: draft,
5536
+ disabled,
5537
+ readOnly: isFull,
5538
+ placeholder: isFull ? "" : placeholder,
5539
+ "aria-labelledby": field?.labelId,
5540
+ "aria-label": field?.labelId ? void 0 : ariaLabel,
5541
+ "aria-describedby": field?.describedBy,
5542
+ "aria-invalid": field?.invalid || void 0,
5543
+ "aria-required": field?.required || void 0,
5544
+ onChange: (event) => setDraft(event.target.value),
5545
+ onKeyDown,
5546
+ onPaste,
5547
+ onBlur: () => {
5548
+ if (addTags(draft) > 0) setDraft("");
5549
+ }
5550
+ }
5551
+ )
5552
+ ] }),
5553
+ /* @__PURE__ */ jsx("span", { className: "tag_input_live", role: "status", children: announcement })
5554
+ ] });
5555
+ };
4285
5556
  var LINE_HEIGHT_PX = {
4286
5557
  sm: 20,
4287
5558
  md: 20,
@@ -4307,13 +5578,16 @@ var Textarea = ({
4307
5578
  maxRows,
4308
5579
  showCounter,
4309
5580
  resize = "vertical",
5581
+ toolbar,
4310
5582
  maxLength,
4311
5583
  ref,
4312
5584
  ...props
4313
5585
  }) => {
4314
5586
  const generatedId = useId();
4315
- const inputId = id ?? generatedId;
5587
+ const field = useFieldControl();
5588
+ const inputId = id ?? field?.inputId ?? generatedId;
4316
5589
  const helperId = supportingText ? `${inputId}-help` : void 0;
5590
+ const describedBy = field?.describedBy ?? helperId;
4317
5591
  const isControlled = value !== void 0;
4318
5592
  const applyTransform = (nextValue) => transformValue ? transformValue(nextValue) : nextValue;
4319
5593
  const [innerValue, setInnerValue] = useState(() => applyTransform(value ?? defaultValue ?? ""));
@@ -4328,242 +5602,204 @@ var Textarea = ({
4328
5602
  setInnerValue(nextValue);
4329
5603
  lastEmittedValueRef.current = nextValue;
4330
5604
  }
4331
- const setRefs = useCallback(
4332
- (node) => {
4333
- innerRef.current = node;
4334
- if (typeof ref === "function") ref(node);
4335
- else if (ref) ref.current = node;
4336
- },
4337
- [ref]
4338
- );
4339
- useSafeLayoutEffect(() => {
4340
- if (!autoGrow) return;
4341
- const el = innerRef.current;
4342
- if (!el) return;
4343
- const lh2 = LINE_HEIGHT_PX[size];
4344
- const minH = minRows ? minRows * lh2 : 0;
4345
- const maxH = maxRows ? maxRows * lh2 : Number.POSITIVE_INFINITY;
4346
- el.style.height = "auto";
4347
- const next = Math.min(Math.max(el.scrollHeight, minH), maxH);
4348
- el.style.height = `${next}px`;
4349
- el.style.overflowY = el.scrollHeight > maxH ? "auto" : "hidden";
4350
- }, [innerValue, autoGrow, size, minRows, maxRows]);
4351
- const rootClassName = cn(
4352
- "textarea",
4353
- size === "sm" && "textarea_size_sm",
4354
- size === "lg" && "textarea_size_lg",
4355
- fullWidth && "textarea_full_width",
4356
- error && "textarea_error",
4357
- props.disabled && "textarea_disabled",
4358
- className
4359
- );
4360
- const counterText = showCounter && maxLength !== void 0 ? `${innerValue.length}/${maxLength}` : showCounter ? String(innerValue.length) : null;
4361
- const emit = (nextValue) => {
4362
- setInnerValue(nextValue);
4363
- if (nextValue !== lastEmittedValueRef.current) {
4364
- lastEmittedValueRef.current = nextValue;
4365
- (onValueChange ?? onChangeAction)?.(nextValue);
4366
- }
4367
- };
4368
- return /* @__PURE__ */ jsxs("div", { className: rootClassName, children: [
4369
- label && showLabel && /* @__PURE__ */ jsx("label", { htmlFor: inputId, className: "textarea_label", children: label }),
4370
- /* @__PURE__ */ jsx("div", { className: "textarea_container", children: /* @__PURE__ */ jsx("div", { className: "textarea_input_wrap", children: /* @__PURE__ */ jsx(
4371
- "textarea",
4372
- {
4373
- id: inputId,
4374
- ref: setRefs,
4375
- className: "textarea_input",
4376
- style: { resize: autoGrow ? "none" : resize },
4377
- rows: autoGrow ? minRows ?? rows : rows,
4378
- maxLength,
4379
- "aria-invalid": !!error,
4380
- "aria-describedby": helperId,
4381
- "aria-label": !showLabel ? label : void 0,
4382
- ...props,
4383
- value: innerValue,
4384
- onCompositionStart: () => {
4385
- isComposingRef.current = true;
4386
- },
4387
- onCompositionEnd: (event) => {
4388
- isComposingRef.current = false;
4389
- emit(applyTransform(event.currentTarget.value));
4390
- },
4391
- onChange: (event) => {
4392
- const rawValue = event.target.value;
4393
- if (isComposingRef.current) {
4394
- setInnerValue(rawValue);
4395
- if (imeStrategy === "immediate" && rawValue !== lastEmittedValueRef.current) {
4396
- lastEmittedValueRef.current = rawValue;
4397
- (onValueChange ?? onChangeAction)?.(rawValue);
4398
- }
4399
- return;
4400
- }
4401
- emit(applyTransform(rawValue));
4402
- }
4403
- }
4404
- ) }) }),
4405
- (supportingText || counterText) && /* @__PURE__ */ jsxs("div", { className: "textarea_footer", children: [
4406
- supportingText ? /* @__PURE__ */ jsx("div", { id: helperId, className: "textarea_helper", children: supportingText }) : /* @__PURE__ */ jsx("span", {}),
4407
- counterText && /* @__PURE__ */ jsx("div", { className: "textarea_counter", "aria-hidden": "true", children: counterText })
4408
- ] })
4409
- ] });
4410
- };
4411
- Textarea.displayName = "Textarea";
4412
- var ClearIcon = () => /* @__PURE__ */ jsx(X, { size: iconSize.lg, "aria-hidden": "true" });
4413
- var DEFAULT_PASSWORD_TOGGLE_LABELS = { show: "\uBE44\uBC00\uBC88\uD638 \uD45C\uC2DC", hide: "\uBE44\uBC00\uBC88\uD638 \uC228\uAE30\uAE30" };
4414
- var TextField = ({
4415
- id,
4416
- label,
4417
- showLabel = true,
4418
- supportingText,
4419
- error,
4420
- success,
4421
- identifier,
4422
- leadingIcon,
4423
- trailingIcon,
4424
- leadingAction,
4425
- trailingAction,
4426
- showPasswordToggle,
4427
- passwordToggleLabels,
4428
- clearable,
4429
- clearLabel = "\uC9C0\uC6B0\uAE30",
4430
- type,
4431
- fullWidth,
4432
- size = "md",
4433
- variant = "outline",
4434
- className,
4435
- onValueChange,
4436
- onChangeAction,
4437
- imeStrategy = "delayed",
4438
- value,
4439
- defaultValue,
4440
- transformValue,
4441
- ref,
4442
- ...props
4443
- }) => {
4444
- const generatedId = useId();
4445
- const inputId = id ?? generatedId;
4446
- const helperId = supportingText ? `${inputId}-help` : void 0;
4447
- const isControlled = value !== void 0;
4448
- const applyTransform = (nextValue) => transformValue ? transformValue(nextValue) : nextValue;
4449
- const [innerValue, setInnerValue] = useState(() => applyTransform(value ?? defaultValue ?? ""));
4450
- const isComposingRef = useRef(false);
4451
- const lastEmittedValueRef = useRef(innerValue);
4452
- const [prevValue, setPrevValue] = useState(value);
4453
- if (isControlled && value !== prevValue && !isComposingRef.current) {
4454
- setPrevValue(value);
4455
- const nextValue = applyTransform(value ?? "");
4456
- setInnerValue(nextValue);
4457
- lastEmittedValueRef.current = nextValue;
4458
- }
4459
- const emit = useCallback(
4460
- (nextValue) => {
4461
- setInnerValue(nextValue);
4462
- if (nextValue !== lastEmittedValueRef.current) {
4463
- lastEmittedValueRef.current = nextValue;
4464
- (onValueChange ?? onChangeAction)?.(nextValue);
4465
- }
5605
+ const setRefs = useCallback(
5606
+ (node) => {
5607
+ innerRef.current = node;
5608
+ if (typeof ref === "function") ref(node);
5609
+ else if (ref) ref.current = node;
4466
5610
  },
4467
- [onValueChange, onChangeAction]
5611
+ [ref]
4468
5612
  );
4469
- const handleClear = useCallback(() => {
4470
- emit("");
4471
- }, [emit]);
4472
- const [passwordRevealed, setPasswordRevealed] = useState(false);
4473
- const togglePassword = useCallback(() => {
4474
- setPasswordRevealed((revealed) => !revealed);
4475
- }, []);
4476
- let resolvedType = type;
4477
- if (showPasswordToggle) {
4478
- resolvedType = passwordRevealed ? "text" : type ?? "password";
4479
- }
4480
- const isError = !!error;
4481
- const isSuccess = !!success && !isError;
5613
+ useSafeLayoutEffect(() => {
5614
+ if (!autoGrow) return;
5615
+ const el = innerRef.current;
5616
+ if (!el) return;
5617
+ const lh2 = LINE_HEIGHT_PX[size];
5618
+ const minH = minRows ? minRows * lh2 : 0;
5619
+ const maxH = maxRows ? maxRows * lh2 : Number.POSITIVE_INFINITY;
5620
+ el.style.height = "auto";
5621
+ const next = Math.min(Math.max(el.scrollHeight, minH), maxH);
5622
+ el.style.height = `${next}px`;
5623
+ el.style.overflowY = el.scrollHeight > maxH ? "auto" : "hidden";
5624
+ }, [innerValue, autoGrow, size, minRows, maxRows]);
4482
5625
  const rootClassName = cn(
4483
- "text_field",
4484
- `text_field_variant_${variant}`,
4485
- size === "sm" && "text_field_size_sm",
4486
- size === "lg" && "text_field_size_lg",
4487
- fullWidth && "text_field_full_width",
4488
- isError && "text_field_error",
4489
- isSuccess && "text_field_success",
4490
- props.disabled && "text_field_disabled",
5626
+ "textarea",
5627
+ size === "sm" && "textarea_size_sm",
5628
+ size === "lg" && "textarea_size_lg",
5629
+ fullWidth && "textarea_full_width",
5630
+ error && "textarea_error",
5631
+ props.disabled && "textarea_disabled",
4491
5632
  className
4492
5633
  );
4493
- const passwordToggleLabel = passwordRevealed ? passwordToggleLabels?.hide ?? DEFAULT_PASSWORD_TOGGLE_LABELS.hide : passwordToggleLabels?.show ?? DEFAULT_PASSWORD_TOGGLE_LABELS.show;
4494
- const resolvedTrailing = showPasswordToggle ? /* @__PURE__ */ jsx("span", { className: "text_field_icon text_field_action", children: /* @__PURE__ */ jsx(
4495
- "button",
4496
- {
4497
- type: "button",
4498
- onClick: togglePassword,
4499
- "aria-label": passwordToggleLabel,
4500
- disabled: props.disabled,
4501
- children: passwordRevealed ? /* @__PURE__ */ jsx(EyeOff, { size: iconSize.lg, "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Eye, { size: iconSize.lg, "aria-hidden": "true" })
4502
- }
4503
- ) }) : clearable && innerValue ? /* @__PURE__ */ jsx(
4504
- "button",
4505
- {
4506
- type: "button",
4507
- className: "text_field_clear",
4508
- onClick: handleClear,
4509
- "aria-label": clearLabel,
4510
- disabled: props.disabled,
4511
- children: /* @__PURE__ */ jsx(ClearIcon, {})
5634
+ const counterText = showCounter && maxLength !== void 0 ? `${innerValue.length}/${maxLength}` : showCounter ? String(innerValue.length) : null;
5635
+ const emit = (nextValue) => {
5636
+ setInnerValue(nextValue);
5637
+ if (nextValue !== lastEmittedValueRef.current) {
5638
+ lastEmittedValueRef.current = nextValue;
5639
+ (onValueChange ?? onChangeAction)?.(nextValue);
4512
5640
  }
4513
- ) : 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;
4514
- 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;
5641
+ };
4515
5642
  return /* @__PURE__ */ jsxs("div", { className: rootClassName, children: [
4516
- label && showLabel && /* @__PURE__ */ jsx("label", { htmlFor: inputId, className: "text_field_label", children: label }),
4517
- /* @__PURE__ */ jsx("div", { className: "text_field_container", children: /* @__PURE__ */ jsxs("div", { className: "text_field_inner", children: [
4518
- resolvedLeading,
4519
- /* @__PURE__ */ jsx(
4520
- "div",
5643
+ label && showLabel && /* @__PURE__ */ jsx("label", { htmlFor: inputId, className: "textarea_label", children: label }),
5644
+ /* @__PURE__ */ jsxs("div", { className: "textarea_container", children: [
5645
+ toolbar && /* @__PURE__ */ jsx("div", { className: "textarea_toolbar", inert: props.disabled || void 0, children: toolbar }),
5646
+ /* @__PURE__ */ jsx("div", { className: "textarea_input_wrap", children: /* @__PURE__ */ jsx(
5647
+ "textarea",
4521
5648
  {
4522
- className: cn(
4523
- "text_field_input_wrap",
4524
- resolvedTrailing && "text_field_input_wrap_no_pad_right"
4525
- ),
4526
- children: /* @__PURE__ */ jsx(
4527
- "input",
4528
- {
4529
- id: inputId,
4530
- ref,
4531
- className: cn("text_field_input", identifier && "text_field_input_identifier"),
4532
- "aria-invalid": isError,
4533
- "aria-describedby": helperId,
4534
- "aria-label": !showLabel ? label : void 0,
4535
- ...props,
4536
- type: resolvedType,
4537
- value: innerValue,
4538
- onCompositionStart: () => {
4539
- isComposingRef.current = true;
4540
- },
4541
- onCompositionEnd: (event) => {
4542
- isComposingRef.current = false;
4543
- emit(applyTransform(event.currentTarget.value));
4544
- },
4545
- onChange: (event) => {
4546
- const rawValue = event.target.value;
4547
- if (isComposingRef.current) {
4548
- setInnerValue(rawValue);
4549
- if (imeStrategy === "immediate" && rawValue !== lastEmittedValueRef.current) {
4550
- lastEmittedValueRef.current = rawValue;
4551
- (onValueChange ?? onChangeAction)?.(rawValue);
4552
- }
4553
- return;
4554
- }
4555
- emit(applyTransform(rawValue));
5649
+ id: inputId,
5650
+ ref: setRefs,
5651
+ className: "textarea_input",
5652
+ style: { resize: autoGrow ? "none" : resize },
5653
+ rows: autoGrow ? minRows ?? rows : rows,
5654
+ maxLength,
5655
+ "aria-invalid": !!error || !!field?.invalid,
5656
+ "aria-describedby": describedBy,
5657
+ "aria-required": field?.required || void 0,
5658
+ "aria-label": !showLabel ? label : void 0,
5659
+ ...props,
5660
+ value: innerValue,
5661
+ onCompositionStart: () => {
5662
+ isComposingRef.current = true;
5663
+ },
5664
+ onCompositionEnd: (event) => {
5665
+ isComposingRef.current = false;
5666
+ emit(applyTransform(event.currentTarget.value));
5667
+ },
5668
+ onChange: (event) => {
5669
+ const rawValue = event.target.value;
5670
+ if (isComposingRef.current) {
5671
+ setInnerValue(rawValue);
5672
+ if (imeStrategy === "immediate" && rawValue !== lastEmittedValueRef.current) {
5673
+ lastEmittedValueRef.current = rawValue;
5674
+ (onValueChange ?? onChangeAction)?.(rawValue);
4556
5675
  }
5676
+ return;
4557
5677
  }
4558
- )
5678
+ emit(applyTransform(rawValue));
5679
+ }
4559
5680
  }
4560
- ),
4561
- resolvedTrailing
4562
- ] }) }),
4563
- supportingText && /* @__PURE__ */ jsx("div", { id: helperId, className: "text_field_helper", children: supportingText })
5681
+ ) })
5682
+ ] }),
5683
+ (supportingText || counterText) && /* @__PURE__ */ jsxs("div", { className: "textarea_footer", children: [
5684
+ supportingText ? /* @__PURE__ */ jsx("div", { id: helperId, className: "textarea_helper", children: supportingText }) : /* @__PURE__ */ jsx("span", {}),
5685
+ counterText && /* @__PURE__ */ jsx("div", { className: "textarea_counter", "aria-hidden": "true", children: counterText })
5686
+ ] })
4564
5687
  ] });
4565
5688
  };
4566
- TextField.displayName = "TextField";
5689
+ Textarea.displayName = "Textarea";
5690
+ var toMinutes = (value) => {
5691
+ if (!value) return null;
5692
+ const [h, m] = value.split(":").map(Number);
5693
+ if (!Number.isInteger(h) || !Number.isInteger(m)) return null;
5694
+ if (h < 0 || h > 23 || m < 0 || m > 59) return null;
5695
+ return h * 60 + m;
5696
+ };
5697
+ var pad2 = (n) => String(n).padStart(2, "0");
5698
+ var TimePicker = ({
5699
+ label,
5700
+ value,
5701
+ onValueChange,
5702
+ minuteStep = 5,
5703
+ minTime,
5704
+ maxTime,
5705
+ disabled,
5706
+ fullWidth = true,
5707
+ hourLabel: hourLabelProp,
5708
+ minuteLabel: minuteLabelProp
5709
+ }) => {
5710
+ const t = useLocaleText();
5711
+ const hourLabel = hourLabelProp ?? t("timePicker.hour");
5712
+ const minuteLabel = minuteLabelProp ?? t("timePicker.minute");
5713
+ const field = useFieldControl();
5714
+ const groupId = React11.useId();
5715
+ const constraintId = React11.useId();
5716
+ const min = toMinutes(minTime) ?? 0;
5717
+ const max = toMinutes(maxTime) ?? 23 * 60 + 59;
5718
+ const parsed = toMinutes(value);
5719
+ const hour = parsed === null ? null : Math.floor(parsed / 60);
5720
+ const minute = parsed === null ? null : parsed % 60;
5721
+ const hourOptions = React11.useMemo(() => {
5722
+ const first = Math.floor(min / 60);
5723
+ const last = Math.floor(max / 60);
5724
+ return Array.from({ length: Math.max(0, last - first + 1) }, (_, i) => {
5725
+ const h = first + i;
5726
+ return { value: String(h), label: pad2(h) };
5727
+ });
5728
+ }, [min, max]);
5729
+ const minuteOptions = React11.useMemo(() => {
5730
+ if (hour === null) return [];
5731
+ const step = Math.max(1, Math.floor(minuteStep));
5732
+ 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) }));
5733
+ }, [hour, minuteStep, min, max]);
5734
+ const emit = (h, m) => onValueChange(`${pad2(h)}:${pad2(m)}`);
5735
+ const handleHourChange = (raw) => {
5736
+ if (!raw) return;
5737
+ const h = Number(raw);
5738
+ const step = Math.max(1, Math.floor(minuteStep));
5739
+ const candidates = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step).filter(
5740
+ (m) => m < 60 && h * 60 + m >= min && h * 60 + m <= max
5741
+ );
5742
+ if (candidates.length === 0) return;
5743
+ const keep = minute !== null && candidates.includes(minute) ? minute : candidates[0];
5744
+ emit(h, keep);
5745
+ };
5746
+ const handleMinuteChange = (raw) => {
5747
+ if (!raw || hour === null) return;
5748
+ emit(hour, Number(raw));
5749
+ };
5750
+ const constraint = minTime || maxTime ? t("timePicker.rangeSr", { min: minTime ?? "00:00", max: maxTime ?? "23:59" }) : "";
5751
+ return /* @__PURE__ */ jsxs(
5752
+ "div",
5753
+ {
5754
+ className: cn("time_picker", {
5755
+ time_picker_full_width: fullWidth,
5756
+ time_picker_disabled: disabled
5757
+ }),
5758
+ children: [
5759
+ label && /* @__PURE__ */ jsx("span", { className: "time_picker_label", id: groupId, children: label }),
5760
+ constraint && /* @__PURE__ */ jsx("span", { id: constraintId, className: "time_picker_sr_only", children: constraint }),
5761
+ /* @__PURE__ */ jsxs(
5762
+ "div",
5763
+ {
5764
+ className: "time_picker_fields",
5765
+ role: "group",
5766
+ "aria-labelledby": field?.labelId ?? (label ? groupId : void 0),
5767
+ "aria-describedby": [field?.describedBy, constraint ? constraintId : void 0].filter(Boolean).join(" ") || void 0,
5768
+ "aria-invalid": field?.invalid || void 0,
5769
+ children: [
5770
+ /* @__PURE__ */ jsx(
5771
+ Dropdown,
5772
+ {
5773
+ size: "sm",
5774
+ fullWidth: true,
5775
+ label: hourLabel,
5776
+ placeholder: hourLabel,
5777
+ options: hourOptions,
5778
+ value: hour === null ? null : String(hour),
5779
+ onValueChange: handleHourChange,
5780
+ disabled
5781
+ }
5782
+ ),
5783
+ /* @__PURE__ */ jsx(
5784
+ Dropdown,
5785
+ {
5786
+ size: "sm",
5787
+ fullWidth: true,
5788
+ label: minuteLabel,
5789
+ placeholder: minuteLabel,
5790
+ options: minuteOptions,
5791
+ value: minute === null ? null : String(minute),
5792
+ onValueChange: handleMinuteChange,
5793
+ disabled: disabled || hour === null
5794
+ }
5795
+ )
5796
+ ]
5797
+ }
5798
+ )
5799
+ ]
5800
+ }
5801
+ );
5802
+ };
4567
5803
  var Toggle = ({
4568
5804
  checked,
4569
5805
  defaultChecked,
@@ -4586,6 +5822,7 @@ var Toggle = ({
4586
5822
  if (!isControlled) setInnerChecked(next);
4587
5823
  (onCheckedChange ?? onChange)?.(next);
4588
5824
  };
5825
+ const field = useFieldControl();
4589
5826
  const rootClassName = cn(
4590
5827
  "toggle",
4591
5828
  `toggle_size_${size}`,
@@ -4600,7 +5837,10 @@ var Toggle = ({
4600
5837
  type: "button",
4601
5838
  role: "switch",
4602
5839
  "aria-checked": isOn,
4603
- "aria-label": ariaLabel,
5840
+ id: field?.inputId ?? props.id,
5841
+ "aria-describedby": field?.describedBy ?? props["aria-describedby"],
5842
+ "aria-labelledby": field?.labelId,
5843
+ "aria-label": field?.labelId ? void 0 : ariaLabel,
4604
5844
  disabled,
4605
5845
  onClick: handleToggle,
4606
5846
  className: rootClassName,
@@ -4626,92 +5866,6 @@ var IconButton = ({
4626
5866
  );
4627
5867
  return /* @__PURE__ */ jsx("button", { ref, type, className: buttonClassName, ...props, children: /* @__PURE__ */ jsx("span", { className: "icon_button_icon", "aria-hidden": "true", children: icon }) });
4628
5868
  };
4629
- var range2 = (start, end) => {
4630
- const out = [];
4631
- for (let i = start; i <= end; i += 1) out.push(i);
4632
- return out;
4633
- };
4634
- var getPaginationItems = (page, totalPages) => {
4635
- if (totalPages <= 7) return range2(1, totalPages);
4636
- const items = [];
4637
- const last = totalPages;
4638
- const sibling = 2;
4639
- if (page <= sibling + 2) {
4640
- for (const p of range2(1, sibling + 3)) items.push(p);
4641
- items.push("ellipsis");
4642
- items.push(last);
4643
- return items;
4644
- }
4645
- if (page >= last - sibling - 1) {
4646
- items.push(1);
4647
- items.push("ellipsis");
4648
- for (const p of range2(last - sibling - 2, last)) items.push(p);
4649
- return items;
4650
- }
4651
- items.push(1);
4652
- items.push("ellipsis");
4653
- for (const p of range2(page - sibling, page + sibling)) items.push(p);
4654
- items.push("ellipsis");
4655
- items.push(last);
4656
- return items;
4657
- };
4658
- var Pagination = ({
4659
- page,
4660
- totalPages,
4661
- onPageChange,
4662
- onChange,
4663
- prevLabel = "\uC774\uC804 \uD398\uC774\uC9C0",
4664
- nextLabel = "\uB2E4\uC74C \uD398\uC774\uC9C0",
4665
- navLabel = "\uD398\uC774\uC9C0 \uC774\uB3D9"
4666
- }) => {
4667
- const emit = onPageChange ?? onChange;
4668
- const prevDisabled = page <= 1;
4669
- const nextDisabled = page >= totalPages;
4670
- const items = React11.useMemo(() => getPaginationItems(page, totalPages), [page, totalPages]);
4671
- return /* @__PURE__ */ jsxs("nav", { className: "pagination", "aria-label": navLabel, children: [
4672
- /* @__PURE__ */ jsx(
4673
- "button",
4674
- {
4675
- type: "button",
4676
- className: "pagination_item",
4677
- onClick: () => emit?.(page - 1),
4678
- disabled: prevDisabled,
4679
- "aria-label": prevLabel,
4680
- children: "\u2039"
4681
- }
4682
- ),
4683
- /* @__PURE__ */ jsx("ul", { className: "pagination_pages", children: items.map((it, idx) => {
4684
- if (it === "ellipsis") {
4685
- const prev = items[idx - 1];
4686
- const next = items[idx + 1];
4687
- return /* @__PURE__ */ jsx("li", { className: "pagination_ellipsis", "aria-hidden": "true", children: "\u2026" }, `e-${prev}-${next}`);
4688
- }
4689
- const isActive = it === page;
4690
- const buttonClassName = cn("pagination_page_button", { pagination_active: isActive });
4691
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
4692
- "button",
4693
- {
4694
- type: "button",
4695
- className: buttonClassName,
4696
- onClick: () => emit?.(it),
4697
- "aria-current": isActive ? "page" : void 0,
4698
- children: it
4699
- }
4700
- ) }, it);
4701
- }) }),
4702
- /* @__PURE__ */ jsx(
4703
- "button",
4704
- {
4705
- type: "button",
4706
- className: "pagination_item",
4707
- onClick: () => emit?.(page + 1),
4708
- disabled: nextDisabled,
4709
- "aria-label": nextLabel,
4710
- children: "\u203A"
4711
- }
4712
- )
4713
- ] });
4714
- };
4715
5869
  var SLIDE_FROM = {
4716
5870
  left: "translateX(-100%)",
4717
5871
  right: "translateX(100%)",
@@ -4727,13 +5881,15 @@ var Drawer = ({
4727
5881
  closeOnOverlay = true,
4728
5882
  dismissible,
4729
5883
  showCloseIcon = true,
4730
- closeLabel = "\uB2EB\uAE30",
5884
+ closeLabel: closeLabelProp,
4731
5885
  ariaLabel,
4732
5886
  onExited,
4733
5887
  children,
4734
5888
  className,
4735
5889
  ...props
4736
5890
  }) => {
5891
+ const t = useLocaleText();
5892
+ const closeLabel = closeLabelProp ?? t("drawer.close");
4737
5893
  const lastContentRef = React11.useRef({ children, title, footer });
4738
5894
  if (open) lastContentRef.current = { children, title, footer };
4739
5895
  const content = open ? { children, title, footer } : lastContentRef.current;
@@ -4833,13 +5989,15 @@ var Modal = ({
4833
5989
  footer,
4834
5990
  footerAlign = "end",
4835
5991
  showCloseIcon = true,
4836
- closeLabel = "\uB2EB\uAE30",
5992
+ closeLabel: closeLabelProp,
4837
5993
  children,
4838
5994
  className,
4839
5995
  ariaLabel,
4840
5996
  onExited,
4841
5997
  ...props
4842
5998
  }) => {
5999
+ const t = useLocaleText();
6000
+ const closeLabel = closeLabelProp ?? t("modal.close");
4843
6001
  const lastContentRef = React11.useRef({ children, title, description, footer });
4844
6002
  if (open) lastContentRef.current = { children, title, description, footer };
4845
6003
  const content = open ? { children, title, description, footer } : lastContentRef.current;
@@ -5006,15 +6164,39 @@ var ThemeProvider = ({
5006
6164
  );
5007
6165
  return /* @__PURE__ */ jsx(ThemeContext.Provider, { value, children });
5008
6166
  };
6167
+ var AppShell = ({
6168
+ sidebar,
6169
+ header,
6170
+ padded = true,
6171
+ className,
6172
+ children,
6173
+ ref,
6174
+ ...props
6175
+ }) => /* @__PURE__ */ jsxs(
6176
+ "div",
6177
+ {
6178
+ ref,
6179
+ className: cn("app_shell", { app_shell_with_sidebar: !!sidebar }, className),
6180
+ ...props,
6181
+ children: [
6182
+ sidebar && /* @__PURE__ */ jsx("div", { className: "app_shell_sidebar", children: sidebar }),
6183
+ /* @__PURE__ */ jsxs("div", { className: "app_shell_body", children: [
6184
+ header && /* @__PURE__ */ jsx("div", { className: "app_shell_header", children: header }),
6185
+ /* @__PURE__ */ jsx("main", { className: cn("app_shell_main", { app_shell_main_padded: padded }), children })
6186
+ ] })
6187
+ ]
6188
+ }
6189
+ );
5009
6190
  var Container = ({
5010
6191
  size = "xl",
5011
6192
  center = true,
5012
- as: Tag = "div",
6193
+ as,
5013
6194
  ref,
5014
6195
  className,
5015
6196
  children,
5016
6197
  ...props
5017
6198
  }) => {
6199
+ const Tag = as ?? "div";
5018
6200
  return /* @__PURE__ */ jsx(
5019
6201
  Tag,
5020
6202
  {
@@ -5032,7 +6214,7 @@ var Grid = ({
5032
6214
  rowGap,
5033
6215
  colGap,
5034
6216
  singleColOnMobile = true,
5035
- as: Tag = "div",
6217
+ as,
5036
6218
  ref,
5037
6219
  className,
5038
6220
  children,
@@ -5040,6 +6222,7 @@ var Grid = ({
5040
6222
  ...props
5041
6223
  }) => {
5042
6224
  const gridTemplateColumns = cols === "auto" ? `repeat(auto-fill, minmax(${minColWidth}, 1fr))` : `repeat(${cols}, 1fr)`;
6225
+ const Tag = as ?? "div";
5043
6226
  return /* @__PURE__ */ jsx(
5044
6227
  Tag,
5045
6228
  {
@@ -5057,15 +6240,36 @@ var Grid = ({
5057
6240
  }
5058
6241
  );
5059
6242
  };
6243
+ var PageHeader = ({
6244
+ title,
6245
+ description,
6246
+ breadcrumb,
6247
+ actions,
6248
+ tabs,
6249
+ className,
6250
+ ref,
6251
+ ...props
6252
+ }) => /* @__PURE__ */ jsxs("div", { ref, className: cn("page_header", className), ...props, children: [
6253
+ breadcrumb && /* @__PURE__ */ jsx("div", { className: "page_header_breadcrumb", children: breadcrumb }),
6254
+ /* @__PURE__ */ jsxs("div", { className: "page_header_bar", children: [
6255
+ /* @__PURE__ */ jsxs("div", { className: "page_header_titles", children: [
6256
+ /* @__PURE__ */ jsx("h1", { className: "page_header_title", children: title }),
6257
+ description && /* @__PURE__ */ jsx("p", { className: "page_header_description", children: description })
6258
+ ] }),
6259
+ actions && /* @__PURE__ */ jsx("div", { className: "page_header_actions", children: actions })
6260
+ ] }),
6261
+ tabs && /* @__PURE__ */ jsx("div", { className: "page_header_tabs", children: tabs })
6262
+ ] });
5060
6263
  var Section = ({
5061
6264
  spacing: spacing2 = "md",
5062
6265
  bg = "default",
5063
- as: Tag = "section",
6266
+ as,
5064
6267
  ref,
5065
6268
  className,
5066
6269
  children,
5067
6270
  ...props
5068
6271
  }) => {
6272
+ const Tag = as ?? "section";
5069
6273
  return /* @__PURE__ */ jsx(
5070
6274
  Tag,
5071
6275
  {
@@ -5082,13 +6286,14 @@ var Stack = ({
5082
6286
  align,
5083
6287
  justify,
5084
6288
  wrap,
5085
- as: Tag = "div",
6289
+ as,
5086
6290
  ref,
5087
6291
  className,
5088
6292
  children,
5089
6293
  style,
5090
6294
  ...props
5091
6295
  }) => {
6296
+ const Tag = as ?? "div";
5092
6297
  return /* @__PURE__ */ jsx(
5093
6298
  Tag,
5094
6299
  {
@@ -5108,4 +6313,4 @@ var Stack = ({
5108
6313
  );
5109
6314
  };
5110
6315
 
5111
- 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 };
6316
+ 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 };