sellmate-design-system-react 9.0.0-beta.94 → 9.0.0-beta.95

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.cjs CHANGED
@@ -7731,6 +7731,13 @@ var TREE_LABEL_TYPOGRAPHY = {
7731
7731
  rowHeight: "h-[32px]"
7732
7732
  }
7733
7733
  };
7734
+ var TREE_BRANCH_MOTION = { duration: 200, easing: "cubic-bezier(0, 0, 0.2, 1)" };
7735
+ function canAnimateTreeBranch() {
7736
+ if (typeof window === "undefined" || typeof HTMLElement.prototype.animate !== "function") {
7737
+ return false;
7738
+ }
7739
+ return !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
7740
+ }
7734
7741
  var TREE_ROW_CLASS = "group/tree-item box-border flex w-full items-center gap-(--cmp-tree-item-rowGap) overflow-hidden bg-(--cmp-tree-item-bg-default) px-(--cmp-tree-item-paddingX) text-left hover:bg-(--cmp-tree-item-bg-hover)";
7735
7742
  var TREE_ROW_BORDER_CLASS = "border-b-(length:--cmp-tree-item-border-width-default) border-(--cmp-tree-item-border-default)";
7736
7743
  var TREE_ROW_ACTIVE_CLASS = "bg-(--cmp-tree-item-bg-hover)";
@@ -16127,6 +16134,21 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16127
16134
  const [focusedIndex, setFocusedIndex] = react.useState(-1);
16128
16135
  const [expanded, setExpanded] = react.useState(() => /* @__PURE__ */ new Set());
16129
16136
  const [searchCollapsed, setSearchCollapsed] = react.useState(NO_SEARCH_COLLAPSED);
16137
+ const [treeMotion, setTreeMotion] = react.useState(null);
16138
+ const treeMotionTimerRef = react.useRef(null);
16139
+ const pendingCollapseRef = react.useRef(null);
16140
+ const cancelTreeMotion = () => {
16141
+ if (treeMotionTimerRef.current != null) clearTimeout(treeMotionTimerRef.current);
16142
+ treeMotionTimerRef.current = null;
16143
+ pendingCollapseRef.current = null;
16144
+ setTreeMotion(null);
16145
+ };
16146
+ react.useEffect(
16147
+ () => () => {
16148
+ if (treeMotionTimerRef.current != null) clearTimeout(treeMotionTimerRef.current);
16149
+ },
16150
+ []
16151
+ );
16130
16152
  const baseId = react.useId();
16131
16153
  const listboxId = `${baseId}-listbox`;
16132
16154
  const activeOptionId = `${baseId}-active-option`;
@@ -16194,6 +16216,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16194
16216
  const commitOpen = (next, valueForRules) => {
16195
16217
  setOpen(next);
16196
16218
  onOpenChange?.(next);
16219
+ cancelTreeMotion();
16197
16220
  if (next) {
16198
16221
  dismissedByPointerRef.current = false;
16199
16222
  alignSelectedTopRef.current = true;
@@ -16264,6 +16287,10 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16264
16287
  (groupValue) => treeSearching ? !searchCollapsed.has(groupValue) : expanded.has(groupValue),
16265
16288
  [expanded, searchCollapsed, treeSearching]
16266
16289
  );
16290
+ const isGroupShownExpanded = react.useCallback(
16291
+ (groupValue) => isGroupExpanded(groupValue) && !(treeMotion != null && !treeMotion.opening && treeMotion.group === groupValue),
16292
+ [isGroupExpanded, treeMotion]
16293
+ );
16267
16294
  const treeRows = react.useMemo(
16268
16295
  () => isTree ? visibleTreeRows(searched, (row) => isGroupExpanded(row.value)) : null,
16269
16296
  [isGroupExpanded, isTree, searched]
@@ -16553,6 +16580,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16553
16580
  const handleSearchChange = (next) => {
16554
16581
  setSearch(next);
16555
16582
  setSearchCollapsed(NO_SEARCH_COLLAPSED);
16583
+ cancelTreeMotion();
16556
16584
  };
16557
16585
  const handleToggleExpand = (groupValue) => {
16558
16586
  rangeAnchorRef.current = null;
@@ -16562,8 +16590,41 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16562
16590
  else next.add(groupValue);
16563
16591
  return next;
16564
16592
  };
16565
- if (treeSearching) setSearchCollapsed(toggle);
16566
- else setExpanded(toggle);
16593
+ const applyToggle = treeSearching ? () => setSearchCollapsed(toggle) : () => setExpanded(toggle);
16594
+ if (treeMotionTimerRef.current != null) clearTimeout(treeMotionTimerRef.current);
16595
+ treeMotionTimerRef.current = null;
16596
+ const pendingCollapse = pendingCollapseRef.current;
16597
+ pendingCollapseRef.current = null;
16598
+ if (treeMotion?.group === groupValue && !treeMotion.opening) {
16599
+ setTreeMotion({ group: groupValue, opening: true });
16600
+ treeMotionTimerRef.current = setTimeout(
16601
+ () => setTreeMotion(null),
16602
+ TREE_BRANCH_MOTION.duration
16603
+ );
16604
+ return;
16605
+ }
16606
+ pendingCollapse?.();
16607
+ if (!canAnimateTreeBranch()) {
16608
+ setTreeMotion(null);
16609
+ applyToggle();
16610
+ return;
16611
+ }
16612
+ if (isGroupExpanded(groupValue)) {
16613
+ setTreeMotion({ group: groupValue, opening: false });
16614
+ pendingCollapseRef.current = applyToggle;
16615
+ treeMotionTimerRef.current = setTimeout(() => {
16616
+ pendingCollapseRef.current = null;
16617
+ setTreeMotion(null);
16618
+ applyToggle();
16619
+ }, TREE_BRANCH_MOTION.duration);
16620
+ } else {
16621
+ applyToggle();
16622
+ setTreeMotion({ group: groupValue, opening: true });
16623
+ treeMotionTimerRef.current = setTimeout(
16624
+ () => setTreeMotion(null),
16625
+ TREE_BRANCH_MOTION.duration
16626
+ );
16627
+ }
16567
16628
  };
16568
16629
  const handleToggleGroup = (leafValues) => {
16569
16630
  if (leafValues.length === 0) return;
@@ -16673,7 +16734,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16673
16734
  const row = rowIndex != null ? filtered[rowIndex] : void 0;
16674
16735
  if (rowIndex == null || row == null) return;
16675
16736
  event.preventDefault();
16676
- const rowExpanded = row.isGroup && isGroupExpanded(row.value);
16737
+ const rowExpanded = row.isGroup && isGroupShownExpanded(row.value);
16677
16738
  if (event.key === "ArrowRight") {
16678
16739
  if (!row.isGroup) return;
16679
16740
  if (!rowExpanded) {
@@ -16918,8 +16979,9 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
16918
16979
  setSize: isDepth || isTree ? void 0 : rowCount,
16919
16980
  multi,
16920
16981
  tree: isTree,
16921
- isExpanded: isGroupExpanded,
16982
+ isExpanded: isGroupShownExpanded,
16922
16983
  onToggleExpand: handleToggleExpand,
16984
+ treeMotion,
16923
16985
  treeLines,
16924
16986
  selected,
16925
16987
  groupLeafValues,
@@ -16976,6 +17038,7 @@ function ItemList({
16976
17038
  tree,
16977
17039
  isExpanded,
16978
17040
  onToggleExpand,
17041
+ treeMotion,
16979
17042
  treeLines,
16980
17043
  selected,
16981
17044
  groupLeafValues,
@@ -16996,295 +17059,322 @@ function ItemList({
16996
17059
  "disabled:cursor-not-allowed disabled:text-[color:var(--cmp-listBoxItem-content-disabled)]"
16997
17060
  );
16998
17061
  const activeClass = "hover:bg-[var(--cmp-listBoxItem-bg-hover)] hover:text-[color:var(--cmp-listBoxItem-content-hover)] focus-visible:bg-[var(--cmp-listBoxItem-bg-hover)] focus-visible:text-[color:var(--cmp-listBoxItem-content-hover)]";
16999
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { id: listboxId, role: tree ? "tree" : "listbox", "aria-multiselectable": multi || void 0, children: [
17000
- showSelectAll && (tree ? (
17001
- // 트리의 전체선택 줄은 STree 의 "전체" 줄 그대로다 (lib/tree-row).
17002
- /* @__PURE__ */ jsxRuntime.jsx(
17003
- TreeAllRow,
17004
- {
17005
- "data-select-all-row": true,
17006
- id: focusedValue === "__select_all__" ? activeOptionId : void 0,
17007
- "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
17008
- "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
17009
- tabIndex: -1,
17010
- label: selectAllLabel,
17011
- selected: selectAllSelected,
17012
- active: focusedValue === "__select_all__",
17013
- className: "select-none",
17014
- onMouseEnter: () => onHoverValue("__select_all__"),
17015
- onMouseLeave: () => onHoverValue(null),
17016
- onToggle: onSelectAll
17017
- }
17018
- )
17019
- ) : /* @__PURE__ */ jsxRuntime.jsxs(
17020
- "button",
17062
+ const wrapTreeMotion = (nodes) => {
17063
+ if (!tree || treeMotion == null) return nodes;
17064
+ const start = items.findIndex((o) => o.isGroup && o.value === treeMotion.group);
17065
+ if (start < 0) return nodes;
17066
+ let end = start + 1;
17067
+ while (end < items.length && items[end].depth > items[start].depth) end++;
17068
+ if (end === start + 1) return nodes;
17069
+ return [
17070
+ ...nodes.slice(0, start + 1),
17071
+ /* @__PURE__ */ jsxRuntime.jsx(TreeRowsMotion, { opening: treeMotion.opening, children: nodes.slice(start + 1, end) }, `__motion_${treeMotion.group}__`),
17072
+ ...nodes.slice(end)
17073
+ ];
17074
+ };
17075
+ const selectAllRow = showSelectAll && (tree ? (
17076
+ // 트리의 전체선택 줄은 STree 의 "전체" 줄 그대로다 (lib/tree-row).
17077
+ /* @__PURE__ */ jsxRuntime.jsx(
17078
+ TreeAllRow,
17021
17079
  {
17022
- type: "button",
17023
17080
  "data-select-all-row": true,
17024
17081
  id: focusedValue === "__select_all__" ? activeOptionId : void 0,
17025
17082
  "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
17026
17083
  "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
17027
- className: cn(
17028
- itemBase,
17029
- focusedValue === "__select_all__" && "bg-[var(--cmp-listBoxItem-bg-hover)] text-[color:var(--cmp-listBoxItem-content-hover)]"
17030
- ),
17031
- style: { ...LIST_BOX_ITEM_PADDING_Y_RIGHT, paddingLeft: LIST_BOX_ITEM_PADDING_LEFT[1] },
17084
+ tabIndex: -1,
17085
+ label: selectAllLabel,
17086
+ selected: selectAllSelected,
17087
+ active: focusedValue === "__select_all__",
17088
+ className: "select-none",
17032
17089
  onMouseEnter: () => onHoverValue("__select_all__"),
17033
17090
  onMouseLeave: () => onHoverValue(null),
17034
- onClick: onSelectAll,
17035
- children: [
17036
- /* @__PURE__ */ jsxRuntime.jsx(CheckboxBox, { state: selectAllSelected, focused: focusedValue === "__select_all__" }),
17037
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate", children: selectAllLabel })
17038
- ]
17091
+ onToggle: onSelectAll
17039
17092
  }
17040
- )),
17041
- topSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: { height: topSpacer } }),
17042
- items.map((o, windowIndex) => {
17043
- const idx = windowIndex + indexOffset;
17044
- const padLeft = LIST_BOX_ITEM_PADDING_LEFT[o.depth === 1 ? 1 : o.depth === 2 ? 2 : 3];
17045
- const rowKey = `${o.value}-${idx}`;
17046
- if (tree) {
17047
- const togglesLeaves = multi && o.isGroup;
17048
- const leafValues = togglesLeaves ? groupLeafValues.get(idx) ?? [] : [];
17049
- const total = leafValues.length;
17050
- const selectedCount = leafValues.reduce((n, v) => selectedSet.has(v) ? n + 1 : n, 0);
17051
- const isSelected2 = selectedSet.has(o.value);
17052
- const checkState = togglesLeaves ? total === 0 || selectedCount === 0 ? false : selectedCount === total ? true : null : isSelected2;
17053
- const rowDisabled = o.disabled || togglesLeaves && total === 0;
17054
- const focusKey = togglesLeaves ? `__group_${idx}__` : o.value;
17055
- const focused = focusedValue === focusKey;
17056
- const expanded = o.isGroup && isExpanded(o.value);
17057
- const line = treeLines[idx];
17058
- const toggle = () => {
17059
- if (rowDisabled) return;
17060
- if (togglesLeaves) onToggleGroup(leafValues);
17061
- else onSelect(o.value, idx, checkboxShiftRef.current);
17062
- };
17063
- return (
17064
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events -- 키보드는 트리거(또는 검색 입력)가 aria-activedescendant 로 맡는다. 줄로 포커스를 옮기지 않는다
17065
- /* @__PURE__ */ jsxRuntime.jsxs(
17066
- "div",
17067
- {
17068
- tabIndex: -1,
17069
- "data-select-row": true,
17070
- "data-index": idx,
17071
- id: focused ? activeOptionId : void 0,
17072
- role: "treeitem",
17073
- "aria-level": o.depth,
17074
- "aria-posinset": line?.posInSet,
17075
- "aria-setsize": line?.setSize,
17076
- "aria-expanded": o.isGroup ? expanded : void 0,
17077
- "aria-selected": checkState === true,
17078
- "aria-disabled": rowDisabled || void 0,
17079
- "data-select-focused": focused ? "true" : void 0,
17080
- "data-select-active": focused ? "true" : void 0,
17081
- className: cn(
17082
- TREE_ROW_CLASS,
17083
- TREE_LABEL_TYPOGRAPHY.sm.rowHeight,
17084
- TREE_ROW_BORDER_CLASS,
17085
- "select-none",
17086
- o.isGroup && "cursor-pointer",
17087
- focused && TREE_ROW_ACTIVE_CLASS
17088
- ),
17089
- onMouseEnter: () => onHoverValue(focusKey),
17090
- onMouseLeave: () => onHoverValue(null),
17091
- onClick: (event) => {
17092
- if (isRowControlClick(event)) return;
17093
- if (isRowSelectClick(event)) {
17094
- if (rowDisabled) return;
17095
- if (togglesLeaves) onToggleGroup(leafValues);
17096
- else onSelect(o.value, idx, event.shiftKey);
17097
- return;
17098
- }
17099
- if (o.isGroup) onToggleExpand(o.value);
17100
- },
17101
- children: [
17102
- /* @__PURE__ */ jsxRuntime.jsx(
17103
- TreeConnector,
17104
- {
17105
- guideline: true,
17106
- size: "sm",
17107
- depth: o.depth,
17108
- hasChildren: o.isGroup,
17109
- isLast: line?.isLast ?? true,
17110
- ancestorLast: line?.ancestorLast ?? []
17111
- }
17112
- ),
17113
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: TREE_CONTENT_CLASS, children: [
17114
- o.isGroup ? (
17115
- // 비활성 그룹도 접고 편다 — 그룹이 비활성이어도 아래에 고를 수 있는 자식이 있을 수 있다.
17116
- /* @__PURE__ */ jsxRuntime.jsx(TreeCaret, { expanded, onToggle: () => onToggleExpand(o.value) })
17117
- ) : /* @__PURE__ */ jsxRuntime.jsx(TreeCaretSpacer, {}),
17118
- /* @__PURE__ */ jsxRuntime.jsx(
17119
- TreeSelectZone,
17120
- {
17121
- selectable: true,
17122
- disabled: rowDisabled,
17123
- control: multi && /* @__PURE__ */ jsxRuntime.jsx(
17124
- "span",
17125
- {
17126
- "data-tree-control": true,
17127
- className: "flex shrink-0",
17128
- onPointerDownCapture: (event) => {
17129
- checkboxShiftRef.current = event.shiftKey;
17130
- },
17131
- children: /* @__PURE__ */ jsxRuntime.jsx(
17132
- SCheckbox,
17133
- {
17134
- value: checkState,
17135
- disabled: rowDisabled,
17136
- onValueChange: toggle
17137
- }
17138
- )
17139
- }
17140
- ),
17141
- children: /* @__PURE__ */ jsxRuntime.jsx(
17142
- OptionLabel,
17143
- {
17144
- label: o.label,
17145
- className: treeLabelClass("sm", {
17146
- // 고른 줄은 굵게 — multi_tree 는 일부만 고른 그룹도 굵다 (STree 와 같다)
17147
- emphasized: checkState !== false,
17148
- disabled: rowDisabled,
17149
- selectable: true
17150
- })
17151
- }
17152
- )
17153
- }
17154
- )
17155
- ] })
17156
- ]
17093
+ )
17094
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
17095
+ "button",
17096
+ {
17097
+ type: "button",
17098
+ "data-select-all-row": true,
17099
+ id: focusedValue === "__select_all__" ? activeOptionId : void 0,
17100
+ "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
17101
+ "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
17102
+ className: cn(
17103
+ itemBase,
17104
+ focusedValue === "__select_all__" && "bg-[var(--cmp-listBoxItem-bg-hover)] text-[color:var(--cmp-listBoxItem-content-hover)]"
17105
+ ),
17106
+ style: { ...LIST_BOX_ITEM_PADDING_Y_RIGHT, paddingLeft: LIST_BOX_ITEM_PADDING_LEFT[1] },
17107
+ onMouseEnter: () => onHoverValue("__select_all__"),
17108
+ onMouseLeave: () => onHoverValue(null),
17109
+ onClick: onSelectAll,
17110
+ children: [
17111
+ /* @__PURE__ */ jsxRuntime.jsx(CheckboxBox, { state: selectAllSelected, focused: focusedValue === "__select_all__" }),
17112
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate", children: selectAllLabel })
17113
+ ]
17114
+ }
17115
+ ));
17116
+ const rowNodes = items.map((o, windowIndex) => {
17117
+ const idx = windowIndex + indexOffset;
17118
+ const padLeft = LIST_BOX_ITEM_PADDING_LEFT[o.depth === 1 ? 1 : o.depth === 2 ? 2 : 3];
17119
+ const rowKey = `${o.value}-${idx}`;
17120
+ if (tree) {
17121
+ const togglesLeaves = multi && o.isGroup;
17122
+ const leafValues = togglesLeaves ? groupLeafValues.get(idx) ?? [] : [];
17123
+ const total = leafValues.length;
17124
+ const selectedCount = leafValues.reduce((n, v) => selectedSet.has(v) ? n + 1 : n, 0);
17125
+ const isSelected2 = selectedSet.has(o.value);
17126
+ const checkState = togglesLeaves ? total === 0 || selectedCount === 0 ? false : selectedCount === total ? true : null : isSelected2;
17127
+ const rowDisabled = o.disabled || togglesLeaves && total === 0;
17128
+ const focusKey = togglesLeaves ? `__group_${idx}__` : o.value;
17129
+ const focused = focusedValue === focusKey;
17130
+ const expanded = o.isGroup && isExpanded(o.value);
17131
+ const line = treeLines[idx];
17132
+ const toggle = () => {
17133
+ if (rowDisabled) return;
17134
+ if (togglesLeaves) onToggleGroup(leafValues);
17135
+ else onSelect(o.value, idx, checkboxShiftRef.current);
17136
+ };
17137
+ return (
17138
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events -- 키보드는 트리거(또는 검색 입력)가 aria-activedescendant 로 맡는다. 줄로 포커스를 옮기지 않는다
17139
+ /* @__PURE__ */ jsxRuntime.jsxs(
17140
+ "div",
17141
+ {
17142
+ tabIndex: -1,
17143
+ "data-select-row": true,
17144
+ "data-index": idx,
17145
+ id: focused ? activeOptionId : void 0,
17146
+ role: "treeitem",
17147
+ "aria-level": o.depth,
17148
+ "aria-posinset": line?.posInSet,
17149
+ "aria-setsize": line?.setSize,
17150
+ "aria-expanded": o.isGroup ? expanded : void 0,
17151
+ "aria-selected": checkState === true,
17152
+ "aria-disabled": rowDisabled || void 0,
17153
+ "data-select-focused": focused ? "true" : void 0,
17154
+ "data-select-active": focused ? "true" : void 0,
17155
+ className: cn(
17156
+ TREE_ROW_CLASS,
17157
+ TREE_LABEL_TYPOGRAPHY.sm.rowHeight,
17158
+ TREE_ROW_BORDER_CLASS,
17159
+ "select-none",
17160
+ o.isGroup && "cursor-pointer",
17161
+ focused && TREE_ROW_ACTIVE_CLASS
17162
+ ),
17163
+ onMouseEnter: () => onHoverValue(focusKey),
17164
+ onMouseLeave: () => onHoverValue(null),
17165
+ onClick: (event) => {
17166
+ if (isRowControlClick(event)) return;
17167
+ if (isRowSelectClick(event)) {
17168
+ if (rowDisabled) return;
17169
+ if (togglesLeaves) onToggleGroup(leafValues);
17170
+ else onSelect(o.value, idx, event.shiftKey);
17171
+ return;
17172
+ }
17173
+ if (o.isGroup) onToggleExpand(o.value);
17157
17174
  },
17158
- rowKey
17159
- )
17160
- );
17161
- }
17162
- if (o.isGroup) {
17163
- const isDepth1 = o.depth === 1;
17164
- const isFirstRow = idx === 0 && !showSelectAll;
17165
- const groupTone = cn(
17166
- isDepth1 ? "bg-[var(--cmp-listBoxItem-depth1-bg-default)] font-bold" : "bg-[var(--cmp-listBoxItem-depth2-middle-bg)] font-medium",
17167
- isDepth1 && !isFirstRow && "border-t border-solid border-[color:var(--cmp-listBoxItem-depth1-border-color)]"
17168
- );
17169
- if (multi) {
17170
- const leafValues = groupLeafValues.get(idx) ?? [];
17171
- const total = leafValues.length;
17172
- const selectedCount = leafValues.reduce((n, v) => selectedSet.has(v) ? n + 1 : n, 0);
17173
- const groupState = total === 0 || selectedCount === 0 ? false : selectedCount === total ? true : null;
17174
- const groupDisabled = o.disabled || total === 0;
17175
- const groupFocused = focusedValue === `__group_${idx}__`;
17176
- return /* @__PURE__ */ jsxRuntime.jsxs(
17177
- "button",
17178
- {
17179
- type: "button",
17180
- "data-select-row": true,
17181
- "data-index": idx,
17182
- id: groupFocused ? activeOptionId : void 0,
17183
- role: "option",
17184
- "aria-selected": groupState === true,
17185
- disabled: groupDisabled,
17186
- "data-select-focused": groupFocused ? "true" : void 0,
17187
- "data-select-active": groupFocused ? "true" : void 0,
17188
- className: cn(
17189
- "flex w-full select-none items-center gap-(--cmp-listBoxItem-gap) border-none py-(--cmp-listBoxItem-paddingY) pr-(--cmp-listBoxItem-paddingRight) text-left typo-field-sm-default text-[color:var(--cmp-listBoxItem-content-default)]",
17190
- groupTone,
17191
- groupDisabled ? "cursor-not-allowed text-[color:var(--cmp-listBoxItem-content-disabled)]" : "cursor-pointer",
17192
- groupDisabled && isDepth1 && "bg-[var(--cmp-listBoxItem-depth1-bg-disabled)]",
17193
- // 포커스 하이라이트는 depth 배경보다 뒤에 둬 tailwind-merge 로 이기게 한다.
17194
- groupFocused && !groupDisabled && "bg-[var(--cmp-listBoxItem-bg-hover)] text-[color:var(--cmp-listBoxItem-content-hover)]"
17175
+ children: [
17176
+ /* @__PURE__ */ jsxRuntime.jsx(
17177
+ TreeConnector,
17178
+ {
17179
+ guideline: true,
17180
+ size: "sm",
17181
+ depth: o.depth,
17182
+ hasChildren: o.isGroup,
17183
+ isLast: line?.isLast ?? true,
17184
+ ancestorLast: line?.ancestorLast ?? []
17185
+ }
17195
17186
  ),
17196
- style: { paddingLeft: padLeft },
17197
- onMouseEnter: () => onHoverValue(`__group_${idx}__`),
17198
- onMouseLeave: () => onHoverValue(null),
17199
- onClick: () => onToggleGroup(leafValues),
17200
- children: [
17201
- /* @__PURE__ */ jsxRuntime.jsx(CheckboxBox, { state: groupState, disabled: groupDisabled, focused: groupFocused }),
17202
- /* @__PURE__ */ jsxRuntime.jsx(OptionLabel, { label: o.label, className: "min-w-0 truncate" }),
17203
- total > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
17204
- "span",
17187
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: TREE_CONTENT_CLASS, children: [
17188
+ o.isGroup ? (
17189
+ // 비활성 그룹도 접고 편다 — 그룹이 비활성이어도 아래에 고를 수 있는 자식이 있을 수 있다.
17190
+ /* @__PURE__ */ jsxRuntime.jsx(TreeCaret, { expanded, onToggle: () => onToggleExpand(o.value) })
17191
+ ) : /* @__PURE__ */ jsxRuntime.jsx(TreeCaretSpacer, {}),
17192
+ /* @__PURE__ */ jsxRuntime.jsx(
17193
+ TreeSelectZone,
17205
17194
  {
17206
- className: cn(
17207
- "shrink-0 font-medium",
17208
- groupFocused ? "text-white" : "text-[color:var(--cmp-listBoxItem-content-disabled)]"
17195
+ selectable: true,
17196
+ disabled: rowDisabled,
17197
+ control: multi && /* @__PURE__ */ jsxRuntime.jsx(
17198
+ "span",
17199
+ {
17200
+ "data-tree-control": true,
17201
+ className: "flex shrink-0",
17202
+ onPointerDownCapture: (event) => {
17203
+ checkboxShiftRef.current = event.shiftKey;
17204
+ },
17205
+ children: /* @__PURE__ */ jsxRuntime.jsx(SCheckbox, { value: checkState, disabled: rowDisabled, onValueChange: toggle })
17206
+ }
17209
17207
  ),
17210
- children: [
17211
- "(",
17212
- selectedCount,
17213
- "/",
17214
- total,
17215
- ")"
17216
- ]
17208
+ children: /* @__PURE__ */ jsxRuntime.jsx(
17209
+ OptionLabel,
17210
+ {
17211
+ label: o.label,
17212
+ className: treeLabelClass("sm", {
17213
+ // 고른 줄은 굵게 — multi_tree 는 일부만 고른 그룹도 굵다 (STree 와 같다)
17214
+ emphasized: checkState !== false,
17215
+ disabled: rowDisabled,
17216
+ selectable: true
17217
+ })
17218
+ }
17219
+ )
17217
17220
  }
17218
17221
  )
17219
- ]
17220
- },
17221
- rowKey
17222
- );
17223
- }
17224
- return /* @__PURE__ */ jsxRuntime.jsx(
17225
- "div",
17222
+ ] })
17223
+ ]
17224
+ },
17225
+ rowKey
17226
+ )
17227
+ );
17228
+ }
17229
+ if (o.isGroup) {
17230
+ const isDepth1 = o.depth === 1;
17231
+ const isFirstRow = idx === 0 && !showSelectAll;
17232
+ const groupTone = cn(
17233
+ isDepth1 ? "bg-[var(--cmp-listBoxItem-depth1-bg-default)] font-bold" : "bg-[var(--cmp-listBoxItem-depth2-middle-bg)] font-medium",
17234
+ isDepth1 && !isFirstRow && "border-t border-solid border-[color:var(--cmp-listBoxItem-depth1-border-color)]"
17235
+ );
17236
+ if (multi) {
17237
+ const leafValues = groupLeafValues.get(idx) ?? [];
17238
+ const total = leafValues.length;
17239
+ const selectedCount = leafValues.reduce((n, v) => selectedSet.has(v) ? n + 1 : n, 0);
17240
+ const groupState = total === 0 || selectedCount === 0 ? false : selectedCount === total ? true : null;
17241
+ const groupDisabled = o.disabled || total === 0;
17242
+ const groupFocused = focusedValue === `__group_${idx}__`;
17243
+ return /* @__PURE__ */ jsxRuntime.jsxs(
17244
+ "button",
17226
17245
  {
17246
+ type: "button",
17227
17247
  "data-select-row": true,
17228
17248
  "data-index": idx,
17249
+ id: groupFocused ? activeOptionId : void 0,
17250
+ role: "option",
17251
+ "aria-selected": groupState === true,
17252
+ disabled: groupDisabled,
17253
+ "data-select-focused": groupFocused ? "true" : void 0,
17254
+ "data-select-active": groupFocused ? "true" : void 0,
17229
17255
  className: cn(
17230
- "select-none py-(--cmp-listBoxItem-paddingY) pr-(--cmp-listBoxItem-paddingRight) typo-field-sm-default text-[color:var(--cmp-listBoxItem-content-default)]",
17256
+ "flex w-full select-none items-center gap-(--cmp-listBoxItem-gap) border-none py-(--cmp-listBoxItem-paddingY) pr-(--cmp-listBoxItem-paddingRight) text-left typo-field-sm-default text-[color:var(--cmp-listBoxItem-content-default)]",
17231
17257
  groupTone,
17232
- // 하위가 전부 비활성이면(select.config 에서 bubble-up) 어미 그룹도 비활성 처리:
17233
- // 텍스트는 회색(#888), depth1 그룹은 파란 배경 대신 회색(#EEE)으로 (depth2 는 #F6F6F6 유지)
17234
- o.disabled && "cursor-not-allowed text-[color:var(--cmp-listBoxItem-content-disabled)]",
17235
- o.disabled && isDepth1 && "bg-[var(--cmp-listBoxItem-depth1-bg-disabled)]"
17258
+ groupDisabled ? "cursor-not-allowed text-[color:var(--cmp-listBoxItem-content-disabled)]" : "cursor-pointer",
17259
+ groupDisabled && isDepth1 && "bg-[var(--cmp-listBoxItem-depth1-bg-disabled)]",
17260
+ // 포커스 하이라이트는 depth 배경보다 뒤에 둬 tailwind-merge 로 이기게 한다.
17261
+ groupFocused && !groupDisabled && "bg-[var(--cmp-listBoxItem-bg-hover)] text-[color:var(--cmp-listBoxItem-content-hover)]"
17236
17262
  ),
17237
17263
  style: { paddingLeft: padLeft },
17238
- children: o.label
17264
+ onMouseEnter: () => onHoverValue(`__group_${idx}__`),
17265
+ onMouseLeave: () => onHoverValue(null),
17266
+ onClick: () => onToggleGroup(leafValues),
17267
+ children: [
17268
+ /* @__PURE__ */ jsxRuntime.jsx(CheckboxBox, { state: groupState, disabled: groupDisabled, focused: groupFocused }),
17269
+ /* @__PURE__ */ jsxRuntime.jsx(OptionLabel, { label: o.label, className: "min-w-0 truncate" }),
17270
+ total > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
17271
+ "span",
17272
+ {
17273
+ className: cn(
17274
+ "shrink-0 font-medium",
17275
+ groupFocused ? "text-white" : "text-[color:var(--cmp-listBoxItem-content-disabled)]"
17276
+ ),
17277
+ children: [
17278
+ "(",
17279
+ selectedCount,
17280
+ "/",
17281
+ total,
17282
+ ")"
17283
+ ]
17284
+ }
17285
+ )
17286
+ ]
17239
17287
  },
17240
17288
  rowKey
17241
17289
  );
17242
17290
  }
17243
- const isSelected = selected.includes(o.value);
17244
- return /* @__PURE__ */ jsxRuntime.jsxs(
17245
- "button",
17291
+ return /* @__PURE__ */ jsxRuntime.jsx(
17292
+ "div",
17246
17293
  {
17247
- type: "button",
17248
17294
  "data-select-row": true,
17249
17295
  "data-index": idx,
17250
- id: focusedValue === o.value ? activeOptionId : void 0,
17251
- disabled: o.disabled,
17252
- role: "option",
17253
- "aria-selected": isSelected,
17254
- "aria-setsize": setSize,
17255
- "aria-posinset": setSize != null ? idx + 1 : void 0,
17256
- "data-select-focused": focusedValue === o.value ? "true" : void 0,
17257
- "data-select-active": focusedValue === o.value ? "true" : void 0,
17258
17296
  className: cn(
17259
- itemBase,
17260
- isSelected && !multi && "font-bold text-[color:var(--cmp-listBoxItem-content-selected)]",
17261
- focusedValue === o.value && "bg-[var(--cmp-listBoxItem-bg-hover)] text-[color:var(--cmp-listBoxItem-content-hover)]",
17262
- // 비활성 항목은 hover/focus 스타일 대상에서 제외한다.
17263
- // (:hover 는 disabled 버튼에도 매칭되므로 클래스 자체를 붙이지 않아야 한다)
17264
- !o.disabled && activeClass
17297
+ "select-none py-(--cmp-listBoxItem-paddingY) pr-(--cmp-listBoxItem-paddingRight) typo-field-sm-default text-[color:var(--cmp-listBoxItem-content-default)]",
17298
+ groupTone,
17299
+ // 하위가 전부 비활성이면(select.config 에서 bubble-up) 어미 그룹도 비활성 처리:
17300
+ // 텍스트는 회색(#888), depth1 그룹은 파란 배경 대신 회색(#EEE)으로 (depth2 는 #F6F6F6 유지)
17301
+ o.disabled && "cursor-not-allowed text-[color:var(--cmp-listBoxItem-content-disabled)]",
17302
+ o.disabled && isDepth1 && "bg-[var(--cmp-listBoxItem-depth1-bg-disabled)]"
17265
17303
  ),
17266
- style: { ...LIST_BOX_ITEM_PADDING_Y_RIGHT, paddingLeft: padLeft },
17267
- onMouseEnter: () => onHoverValue(o.value),
17268
- onMouseLeave: () => onHoverValue(null),
17269
- onClick: (event) => onSelect(o.value, idx, event.shiftKey),
17270
- children: [
17271
- multi && /* @__PURE__ */ jsxRuntime.jsx(
17272
- CheckboxBox,
17273
- {
17274
- state: isSelected,
17275
- disabled: o.disabled,
17276
- focused: focusedValue === o.value
17277
- }
17278
- ),
17279
- /* @__PURE__ */ jsxRuntime.jsx(OptionLabel, { label: o.label, className: "flex-1 truncate" })
17280
- ]
17304
+ style: { paddingLeft: padLeft },
17305
+ children: o.label
17281
17306
  },
17282
17307
  rowKey
17283
17308
  );
17284
- }),
17309
+ }
17310
+ const isSelected = selected.includes(o.value);
17311
+ return /* @__PURE__ */ jsxRuntime.jsxs(
17312
+ "button",
17313
+ {
17314
+ type: "button",
17315
+ "data-select-row": true,
17316
+ "data-index": idx,
17317
+ id: focusedValue === o.value ? activeOptionId : void 0,
17318
+ disabled: o.disabled,
17319
+ role: "option",
17320
+ "aria-selected": isSelected,
17321
+ "aria-setsize": setSize,
17322
+ "aria-posinset": setSize != null ? idx + 1 : void 0,
17323
+ "data-select-focused": focusedValue === o.value ? "true" : void 0,
17324
+ "data-select-active": focusedValue === o.value ? "true" : void 0,
17325
+ className: cn(
17326
+ itemBase,
17327
+ isSelected && !multi && "font-bold text-[color:var(--cmp-listBoxItem-content-selected)]",
17328
+ focusedValue === o.value && "bg-[var(--cmp-listBoxItem-bg-hover)] text-[color:var(--cmp-listBoxItem-content-hover)]",
17329
+ // 비활성 항목은 hover/focus 스타일 대상에서 제외한다.
17330
+ // (:hover 는 disabled 버튼에도 매칭되므로 클래스 자체를 붙이지 않아야 한다)
17331
+ !o.disabled && activeClass
17332
+ ),
17333
+ style: { ...LIST_BOX_ITEM_PADDING_Y_RIGHT, paddingLeft: padLeft },
17334
+ onMouseEnter: () => onHoverValue(o.value),
17335
+ onMouseLeave: () => onHoverValue(null),
17336
+ onClick: (event) => onSelect(o.value, idx, event.shiftKey),
17337
+ children: [
17338
+ multi && /* @__PURE__ */ jsxRuntime.jsx(
17339
+ CheckboxBox,
17340
+ {
17341
+ state: isSelected,
17342
+ disabled: o.disabled,
17343
+ focused: focusedValue === o.value
17344
+ }
17345
+ ),
17346
+ /* @__PURE__ */ jsxRuntime.jsx(OptionLabel, { label: o.label, className: "flex-1 truncate" })
17347
+ ]
17348
+ },
17349
+ rowKey
17350
+ );
17351
+ });
17352
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { id: listboxId, role: tree ? "tree" : "listbox", "aria-multiselectable": multi || void 0, children: [
17353
+ selectAllRow,
17354
+ topSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: { height: topSpacer } }),
17355
+ wrapTreeMotion(rowNodes),
17285
17356
  bottomSpacer > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: { height: bottomSpacer } })
17286
17357
  ] });
17287
17358
  }
17359
+ function TreeRowsMotion({ opening, children }) {
17360
+ const ref = react.useRef(null);
17361
+ const animationRef = react.useRef(null);
17362
+ const mountedRef = react.useRef(false);
17363
+ react.useLayoutEffect(() => {
17364
+ const el = ref.current;
17365
+ if (el == null || typeof el.animate !== "function") return;
17366
+ const from = !mountedRef.current && opening ? 0 : el.getBoundingClientRect().height;
17367
+ mountedRef.current = true;
17368
+ animationRef.current?.cancel();
17369
+ animationRef.current = el.animate(
17370
+ [{ height: `${from}px` }, { height: opening ? `${el.scrollHeight}px` : "0px" }],
17371
+ // 접힌 뒤 줄이 목록에서 빠질 때까지 0 으로 붙잡아 둔다. 편 뒤에는 제 높이(auto)로 돌아간다
17372
+ { ...TREE_BRANCH_MOTION, fill: opening ? "none" : "forwards" }
17373
+ );
17374
+ }, [opening]);
17375
+ react.useEffect(() => () => animationRef.current?.cancel(), []);
17376
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: "overflow-hidden", "aria-hidden": opening ? void 0 : true, children });
17377
+ }
17288
17378
  function OptionLabel({ label, className }) {
17289
17379
  return isHtmlLabel(label) ? /* @__PURE__ */ jsxRuntime.jsx("span", { className, dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(label) } }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className, children: label });
17290
17380
  }