sellmate-design-system-react 9.0.0-beta.41 → 9.0.0-beta.43

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.
@@ -13,7 +13,7 @@
13
13
  | `message?` | `SGuideMessage` | `''` | 팝업 메시지 (배열이면 리스트, 중첩 배열이면 depth 표현) |
14
14
  | `url?` | `string` | `''` | notion 타입 클릭 시 이동 URL |
15
15
  | `popupTitle?` | `string` | `''` | 팝업 제목 (없으면 type 기본값) |
16
- | `popupWidth?` | `number` | | 팝업 너비 (px, 기본 426). 창보다 넓게 줘도폭까지만 벌어진다. |
16
+ | `popupWidth?` | `number \| 'auto'` | `'auto'` | 팝업 너비. 기본 `'auto'` 는 내용 폭에 맞춘다 — 가장 긴 줄이 곧 팝업 폭이다. 숫자(px)를 주면 폭으로 고정한다. 어느 쪽이든 좌우에 여백을 남기는 선까지만 벌어진다. |
17
17
  | `className?` | `string` | — | |
18
18
  | `style?` | `CSSProperties` | — | |
19
19
 
@@ -13,8 +13,11 @@ export interface SGuideProps {
13
13
  url?: string;
14
14
  /** 팝업 제목 (없으면 type 기본값) */
15
15
  popupTitle?: string;
16
- /** 팝업 너비 (px, 기본 426). 창보다 넓게 줘도 창 폭까지만 벌어진다. */
17
- popupWidth?: number;
16
+ /**
17
+ * 팝업 너비. 기본 `'auto'` 는 내용 폭에 맞춘다 — 가장 긴 줄이 곧 팝업 폭이다.
18
+ * 숫자(px)를 주면 그 폭으로 고정한다. 어느 쪽이든 창 좌우에 여백을 남기는 선까지만 벌어진다.
19
+ */
20
+ popupWidth?: number | 'auto';
18
21
  className?: string;
19
22
  style?: CSSProperties;
20
23
  }
package/dist/index.cjs CHANGED
@@ -5103,7 +5103,11 @@ var STag = /* @__PURE__ */ react.forwardRef(function STag2({
5103
5103
  ...rest,
5104
5104
  children: [
5105
5105
  icon && iconLeft && iconNode,
5106
- label && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex min-w-0 items-center justify-center", children: label }),
5106
+ label && // leading-none: 태그는 min-height 높이가 정해지고 flex 세로 중앙 정렬하므로
5107
+ // 레이블에 line-height 가 필요 없다. 오히려 line-height 를 주면 브라우저가
5108
+ // 폰트 ascent/descent 를 정수 px 로 반올림한 뒤 남는 half-leading 을 내림 처리해
5109
+ // 베이스라인이 최대 1px 위로 밀린다(xs 에서 특히 눈에 띈다).
5110
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex min-w-0 items-center justify-center leading-none", children: label }),
5107
5111
  icon && !iconLeft && iconNode
5108
5112
  ]
5109
5113
  }
@@ -7875,6 +7879,8 @@ var SPopup = /* @__PURE__ */ react.forwardRef(function SPopup2({
7875
7879
  });
7876
7880
  var DEFAULT_LABEL = { tip: "\uD65C\uC6A9 TIP", notion: "\uC0AC\uC6A9\uBC95 \uC548\uB0B4" };
7877
7881
  var ICON = { tip: "helpOutline", notion: "notion" };
7882
+ var VIEWPORT_GUTTER = 24;
7883
+ var CLOSE_INSET = 12;
7878
7884
  function flattenMessages(message, depth = 0) {
7879
7885
  if (Array.isArray(message)) return message.flatMap((m) => flattenMessages(m, depth + 1));
7880
7886
  return message !== "" ? [{ text: message, depth }] : [];
@@ -7885,13 +7891,14 @@ function SGuide({
7885
7891
  message = "",
7886
7892
  url = "",
7887
7893
  popupTitle = "",
7888
- popupWidth,
7894
+ popupWidth = "auto",
7889
7895
  className,
7890
7896
  style
7891
7897
  }) {
7892
7898
  const [open, setOpen] = react.useState(false);
7893
7899
  const portalContainer = usePortalContainer();
7894
7900
  const isNotion = type === "notion";
7901
+ const isAutoWidth = popupWidth === "auto";
7895
7902
  const defaultLabel = DEFAULT_LABEL[type];
7896
7903
  const iconName = ICON[type];
7897
7904
  const messages = flattenMessages(message);
@@ -7931,30 +7938,45 @@ function SGuide({
7931
7938
  side: "bottom",
7932
7939
  align: "start",
7933
7940
  sideOffset: 4,
7941
+ collisionPadding: VIEWPORT_GUTTER,
7934
7942
  className: cn(
7935
7943
  "relative z-50 rounded-[var(--cmp-guide-contents-radius)] bg-[var(--color-white)]",
7936
- // popupWidth(기본 426px)가 창보다 넓으면 창 폭에 맞춘다 — max-width 가 width 를 이긴다.
7937
- "max-w-[var(--radix-popover-content-available-width)]",
7938
7944
  FLOATING_SLIDE_ANIM
7939
7945
  ),
7940
7946
  style: {
7941
- width: popupWidth != null ? popupWidth : 426,
7947
+ width: isAutoWidth ? "max-content" : popupWidth,
7948
+ // 유일한 폭 상한. auto 든 고정 폭이든 창 좌우 여백을 먹고 들어가지 않는다.
7949
+ // 트리거 위치가 아니라 창 기준이라, 오른쪽 끝 트리거에서도 폭이 깎이지 않는다
7950
+ // (넘치면 Radix 가 왼쪽으로 민다). 100vw 는 세로 스크롤바를 포함하지만
7951
+ // 여백이 그보다 넉넉해 화면 밖으로 나가지 않는다.
7952
+ maxWidth: `calc(100vw - ${VIEWPORT_GUTTER * 2}px)`,
7942
7953
  padding: "var(--cmp-guide-contents-paddingY) var(--cmp-guide-contents-paddingX)",
7943
7954
  boxShadow: "4px 4px 24px 4px rgba(0,0,0,0.1)"
7944
7955
  },
7945
7956
  children: [
7946
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute right-[12px] top-[12px]", children: /* @__PURE__ */ jsxRuntime.jsx(SGhostButton, { icon: "close", size: "sm", ariaLabel: "close", onClick: () => setOpen(false) }) }),
7947
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-[var(--cmp-guide-contents-title-gap)]", children: [
7948
- /* @__PURE__ */ jsxRuntime.jsx(
7949
- SIcon,
7950
- {
7951
- name: iconName,
7952
- size: 24,
7953
- color: isNotion ? "var(--sys-color-fg-primary)" : "var(--cmp-guide-contents-icon)"
7954
- }
7955
- ),
7956
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "m-0 text-[16px] font-bold leading-[26px] text-[color:var(--cmp-guide-contents-typography-color)]", children: popupTitle || defaultLabel })
7957
- ] }),
7957
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute", style: { top: CLOSE_INSET, right: CLOSE_INSET }, children: /* @__PURE__ */ jsxRuntime.jsx(SGhostButton, { icon: "close", size: "sm", ariaLabel: "close", onClick: () => setOpen(false) }) }),
7958
+ /* @__PURE__ */ jsxRuntime.jsxs(
7959
+ "div",
7960
+ {
7961
+ className: "flex items-center gap-[var(--cmp-guide-contents-title-gap)]",
7962
+ style: {
7963
+ // 닫기 버튼은 absolute 라 폭 계산에 안 들어간다. 폭이 내용에 붙는 auto 에서 제목이
7964
+ // 버튼 밑으로 들어가지 않도록, 버튼이 콘텐츠 영역을 침범하는 만큼을 미리 비운다.
7965
+ paddingRight: `max(0px, calc(var(--cmp-ghostButton-sm-size) + ${CLOSE_INSET}px + var(--cmp-guide-contents-title-gap) - var(--cmp-guide-contents-paddingX)))`
7966
+ },
7967
+ children: [
7968
+ /* @__PURE__ */ jsxRuntime.jsx(
7969
+ SIcon,
7970
+ {
7971
+ name: iconName,
7972
+ size: 24,
7973
+ color: isNotion ? "var(--sys-color-fg-primary)" : "var(--cmp-guide-contents-icon)"
7974
+ }
7975
+ ),
7976
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "m-0 break-keep text-[16px] font-bold leading-[26px] text-[color:var(--cmp-guide-contents-typography-color)]", children: popupTitle || defaultLabel })
7977
+ ]
7978
+ }
7979
+ ),
7958
7980
  /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "m-0 mt-[var(--cmp-guide-contents-gap)] flex w-full list-none flex-col gap-[var(--cmp-guide-contents-body-gap)] p-0 text-[12px] leading-[20px] text-[color:var(--cmp-guide-contents-typography-color)]", children: messages.map((m, i) => /* @__PURE__ */ jsxRuntime.jsx(
7959
7981
  "li",
7960
7982
  {
@@ -7965,7 +7987,7 @@ function SGuide({
7965
7987
  children: /* @__PURE__ */ jsxRuntime.jsx(
7966
7988
  "p",
7967
7989
  {
7968
- className: "m-0 w-full min-w-0 break-words",
7990
+ className: "m-0 w-full min-w-0 break-keep break-words text-pretty",
7969
7991
  dangerouslySetInnerHTML: { __html: sanitizeInlineHtml(m.text) }
7970
7992
  }
7971
7993
  )
@@ -12764,6 +12786,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12764
12786
  const dismissedByPointerRef = react.useRef(false);
12765
12787
  const alignSelectedTopRef = react.useRef(false);
12766
12788
  const selectedAnchorRef = react.useRef(-1);
12789
+ const rangeAnchorRef = react.useRef(null);
12767
12790
  const flat = react.useMemo(() => flattenOptions(options), [options]);
12768
12791
  const selectable = react.useMemo(() => selectableOptions(flat), [flat]);
12769
12792
  const hasLeaf = react.useMemo(() => flat.some((o) => !o.isGroup), [flat]);
@@ -12864,7 +12887,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12864
12887
  if (leafValues.length === 0) return;
12865
12888
  items.push({ key: `__group_${i}__`, isGroup: true, leafValues });
12866
12889
  } else {
12867
- items.push({ key: o.value, value: o.value });
12890
+ items.push({ key: o.value, value: o.value, index: i });
12868
12891
  }
12869
12892
  });
12870
12893
  return items;
@@ -12889,20 +12912,36 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12889
12912
  emit3([]);
12890
12913
  if (rules && rules.length > 0) setRuleError(runRules(multi ? [] : "", rules));
12891
12914
  };
12892
- const handleSelect = (nextValue) => {
12915
+ const handleSelect = (nextValue, index, extend = false) => {
12893
12916
  if (multi) {
12894
- const next = selected.includes(nextValue) ? selected.filter((v) => v !== nextValue) : [...selected, nextValue];
12895
- emit3(next);
12917
+ const anchor = rangeAnchorRef.current;
12918
+ if (extend && anchor != null && anchor !== index && filtered[anchor]?.isGroup === false) {
12919
+ const [from, to] = anchor < index ? [anchor, index] : [index, anchor];
12920
+ const selectedSet = new Set(selected);
12921
+ const shouldSelect = selectedSet.has(filtered[anchor].value);
12922
+ const rangeValues = filtered.slice(from, to + 1).filter((o) => !o.isGroup && !o.disabled).map((o) => o.value);
12923
+ const inRange = new Set(rangeValues);
12924
+ emit3(
12925
+ shouldSelect ? [...selected, ...rangeValues.filter((v) => !selectedSet.has(v))] : selected.filter((v) => !inRange.has(v))
12926
+ );
12927
+ return;
12928
+ }
12929
+ rangeAnchorRef.current = index;
12930
+ emit3(
12931
+ selected.includes(nextValue) ? selected.filter((v) => v !== nextValue) : [...selected, nextValue]
12932
+ );
12896
12933
  return;
12897
12934
  }
12898
12935
  emit3([nextValue]);
12899
12936
  commitOpen(false, [nextValue]);
12900
12937
  };
12901
12938
  const handleSelectAll = () => {
12939
+ rangeAnchorRef.current = null;
12902
12940
  emit3(allSelected ? [] : allValues);
12903
12941
  };
12904
12942
  const handleToggleGroup = (leafValues) => {
12905
12943
  if (leafValues.length === 0) return;
12944
+ rangeAnchorRef.current = null;
12906
12945
  const selectedSet = new Set(selected);
12907
12946
  const allSelectedInGroup = leafValues.every((v) => selectedSet.has(v));
12908
12947
  if (allSelectedInGroup) {
@@ -12920,6 +12959,9 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12920
12959
  const anchor = alignSelectedTopRef.current ? selectedAnchorRef.current : -1;
12921
12960
  setFocusedIndex(anchor >= 0 ? anchor : 0);
12922
12961
  }, [open, search, navigableItems.length]);
12962
+ react.useEffect(() => {
12963
+ rangeAnchorRef.current = null;
12964
+ }, [open, search]);
12923
12965
  react.useLayoutEffect(() => {
12924
12966
  if (!open || focusedIndex < 0 || !listEl) return;
12925
12967
  const focusedEl = listEl.querySelector('[data-select-focused="true"]');
@@ -12947,7 +12989,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12947
12989
  return current <= 0 ? navigableItems.length - 1 : current - 1;
12948
12990
  });
12949
12991
  };
12950
- const selectFocusedItem = () => {
12992
+ const selectFocusedItem = (extend = false) => {
12951
12993
  if (focusedIndex < 0 || focusedIndex >= navigableItems.length) return;
12952
12994
  const focused = navigableItems[focusedIndex];
12953
12995
  if (focused.isSelectAll) {
@@ -12958,7 +13000,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12958
13000
  handleToggleGroup(focused.leafValues ?? []);
12959
13001
  return;
12960
13002
  }
12961
- handleSelect(focused.value);
13003
+ handleSelect(focused.value, focused.index, extend);
12962
13004
  };
12963
13005
  const handleKeyDown = (event) => {
12964
13006
  if (!open) {
@@ -12980,7 +13022,7 @@ var SSelectImpl = /* @__PURE__ */ react.forwardRef(function SSelect({
12980
13022
  }
12981
13023
  if (event.key === "Enter") {
12982
13024
  event.preventDefault();
12983
- selectFocusedItem();
13025
+ selectFocusedItem(event.shiftKey);
12984
13026
  return;
12985
13027
  }
12986
13028
  if (event.key === "Escape") {
@@ -13325,7 +13367,7 @@ function ItemList({
13325
13367
  style: { paddingTop: 4, paddingBottom: 4, paddingRight: 12, paddingLeft: padLeft },
13326
13368
  onMouseEnter: () => onHoverValue(o.value),
13327
13369
  onMouseLeave: () => onHoverValue(null),
13328
- onClick: () => onSelect(o.value),
13370
+ onClick: (event) => onSelect(o.value, idx, event.shiftKey),
13329
13371
  children: [
13330
13372
  multi && /* @__PURE__ */ jsxRuntime.jsx(
13331
13373
  CheckboxBox,
@@ -13505,6 +13547,8 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
13505
13547
  setColumnWidths(visibleCols.map(colWidthFromDef));
13506
13548
  }, [colSignature]);
13507
13549
  const selectedKeys = react.useMemo(() => new Set(selected.map((r) => r[rowKey])), [selected, rowKey]);
13550
+ const rangeAnchorRef = react.useRef(null);
13551
+ const shiftPressedRef = react.useRef(false);
13508
13552
  const isRowSelected = react.useCallback(
13509
13553
  (row) => selectedKeys.has(row[rowKey]),
13510
13554
  [selectedKeys, rowKey]
@@ -13686,18 +13730,24 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
13686
13730
  let bodyRows = rows;
13687
13731
  let topSpacer = 0;
13688
13732
  let bottomSpacer = 0;
13733
+ let rowOffset = 0;
13689
13734
  if (virtualScroll) {
13690
13735
  bodyRows = rows.slice(vsRange.start, vsRange.end);
13736
+ rowOffset = vsRange.start;
13691
13737
  topSpacer = vsRange.start * effectiveRowHeight;
13692
13738
  bottomSpacer = Math.max(0, (rowCount - vsRange.end) * effectiveRowHeight);
13693
13739
  } else if (pageInfo) {
13694
13740
  bodyRows = rows.slice(pageInfo.startIndex, pageInfo.endIndex);
13741
+ rowOffset = pageInfo.startIndex;
13695
13742
  }
13696
13743
  const isNoData = rowCount === 0 && !loading2;
13697
13744
  const showNoDataSlot = isNoData && emptySlot != null;
13698
13745
  const showPagination = !virtualScroll && (pagination != null || internalPagination);
13699
13746
  const lastPage = internalPagination ? Math.max(1, Math.ceil(rowCount / (innerRowsPerPage || 1))) : pagination?.lastPage ?? Math.max(1, Math.ceil(rowCount / (pagination?.rowsPerPage ?? 10)));
13700
13747
  const displayPage = internalPagination ? currentPage : pagination?.page ?? 1;
13748
+ react.useEffect(() => {
13749
+ rangeAnchorRef.current = null;
13750
+ }, [displayPage]);
13701
13751
  const changePage = (p) => {
13702
13752
  if (internalPagination) setCurrentPage(p);
13703
13753
  onPageChange?.(p);
@@ -13712,6 +13762,7 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
13712
13762
  };
13713
13763
  const headerCheck = getIsAllChecked(bodyRows);
13714
13764
  const toggleAll = () => {
13765
+ rangeAnchorRef.current = null;
13715
13766
  if (headerCheck === true) {
13716
13767
  const pageKeys = new Set(bodyRows.map((r) => r[rowKey]));
13717
13768
  onSelectedChange?.(selected.filter((r) => !pageKeys.has(r[rowKey])));
@@ -13720,7 +13771,23 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
13720
13771
  onSelectedChange?.([...selected, ...add]);
13721
13772
  }
13722
13773
  };
13723
- const toggleRow = (row) => {
13774
+ const toggleRow = (row, index) => {
13775
+ const anchor = rangeAnchorRef.current;
13776
+ const anchorAlive = anchor != null && anchor.index !== index && rows[anchor.index]?.[rowKey] === anchor.key;
13777
+ if (shiftPressedRef.current && anchor != null && anchorAlive) {
13778
+ const [from, to] = anchor.index < index ? [anchor.index, index] : [index, anchor.index];
13779
+ const shouldSelect = selectedKeys.has(anchor.key);
13780
+ const range = rows.slice(from, to + 1);
13781
+ if (shouldSelect) {
13782
+ const add = range.filter((r) => !selectedKeys.has(r[rowKey]));
13783
+ onSelectedChange?.([...selected, ...add]);
13784
+ } else {
13785
+ const keys = new Set(range.map((r) => r[rowKey]));
13786
+ onSelectedChange?.(selected.filter((r) => !keys.has(r[rowKey])));
13787
+ }
13788
+ return;
13789
+ }
13790
+ rangeAnchorRef.current = { index, key: row[rowKey] };
13724
13791
  if (selectedKeys.has(row[rowKey]))
13725
13792
  onSelectedChange?.(selected.filter((r) => r[rowKey] !== row[rowKey]));
13726
13793
  else onSelectedChange?.([...selected, row]);
@@ -14078,7 +14145,17 @@ var STable = /* @__PURE__ */ react.forwardRef(function STable2({
14078
14145
  maxWidth: SELECTABLE_COLUMN_WIDTH
14079
14146
  },
14080
14147
  onClick: (e) => e.stopPropagation(),
14081
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(SCheckbox, { value: isSel, onValueChange: () => toggleRow(row) }) })
14148
+ onClickCapture: (e) => {
14149
+ shiftPressedRef.current = e.shiftKey;
14150
+ if (e.shiftKey) window.getSelection()?.removeAllRanges();
14151
+ },
14152
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex select-none items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
14153
+ SCheckbox,
14154
+ {
14155
+ value: isSel,
14156
+ onValueChange: () => toggleRow(row, rowOffset + ri)
14157
+ }
14158
+ ) })
14082
14159
  }
14083
14160
  ),
14084
14161
  visibleCols.map((col, i) => {