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.js CHANGED
@@ -7702,6 +7702,13 @@ var TREE_LABEL_TYPOGRAPHY = {
7702
7702
  rowHeight: "h-[32px]"
7703
7703
  }
7704
7704
  };
7705
+ var TREE_BRANCH_MOTION = { duration: 200, easing: "cubic-bezier(0, 0, 0.2, 1)" };
7706
+ function canAnimateTreeBranch() {
7707
+ if (typeof window === "undefined" || typeof HTMLElement.prototype.animate !== "function") {
7708
+ return false;
7709
+ }
7710
+ return !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
7711
+ }
7705
7712
  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)";
7706
7713
  var TREE_ROW_BORDER_CLASS = "border-b-(length:--cmp-tree-item-border-width-default) border-(--cmp-tree-item-border-default)";
7707
7714
  var TREE_ROW_ACTIVE_CLASS = "bg-(--cmp-tree-item-bg-hover)";
@@ -16098,6 +16105,21 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16098
16105
  const [focusedIndex, setFocusedIndex] = useState(-1);
16099
16106
  const [expanded, setExpanded] = useState(() => /* @__PURE__ */ new Set());
16100
16107
  const [searchCollapsed, setSearchCollapsed] = useState(NO_SEARCH_COLLAPSED);
16108
+ const [treeMotion, setTreeMotion] = useState(null);
16109
+ const treeMotionTimerRef = useRef(null);
16110
+ const pendingCollapseRef = useRef(null);
16111
+ const cancelTreeMotion = () => {
16112
+ if (treeMotionTimerRef.current != null) clearTimeout(treeMotionTimerRef.current);
16113
+ treeMotionTimerRef.current = null;
16114
+ pendingCollapseRef.current = null;
16115
+ setTreeMotion(null);
16116
+ };
16117
+ useEffect(
16118
+ () => () => {
16119
+ if (treeMotionTimerRef.current != null) clearTimeout(treeMotionTimerRef.current);
16120
+ },
16121
+ []
16122
+ );
16101
16123
  const baseId = useId();
16102
16124
  const listboxId = `${baseId}-listbox`;
16103
16125
  const activeOptionId = `${baseId}-active-option`;
@@ -16165,6 +16187,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16165
16187
  const commitOpen = (next, valueForRules) => {
16166
16188
  setOpen(next);
16167
16189
  onOpenChange?.(next);
16190
+ cancelTreeMotion();
16168
16191
  if (next) {
16169
16192
  dismissedByPointerRef.current = false;
16170
16193
  alignSelectedTopRef.current = true;
@@ -16235,6 +16258,10 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16235
16258
  (groupValue) => treeSearching ? !searchCollapsed.has(groupValue) : expanded.has(groupValue),
16236
16259
  [expanded, searchCollapsed, treeSearching]
16237
16260
  );
16261
+ const isGroupShownExpanded = useCallback(
16262
+ (groupValue) => isGroupExpanded(groupValue) && !(treeMotion != null && !treeMotion.opening && treeMotion.group === groupValue),
16263
+ [isGroupExpanded, treeMotion]
16264
+ );
16238
16265
  const treeRows = useMemo(
16239
16266
  () => isTree ? visibleTreeRows(searched, (row) => isGroupExpanded(row.value)) : null,
16240
16267
  [isGroupExpanded, isTree, searched]
@@ -16524,6 +16551,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16524
16551
  const handleSearchChange = (next) => {
16525
16552
  setSearch(next);
16526
16553
  setSearchCollapsed(NO_SEARCH_COLLAPSED);
16554
+ cancelTreeMotion();
16527
16555
  };
16528
16556
  const handleToggleExpand = (groupValue) => {
16529
16557
  rangeAnchorRef.current = null;
@@ -16533,8 +16561,41 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16533
16561
  else next.add(groupValue);
16534
16562
  return next;
16535
16563
  };
16536
- if (treeSearching) setSearchCollapsed(toggle);
16537
- else setExpanded(toggle);
16564
+ const applyToggle = treeSearching ? () => setSearchCollapsed(toggle) : () => setExpanded(toggle);
16565
+ if (treeMotionTimerRef.current != null) clearTimeout(treeMotionTimerRef.current);
16566
+ treeMotionTimerRef.current = null;
16567
+ const pendingCollapse = pendingCollapseRef.current;
16568
+ pendingCollapseRef.current = null;
16569
+ if (treeMotion?.group === groupValue && !treeMotion.opening) {
16570
+ setTreeMotion({ group: groupValue, opening: true });
16571
+ treeMotionTimerRef.current = setTimeout(
16572
+ () => setTreeMotion(null),
16573
+ TREE_BRANCH_MOTION.duration
16574
+ );
16575
+ return;
16576
+ }
16577
+ pendingCollapse?.();
16578
+ if (!canAnimateTreeBranch()) {
16579
+ setTreeMotion(null);
16580
+ applyToggle();
16581
+ return;
16582
+ }
16583
+ if (isGroupExpanded(groupValue)) {
16584
+ setTreeMotion({ group: groupValue, opening: false });
16585
+ pendingCollapseRef.current = applyToggle;
16586
+ treeMotionTimerRef.current = setTimeout(() => {
16587
+ pendingCollapseRef.current = null;
16588
+ setTreeMotion(null);
16589
+ applyToggle();
16590
+ }, TREE_BRANCH_MOTION.duration);
16591
+ } else {
16592
+ applyToggle();
16593
+ setTreeMotion({ group: groupValue, opening: true });
16594
+ treeMotionTimerRef.current = setTimeout(
16595
+ () => setTreeMotion(null),
16596
+ TREE_BRANCH_MOTION.duration
16597
+ );
16598
+ }
16538
16599
  };
16539
16600
  const handleToggleGroup = (leafValues) => {
16540
16601
  if (leafValues.length === 0) return;
@@ -16644,7 +16705,7 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16644
16705
  const row = rowIndex != null ? filtered[rowIndex] : void 0;
16645
16706
  if (rowIndex == null || row == null) return;
16646
16707
  event.preventDefault();
16647
- const rowExpanded = row.isGroup && isGroupExpanded(row.value);
16708
+ const rowExpanded = row.isGroup && isGroupShownExpanded(row.value);
16648
16709
  if (event.key === "ArrowRight") {
16649
16710
  if (!row.isGroup) return;
16650
16711
  if (!rowExpanded) {
@@ -16889,8 +16950,9 @@ var SSelectImpl = /* @__PURE__ */ forwardRef(function SSelect({
16889
16950
  setSize: isDepth || isTree ? void 0 : rowCount,
16890
16951
  multi,
16891
16952
  tree: isTree,
16892
- isExpanded: isGroupExpanded,
16953
+ isExpanded: isGroupShownExpanded,
16893
16954
  onToggleExpand: handleToggleExpand,
16955
+ treeMotion,
16894
16956
  treeLines,
16895
16957
  selected,
16896
16958
  groupLeafValues,
@@ -16947,6 +17009,7 @@ function ItemList({
16947
17009
  tree,
16948
17010
  isExpanded,
16949
17011
  onToggleExpand,
17012
+ treeMotion,
16950
17013
  treeLines,
16951
17014
  selected,
16952
17015
  groupLeafValues,
@@ -16967,295 +17030,322 @@ function ItemList({
16967
17030
  "disabled:cursor-not-allowed disabled:text-[color:var(--cmp-listBoxItem-content-disabled)]"
16968
17031
  );
16969
17032
  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)]";
16970
- return /* @__PURE__ */ jsxs("div", { id: listboxId, role: tree ? "tree" : "listbox", "aria-multiselectable": multi || void 0, children: [
16971
- showSelectAll && (tree ? (
16972
- // 트리의 전체선택 줄은 STree 의 "전체" 줄 그대로다 (lib/tree-row).
16973
- /* @__PURE__ */ jsx(
16974
- TreeAllRow,
16975
- {
16976
- "data-select-all-row": true,
16977
- id: focusedValue === "__select_all__" ? activeOptionId : void 0,
16978
- "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
16979
- "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
16980
- tabIndex: -1,
16981
- label: selectAllLabel,
16982
- selected: selectAllSelected,
16983
- active: focusedValue === "__select_all__",
16984
- className: "select-none",
16985
- onMouseEnter: () => onHoverValue("__select_all__"),
16986
- onMouseLeave: () => onHoverValue(null),
16987
- onToggle: onSelectAll
16988
- }
16989
- )
16990
- ) : /* @__PURE__ */ jsxs(
16991
- "button",
17033
+ const wrapTreeMotion = (nodes) => {
17034
+ if (!tree || treeMotion == null) return nodes;
17035
+ const start = items.findIndex((o) => o.isGroup && o.value === treeMotion.group);
17036
+ if (start < 0) return nodes;
17037
+ let end = start + 1;
17038
+ while (end < items.length && items[end].depth > items[start].depth) end++;
17039
+ if (end === start + 1) return nodes;
17040
+ return [
17041
+ ...nodes.slice(0, start + 1),
17042
+ /* @__PURE__ */ jsx(TreeRowsMotion, { opening: treeMotion.opening, children: nodes.slice(start + 1, end) }, `__motion_${treeMotion.group}__`),
17043
+ ...nodes.slice(end)
17044
+ ];
17045
+ };
17046
+ const selectAllRow = showSelectAll && (tree ? (
17047
+ // 트리의 전체선택 줄은 STree 의 "전체" 줄 그대로다 (lib/tree-row).
17048
+ /* @__PURE__ */ jsx(
17049
+ TreeAllRow,
16992
17050
  {
16993
- type: "button",
16994
17051
  "data-select-all-row": true,
16995
17052
  id: focusedValue === "__select_all__" ? activeOptionId : void 0,
16996
17053
  "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
16997
17054
  "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
16998
- className: cn(
16999
- itemBase,
17000
- focusedValue === "__select_all__" && "bg-[var(--cmp-listBoxItem-bg-hover)] text-[color:var(--cmp-listBoxItem-content-hover)]"
17001
- ),
17002
- style: { ...LIST_BOX_ITEM_PADDING_Y_RIGHT, paddingLeft: LIST_BOX_ITEM_PADDING_LEFT[1] },
17055
+ tabIndex: -1,
17056
+ label: selectAllLabel,
17057
+ selected: selectAllSelected,
17058
+ active: focusedValue === "__select_all__",
17059
+ className: "select-none",
17003
17060
  onMouseEnter: () => onHoverValue("__select_all__"),
17004
17061
  onMouseLeave: () => onHoverValue(null),
17005
- onClick: onSelectAll,
17006
- children: [
17007
- /* @__PURE__ */ jsx(CheckboxBox, { state: selectAllSelected, focused: focusedValue === "__select_all__" }),
17008
- /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: selectAllLabel })
17009
- ]
17062
+ onToggle: onSelectAll
17010
17063
  }
17011
- )),
17012
- topSpacer > 0 && /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: { height: topSpacer } }),
17013
- items.map((o, windowIndex) => {
17014
- const idx = windowIndex + indexOffset;
17015
- const padLeft = LIST_BOX_ITEM_PADDING_LEFT[o.depth === 1 ? 1 : o.depth === 2 ? 2 : 3];
17016
- const rowKey = `${o.value}-${idx}`;
17017
- if (tree) {
17018
- const togglesLeaves = multi && o.isGroup;
17019
- const leafValues = togglesLeaves ? groupLeafValues.get(idx) ?? [] : [];
17020
- const total = leafValues.length;
17021
- const selectedCount = leafValues.reduce((n, v) => selectedSet.has(v) ? n + 1 : n, 0);
17022
- const isSelected2 = selectedSet.has(o.value);
17023
- const checkState = togglesLeaves ? total === 0 || selectedCount === 0 ? false : selectedCount === total ? true : null : isSelected2;
17024
- const rowDisabled = o.disabled || togglesLeaves && total === 0;
17025
- const focusKey = togglesLeaves ? `__group_${idx}__` : o.value;
17026
- const focused = focusedValue === focusKey;
17027
- const expanded = o.isGroup && isExpanded(o.value);
17028
- const line = treeLines[idx];
17029
- const toggle = () => {
17030
- if (rowDisabled) return;
17031
- if (togglesLeaves) onToggleGroup(leafValues);
17032
- else onSelect(o.value, idx, checkboxShiftRef.current);
17033
- };
17034
- return (
17035
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events -- 키보드는 트리거(또는 검색 입력)가 aria-activedescendant 로 맡는다. 줄로 포커스를 옮기지 않는다
17036
- /* @__PURE__ */ jsxs(
17037
- "div",
17038
- {
17039
- tabIndex: -1,
17040
- "data-select-row": true,
17041
- "data-index": idx,
17042
- id: focused ? activeOptionId : void 0,
17043
- role: "treeitem",
17044
- "aria-level": o.depth,
17045
- "aria-posinset": line?.posInSet,
17046
- "aria-setsize": line?.setSize,
17047
- "aria-expanded": o.isGroup ? expanded : void 0,
17048
- "aria-selected": checkState === true,
17049
- "aria-disabled": rowDisabled || void 0,
17050
- "data-select-focused": focused ? "true" : void 0,
17051
- "data-select-active": focused ? "true" : void 0,
17052
- className: cn(
17053
- TREE_ROW_CLASS,
17054
- TREE_LABEL_TYPOGRAPHY.sm.rowHeight,
17055
- TREE_ROW_BORDER_CLASS,
17056
- "select-none",
17057
- o.isGroup && "cursor-pointer",
17058
- focused && TREE_ROW_ACTIVE_CLASS
17059
- ),
17060
- onMouseEnter: () => onHoverValue(focusKey),
17061
- onMouseLeave: () => onHoverValue(null),
17062
- onClick: (event) => {
17063
- if (isRowControlClick(event)) return;
17064
- if (isRowSelectClick(event)) {
17065
- if (rowDisabled) return;
17066
- if (togglesLeaves) onToggleGroup(leafValues);
17067
- else onSelect(o.value, idx, event.shiftKey);
17068
- return;
17069
- }
17070
- if (o.isGroup) onToggleExpand(o.value);
17071
- },
17072
- children: [
17073
- /* @__PURE__ */ jsx(
17074
- TreeConnector,
17075
- {
17076
- guideline: true,
17077
- size: "sm",
17078
- depth: o.depth,
17079
- hasChildren: o.isGroup,
17080
- isLast: line?.isLast ?? true,
17081
- ancestorLast: line?.ancestorLast ?? []
17082
- }
17083
- ),
17084
- /* @__PURE__ */ jsxs("div", { className: TREE_CONTENT_CLASS, children: [
17085
- o.isGroup ? (
17086
- // 비활성 그룹도 접고 편다 — 그룹이 비활성이어도 아래에 고를 수 있는 자식이 있을 수 있다.
17087
- /* @__PURE__ */ jsx(TreeCaret, { expanded, onToggle: () => onToggleExpand(o.value) })
17088
- ) : /* @__PURE__ */ jsx(TreeCaretSpacer, {}),
17089
- /* @__PURE__ */ jsx(
17090
- TreeSelectZone,
17091
- {
17092
- selectable: true,
17093
- disabled: rowDisabled,
17094
- control: multi && /* @__PURE__ */ jsx(
17095
- "span",
17096
- {
17097
- "data-tree-control": true,
17098
- className: "flex shrink-0",
17099
- onPointerDownCapture: (event) => {
17100
- checkboxShiftRef.current = event.shiftKey;
17101
- },
17102
- children: /* @__PURE__ */ jsx(
17103
- SCheckbox,
17104
- {
17105
- value: checkState,
17106
- disabled: rowDisabled,
17107
- onValueChange: toggle
17108
- }
17109
- )
17110
- }
17111
- ),
17112
- children: /* @__PURE__ */ jsx(
17113
- OptionLabel,
17114
- {
17115
- label: o.label,
17116
- className: treeLabelClass("sm", {
17117
- // 고른 줄은 굵게 — multi_tree 는 일부만 고른 그룹도 굵다 (STree 와 같다)
17118
- emphasized: checkState !== false,
17119
- disabled: rowDisabled,
17120
- selectable: true
17121
- })
17122
- }
17123
- )
17124
- }
17125
- )
17126
- ] })
17127
- ]
17064
+ )
17065
+ ) : /* @__PURE__ */ jsxs(
17066
+ "button",
17067
+ {
17068
+ type: "button",
17069
+ "data-select-all-row": true,
17070
+ id: focusedValue === "__select_all__" ? activeOptionId : void 0,
17071
+ "data-select-focused": focusedValue === "__select_all__" ? "true" : void 0,
17072
+ "data-select-active": focusedValue === "__select_all__" ? "true" : void 0,
17073
+ className: cn(
17074
+ itemBase,
17075
+ focusedValue === "__select_all__" && "bg-[var(--cmp-listBoxItem-bg-hover)] text-[color:var(--cmp-listBoxItem-content-hover)]"
17076
+ ),
17077
+ style: { ...LIST_BOX_ITEM_PADDING_Y_RIGHT, paddingLeft: LIST_BOX_ITEM_PADDING_LEFT[1] },
17078
+ onMouseEnter: () => onHoverValue("__select_all__"),
17079
+ onMouseLeave: () => onHoverValue(null),
17080
+ onClick: onSelectAll,
17081
+ children: [
17082
+ /* @__PURE__ */ jsx(CheckboxBox, { state: selectAllSelected, focused: focusedValue === "__select_all__" }),
17083
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: selectAllLabel })
17084
+ ]
17085
+ }
17086
+ ));
17087
+ const rowNodes = items.map((o, windowIndex) => {
17088
+ const idx = windowIndex + indexOffset;
17089
+ const padLeft = LIST_BOX_ITEM_PADDING_LEFT[o.depth === 1 ? 1 : o.depth === 2 ? 2 : 3];
17090
+ const rowKey = `${o.value}-${idx}`;
17091
+ if (tree) {
17092
+ const togglesLeaves = multi && o.isGroup;
17093
+ const leafValues = togglesLeaves ? groupLeafValues.get(idx) ?? [] : [];
17094
+ const total = leafValues.length;
17095
+ const selectedCount = leafValues.reduce((n, v) => selectedSet.has(v) ? n + 1 : n, 0);
17096
+ const isSelected2 = selectedSet.has(o.value);
17097
+ const checkState = togglesLeaves ? total === 0 || selectedCount === 0 ? false : selectedCount === total ? true : null : isSelected2;
17098
+ const rowDisabled = o.disabled || togglesLeaves && total === 0;
17099
+ const focusKey = togglesLeaves ? `__group_${idx}__` : o.value;
17100
+ const focused = focusedValue === focusKey;
17101
+ const expanded = o.isGroup && isExpanded(o.value);
17102
+ const line = treeLines[idx];
17103
+ const toggle = () => {
17104
+ if (rowDisabled) return;
17105
+ if (togglesLeaves) onToggleGroup(leafValues);
17106
+ else onSelect(o.value, idx, checkboxShiftRef.current);
17107
+ };
17108
+ return (
17109
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events -- 키보드는 트리거(또는 검색 입력)가 aria-activedescendant 로 맡는다. 줄로 포커스를 옮기지 않는다
17110
+ /* @__PURE__ */ jsxs(
17111
+ "div",
17112
+ {
17113
+ tabIndex: -1,
17114
+ "data-select-row": true,
17115
+ "data-index": idx,
17116
+ id: focused ? activeOptionId : void 0,
17117
+ role: "treeitem",
17118
+ "aria-level": o.depth,
17119
+ "aria-posinset": line?.posInSet,
17120
+ "aria-setsize": line?.setSize,
17121
+ "aria-expanded": o.isGroup ? expanded : void 0,
17122
+ "aria-selected": checkState === true,
17123
+ "aria-disabled": rowDisabled || void 0,
17124
+ "data-select-focused": focused ? "true" : void 0,
17125
+ "data-select-active": focused ? "true" : void 0,
17126
+ className: cn(
17127
+ TREE_ROW_CLASS,
17128
+ TREE_LABEL_TYPOGRAPHY.sm.rowHeight,
17129
+ TREE_ROW_BORDER_CLASS,
17130
+ "select-none",
17131
+ o.isGroup && "cursor-pointer",
17132
+ focused && TREE_ROW_ACTIVE_CLASS
17133
+ ),
17134
+ onMouseEnter: () => onHoverValue(focusKey),
17135
+ onMouseLeave: () => onHoverValue(null),
17136
+ onClick: (event) => {
17137
+ if (isRowControlClick(event)) return;
17138
+ if (isRowSelectClick(event)) {
17139
+ if (rowDisabled) return;
17140
+ if (togglesLeaves) onToggleGroup(leafValues);
17141
+ else onSelect(o.value, idx, event.shiftKey);
17142
+ return;
17143
+ }
17144
+ if (o.isGroup) onToggleExpand(o.value);
17128
17145
  },
17129
- rowKey
17130
- )
17131
- );
17132
- }
17133
- if (o.isGroup) {
17134
- const isDepth1 = o.depth === 1;
17135
- const isFirstRow = idx === 0 && !showSelectAll;
17136
- const groupTone = cn(
17137
- isDepth1 ? "bg-[var(--cmp-listBoxItem-depth1-bg-default)] font-bold" : "bg-[var(--cmp-listBoxItem-depth2-middle-bg)] font-medium",
17138
- isDepth1 && !isFirstRow && "border-t border-solid border-[color:var(--cmp-listBoxItem-depth1-border-color)]"
17139
- );
17140
- if (multi) {
17141
- const leafValues = groupLeafValues.get(idx) ?? [];
17142
- const total = leafValues.length;
17143
- const selectedCount = leafValues.reduce((n, v) => selectedSet.has(v) ? n + 1 : n, 0);
17144
- const groupState = total === 0 || selectedCount === 0 ? false : selectedCount === total ? true : null;
17145
- const groupDisabled = o.disabled || total === 0;
17146
- const groupFocused = focusedValue === `__group_${idx}__`;
17147
- return /* @__PURE__ */ jsxs(
17148
- "button",
17149
- {
17150
- type: "button",
17151
- "data-select-row": true,
17152
- "data-index": idx,
17153
- id: groupFocused ? activeOptionId : void 0,
17154
- role: "option",
17155
- "aria-selected": groupState === true,
17156
- disabled: groupDisabled,
17157
- "data-select-focused": groupFocused ? "true" : void 0,
17158
- "data-select-active": groupFocused ? "true" : void 0,
17159
- className: cn(
17160
- "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)]",
17161
- groupTone,
17162
- groupDisabled ? "cursor-not-allowed text-[color:var(--cmp-listBoxItem-content-disabled)]" : "cursor-pointer",
17163
- groupDisabled && isDepth1 && "bg-[var(--cmp-listBoxItem-depth1-bg-disabled)]",
17164
- // 포커스 하이라이트는 depth 배경보다 뒤에 둬 tailwind-merge 로 이기게 한다.
17165
- groupFocused && !groupDisabled && "bg-[var(--cmp-listBoxItem-bg-hover)] text-[color:var(--cmp-listBoxItem-content-hover)]"
17146
+ children: [
17147
+ /* @__PURE__ */ jsx(
17148
+ TreeConnector,
17149
+ {
17150
+ guideline: true,
17151
+ size: "sm",
17152
+ depth: o.depth,
17153
+ hasChildren: o.isGroup,
17154
+ isLast: line?.isLast ?? true,
17155
+ ancestorLast: line?.ancestorLast ?? []
17156
+ }
17166
17157
  ),
17167
- style: { paddingLeft: padLeft },
17168
- onMouseEnter: () => onHoverValue(`__group_${idx}__`),
17169
- onMouseLeave: () => onHoverValue(null),
17170
- onClick: () => onToggleGroup(leafValues),
17171
- children: [
17172
- /* @__PURE__ */ jsx(CheckboxBox, { state: groupState, disabled: groupDisabled, focused: groupFocused }),
17173
- /* @__PURE__ */ jsx(OptionLabel, { label: o.label, className: "min-w-0 truncate" }),
17174
- total > 0 && /* @__PURE__ */ jsxs(
17175
- "span",
17158
+ /* @__PURE__ */ jsxs("div", { className: TREE_CONTENT_CLASS, children: [
17159
+ o.isGroup ? (
17160
+ // 비활성 그룹도 접고 편다 — 그룹이 비활성이어도 아래에 고를 수 있는 자식이 있을 수 있다.
17161
+ /* @__PURE__ */ jsx(TreeCaret, { expanded, onToggle: () => onToggleExpand(o.value) })
17162
+ ) : /* @__PURE__ */ jsx(TreeCaretSpacer, {}),
17163
+ /* @__PURE__ */ jsx(
17164
+ TreeSelectZone,
17176
17165
  {
17177
- className: cn(
17178
- "shrink-0 font-medium",
17179
- groupFocused ? "text-white" : "text-[color:var(--cmp-listBoxItem-content-disabled)]"
17166
+ selectable: true,
17167
+ disabled: rowDisabled,
17168
+ control: multi && /* @__PURE__ */ jsx(
17169
+ "span",
17170
+ {
17171
+ "data-tree-control": true,
17172
+ className: "flex shrink-0",
17173
+ onPointerDownCapture: (event) => {
17174
+ checkboxShiftRef.current = event.shiftKey;
17175
+ },
17176
+ children: /* @__PURE__ */ jsx(SCheckbox, { value: checkState, disabled: rowDisabled, onValueChange: toggle })
17177
+ }
17180
17178
  ),
17181
- children: [
17182
- "(",
17183
- selectedCount,
17184
- "/",
17185
- total,
17186
- ")"
17187
- ]
17179
+ children: /* @__PURE__ */ jsx(
17180
+ OptionLabel,
17181
+ {
17182
+ label: o.label,
17183
+ className: treeLabelClass("sm", {
17184
+ // 고른 줄은 굵게 — multi_tree 는 일부만 고른 그룹도 굵다 (STree 와 같다)
17185
+ emphasized: checkState !== false,
17186
+ disabled: rowDisabled,
17187
+ selectable: true
17188
+ })
17189
+ }
17190
+ )
17188
17191
  }
17189
17192
  )
17190
- ]
17191
- },
17192
- rowKey
17193
- );
17194
- }
17195
- return /* @__PURE__ */ jsx(
17196
- "div",
17193
+ ] })
17194
+ ]
17195
+ },
17196
+ rowKey
17197
+ )
17198
+ );
17199
+ }
17200
+ if (o.isGroup) {
17201
+ const isDepth1 = o.depth === 1;
17202
+ const isFirstRow = idx === 0 && !showSelectAll;
17203
+ const groupTone = cn(
17204
+ isDepth1 ? "bg-[var(--cmp-listBoxItem-depth1-bg-default)] font-bold" : "bg-[var(--cmp-listBoxItem-depth2-middle-bg)] font-medium",
17205
+ isDepth1 && !isFirstRow && "border-t border-solid border-[color:var(--cmp-listBoxItem-depth1-border-color)]"
17206
+ );
17207
+ if (multi) {
17208
+ const leafValues = groupLeafValues.get(idx) ?? [];
17209
+ const total = leafValues.length;
17210
+ const selectedCount = leafValues.reduce((n, v) => selectedSet.has(v) ? n + 1 : n, 0);
17211
+ const groupState = total === 0 || selectedCount === 0 ? false : selectedCount === total ? true : null;
17212
+ const groupDisabled = o.disabled || total === 0;
17213
+ const groupFocused = focusedValue === `__group_${idx}__`;
17214
+ return /* @__PURE__ */ jsxs(
17215
+ "button",
17197
17216
  {
17217
+ type: "button",
17198
17218
  "data-select-row": true,
17199
17219
  "data-index": idx,
17220
+ id: groupFocused ? activeOptionId : void 0,
17221
+ role: "option",
17222
+ "aria-selected": groupState === true,
17223
+ disabled: groupDisabled,
17224
+ "data-select-focused": groupFocused ? "true" : void 0,
17225
+ "data-select-active": groupFocused ? "true" : void 0,
17200
17226
  className: cn(
17201
- "select-none py-(--cmp-listBoxItem-paddingY) pr-(--cmp-listBoxItem-paddingRight) typo-field-sm-default text-[color:var(--cmp-listBoxItem-content-default)]",
17227
+ "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)]",
17202
17228
  groupTone,
17203
- // 하위가 전부 비활성이면(select.config 에서 bubble-up) 어미 그룹도 비활성 처리:
17204
- // 텍스트는 회색(#888), depth1 그룹은 파란 배경 대신 회색(#EEE)으로 (depth2 는 #F6F6F6 유지)
17205
- o.disabled && "cursor-not-allowed text-[color:var(--cmp-listBoxItem-content-disabled)]",
17206
- o.disabled && isDepth1 && "bg-[var(--cmp-listBoxItem-depth1-bg-disabled)]"
17229
+ groupDisabled ? "cursor-not-allowed text-[color:var(--cmp-listBoxItem-content-disabled)]" : "cursor-pointer",
17230
+ groupDisabled && isDepth1 && "bg-[var(--cmp-listBoxItem-depth1-bg-disabled)]",
17231
+ // 포커스 하이라이트는 depth 배경보다 뒤에 둬 tailwind-merge 로 이기게 한다.
17232
+ groupFocused && !groupDisabled && "bg-[var(--cmp-listBoxItem-bg-hover)] text-[color:var(--cmp-listBoxItem-content-hover)]"
17207
17233
  ),
17208
17234
  style: { paddingLeft: padLeft },
17209
- children: o.label
17235
+ onMouseEnter: () => onHoverValue(`__group_${idx}__`),
17236
+ onMouseLeave: () => onHoverValue(null),
17237
+ onClick: () => onToggleGroup(leafValues),
17238
+ children: [
17239
+ /* @__PURE__ */ jsx(CheckboxBox, { state: groupState, disabled: groupDisabled, focused: groupFocused }),
17240
+ /* @__PURE__ */ jsx(OptionLabel, { label: o.label, className: "min-w-0 truncate" }),
17241
+ total > 0 && /* @__PURE__ */ jsxs(
17242
+ "span",
17243
+ {
17244
+ className: cn(
17245
+ "shrink-0 font-medium",
17246
+ groupFocused ? "text-white" : "text-[color:var(--cmp-listBoxItem-content-disabled)]"
17247
+ ),
17248
+ children: [
17249
+ "(",
17250
+ selectedCount,
17251
+ "/",
17252
+ total,
17253
+ ")"
17254
+ ]
17255
+ }
17256
+ )
17257
+ ]
17210
17258
  },
17211
17259
  rowKey
17212
17260
  );
17213
17261
  }
17214
- const isSelected = selected.includes(o.value);
17215
- return /* @__PURE__ */ jsxs(
17216
- "button",
17262
+ return /* @__PURE__ */ jsx(
17263
+ "div",
17217
17264
  {
17218
- type: "button",
17219
17265
  "data-select-row": true,
17220
17266
  "data-index": idx,
17221
- id: focusedValue === o.value ? activeOptionId : void 0,
17222
- disabled: o.disabled,
17223
- role: "option",
17224
- "aria-selected": isSelected,
17225
- "aria-setsize": setSize,
17226
- "aria-posinset": setSize != null ? idx + 1 : void 0,
17227
- "data-select-focused": focusedValue === o.value ? "true" : void 0,
17228
- "data-select-active": focusedValue === o.value ? "true" : void 0,
17229
17267
  className: cn(
17230
- itemBase,
17231
- isSelected && !multi && "font-bold text-[color:var(--cmp-listBoxItem-content-selected)]",
17232
- focusedValue === o.value && "bg-[var(--cmp-listBoxItem-bg-hover)] text-[color:var(--cmp-listBoxItem-content-hover)]",
17233
- // 비활성 항목은 hover/focus 스타일 대상에서 제외한다.
17234
- // (:hover 는 disabled 버튼에도 매칭되므로 클래스 자체를 붙이지 않아야 한다)
17235
- !o.disabled && activeClass
17268
+ "select-none py-(--cmp-listBoxItem-paddingY) pr-(--cmp-listBoxItem-paddingRight) typo-field-sm-default text-[color:var(--cmp-listBoxItem-content-default)]",
17269
+ groupTone,
17270
+ // 하위가 전부 비활성이면(select.config 에서 bubble-up) 어미 그룹도 비활성 처리:
17271
+ // 텍스트는 회색(#888), depth1 그룹은 파란 배경 대신 회색(#EEE)으로 (depth2 는 #F6F6F6 유지)
17272
+ o.disabled && "cursor-not-allowed text-[color:var(--cmp-listBoxItem-content-disabled)]",
17273
+ o.disabled && isDepth1 && "bg-[var(--cmp-listBoxItem-depth1-bg-disabled)]"
17236
17274
  ),
17237
- style: { ...LIST_BOX_ITEM_PADDING_Y_RIGHT, paddingLeft: padLeft },
17238
- onMouseEnter: () => onHoverValue(o.value),
17239
- onMouseLeave: () => onHoverValue(null),
17240
- onClick: (event) => onSelect(o.value, idx, event.shiftKey),
17241
- children: [
17242
- multi && /* @__PURE__ */ jsx(
17243
- CheckboxBox,
17244
- {
17245
- state: isSelected,
17246
- disabled: o.disabled,
17247
- focused: focusedValue === o.value
17248
- }
17249
- ),
17250
- /* @__PURE__ */ jsx(OptionLabel, { label: o.label, className: "flex-1 truncate" })
17251
- ]
17275
+ style: { paddingLeft: padLeft },
17276
+ children: o.label
17252
17277
  },
17253
17278
  rowKey
17254
17279
  );
17255
- }),
17280
+ }
17281
+ const isSelected = selected.includes(o.value);
17282
+ return /* @__PURE__ */ jsxs(
17283
+ "button",
17284
+ {
17285
+ type: "button",
17286
+ "data-select-row": true,
17287
+ "data-index": idx,
17288
+ id: focusedValue === o.value ? activeOptionId : void 0,
17289
+ disabled: o.disabled,
17290
+ role: "option",
17291
+ "aria-selected": isSelected,
17292
+ "aria-setsize": setSize,
17293
+ "aria-posinset": setSize != null ? idx + 1 : void 0,
17294
+ "data-select-focused": focusedValue === o.value ? "true" : void 0,
17295
+ "data-select-active": focusedValue === o.value ? "true" : void 0,
17296
+ className: cn(
17297
+ itemBase,
17298
+ isSelected && !multi && "font-bold text-[color:var(--cmp-listBoxItem-content-selected)]",
17299
+ focusedValue === o.value && "bg-[var(--cmp-listBoxItem-bg-hover)] text-[color:var(--cmp-listBoxItem-content-hover)]",
17300
+ // 비활성 항목은 hover/focus 스타일 대상에서 제외한다.
17301
+ // (:hover 는 disabled 버튼에도 매칭되므로 클래스 자체를 붙이지 않아야 한다)
17302
+ !o.disabled && activeClass
17303
+ ),
17304
+ style: { ...LIST_BOX_ITEM_PADDING_Y_RIGHT, paddingLeft: padLeft },
17305
+ onMouseEnter: () => onHoverValue(o.value),
17306
+ onMouseLeave: () => onHoverValue(null),
17307
+ onClick: (event) => onSelect(o.value, idx, event.shiftKey),
17308
+ children: [
17309
+ multi && /* @__PURE__ */ jsx(
17310
+ CheckboxBox,
17311
+ {
17312
+ state: isSelected,
17313
+ disabled: o.disabled,
17314
+ focused: focusedValue === o.value
17315
+ }
17316
+ ),
17317
+ /* @__PURE__ */ jsx(OptionLabel, { label: o.label, className: "flex-1 truncate" })
17318
+ ]
17319
+ },
17320
+ rowKey
17321
+ );
17322
+ });
17323
+ return /* @__PURE__ */ jsxs("div", { id: listboxId, role: tree ? "tree" : "listbox", "aria-multiselectable": multi || void 0, children: [
17324
+ selectAllRow,
17325
+ topSpacer > 0 && /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: { height: topSpacer } }),
17326
+ wrapTreeMotion(rowNodes),
17256
17327
  bottomSpacer > 0 && /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: { height: bottomSpacer } })
17257
17328
  ] });
17258
17329
  }
17330
+ function TreeRowsMotion({ opening, children }) {
17331
+ const ref = useRef(null);
17332
+ const animationRef = useRef(null);
17333
+ const mountedRef = useRef(false);
17334
+ useLayoutEffect(() => {
17335
+ const el = ref.current;
17336
+ if (el == null || typeof el.animate !== "function") return;
17337
+ const from = !mountedRef.current && opening ? 0 : el.getBoundingClientRect().height;
17338
+ mountedRef.current = true;
17339
+ animationRef.current?.cancel();
17340
+ animationRef.current = el.animate(
17341
+ [{ height: `${from}px` }, { height: opening ? `${el.scrollHeight}px` : "0px" }],
17342
+ // 접힌 뒤 줄이 목록에서 빠질 때까지 0 으로 붙잡아 둔다. 편 뒤에는 제 높이(auto)로 돌아간다
17343
+ { ...TREE_BRANCH_MOTION, fill: opening ? "none" : "forwards" }
17344
+ );
17345
+ }, [opening]);
17346
+ useEffect(() => () => animationRef.current?.cancel(), []);
17347
+ return /* @__PURE__ */ jsx("div", { ref, className: "overflow-hidden", "aria-hidden": opening ? void 0 : true, children });
17348
+ }
17259
17349
  function OptionLabel({ label, className }) {
17260
17350
  return isHtmlLabel(label) ? /* @__PURE__ */ jsx("span", { className, dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(label) } }) : /* @__PURE__ */ jsx("span", { className, children: label });
17261
17351
  }