sellmate-design-system-react 0.5.0 → 0.6.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.
Files changed (30) hide show
  1. package/README.md +20 -0
  2. package/dist/components/SDateRangePicker/SDateRangePicker.cjs +180 -37
  3. package/dist/components/SDateRangePicker/SDateRangePicker.cjs.map +1 -1
  4. package/dist/components/SDateRangePicker/SDateRangePicker.d.ts +6 -1
  5. package/dist/components/SDateRangePicker/SDateRangePicker.js +180 -37
  6. package/dist/components/SDateRangePicker/SDateRangePicker.js.map +1 -1
  7. package/dist/components/SGnb/SGnb.cjs +124 -54
  8. package/dist/components/SGnb/SGnb.cjs.map +1 -1
  9. package/dist/components/SGnb/SGnb.d.ts +7 -1
  10. package/dist/components/SGnb/SGnb.js +125 -55
  11. package/dist/components/SGnb/SGnb.js.map +1 -1
  12. package/dist/components/SKeyValueTable/SKeyValueTable.cjs +180 -37
  13. package/dist/components/SKeyValueTable/SKeyValueTable.cjs.map +1 -1
  14. package/dist/components/SKeyValueTable/SKeyValueTable.js +180 -37
  15. package/dist/components/SKeyValueTable/SKeyValueTable.js.map +1 -1
  16. package/dist/components/SLayout/SLayout.cjs +21 -6
  17. package/dist/components/SLayout/SLayout.cjs.map +1 -1
  18. package/dist/components/SLayout/SLayout.d.ts +17 -2
  19. package/dist/components/SLayout/SLayout.js +21 -6
  20. package/dist/components/SLayout/SLayout.js.map +1 -1
  21. package/dist/components/SPage/SPage.cjs +21 -6
  22. package/dist/components/SPage/SPage.cjs.map +1 -1
  23. package/dist/components/SPage/SPage.js +21 -6
  24. package/dist/components/SPage/SPage.js.map +1 -1
  25. package/dist/index.cjs +303 -90
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.js +303 -90
  28. package/dist/index.js.map +1 -1
  29. package/dist/styles.css +61 -0
  30. package/package.json +3 -1
@@ -3,7 +3,10 @@ import { type SGnbType, type SGnbHeader, type SGnbColor, type SGnbMenuItem } fro
3
3
  export interface SGnbProps extends Omit<HTMLAttributes<HTMLDivElement>, 'color'> {
4
4
  /** 메뉴 스타일: box(라운드) / belt(풀폭 행). 미지정 시 SLayout 의 type 을 따른다. */
5
5
  type?: SGnbType;
6
- /** 상단바 구조: fix(GNB 폭 고정) / full(레이아웃 전폭). 미지정 시 SLayout 의 header 를 따른다. */
6
+ /**
7
+ * 상단바 구조: fix(GNB 폭 고정) / full(레이아웃 전폭). 미지정 시 SLayout 의 header 를 따른다.
8
+ * 단, useRail 이고 레일에 children 없는 아이템이 있으면 메뉴 폭이 출렁이므로 full 로 강제된다.
9
+ */
7
10
  header?: SGnbHeader;
8
11
  /** 색상 테마: light / dark */
9
12
  color?: SGnbColor;
@@ -12,6 +15,7 @@ export interface SGnbProps extends Omit<HTMLAttributes<HTMLDivElement>, 'color'>
12
15
  /**
13
16
  * 좌측 레일 사용 여부. 켜면 items 의 첫 depth 가 아이콘+라벨 버튼의 세로 레일로 서고,
14
17
  * 선택된 레일 아이템의 children 이 오른쪽 메뉴 패널에 깔린다. 미지정 시 SLayout 의 useRail 을 따른다.
18
+ * children 없는 레일 아이템이 활성이면 메뉴 패널은 렌더되지 않고 레일만 남는다.
15
19
  */
16
20
  useRail?: boolean;
17
21
  /** 현재 선택된 아이템 value */
@@ -42,6 +46,8 @@ export interface SGnbProps extends Omit<HTMLAttributes<HTMLDivElement>, 'color'>
42
46
  *
43
47
  * useRail 이면 items 의 첫 depth 가 좌측 레일(아이콘+라벨 68px)로 서고, 선택된 레일 아이템의
44
48
  * children 이 메뉴 패널의 depth1 이 된다 — 트리는 그대로 두고 어디부터 그릴지만 바뀐다.
49
+ * children 없는 레일 아이템은 자기 자신이 선택(리프)이라 메뉴 패널을 렌더하지 않는다. 이런 리프가
50
+ * 하나라도 있으면 활성에 따라 메뉴 폭이 출렁이므로 header 는 full 로 강제해 상단바를 전폭에 고정한다.
45
51
  *
46
52
  * 접힘/펼침은 GNB_FOLD_MS 동안의 슬라이드다 — 레일·메뉴가 함께 왼쪽으로 빠져나가고 펼치면 오른쪽에서 들어온다.
47
53
  * 페이지 쪽 폭은 fix 면 이 컴포넌트의 폭 전환이, full 이면 SLayout 의 메뉴 열 전환이 같은 길이로 맞춘다.
@@ -3,7 +3,7 @@ import { createContext, forwardRef, useState, useCallback, useMemo, useContext,
3
3
  import { clsx } from 'clsx';
4
4
  import { extendTailwindMerge } from 'tailwind-merge';
5
5
  import * as RPopover from '@radix-ui/react-popover';
6
- import { jsx, jsxs } from 'react/jsx-runtime';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
 
8
8
  var twMerge = extendTailwindMerge({
9
9
  extend: {
@@ -3233,15 +3233,30 @@ forwardRef(function SLayout2({
3233
3233
  },
3234
3234
  [isControlled, onFoldedChange]
3235
3235
  );
3236
- const ctx = useMemo(
3237
- () => ({ type, header, useRail, folded: currentFolded, setFolded }),
3238
- [type, header, useRail, currentFolded, setFolded]
3236
+ const [gnbState, setGnbState] = useState({ forceFull: false, menuVisible: true });
3237
+ const reportGnbState = useCallback(
3238
+ (next) => setGnbState(
3239
+ (prev) => prev.forceFull === next.forceFull && prev.menuVisible === next.menuVisible ? prev : next
3240
+ ),
3241
+ []
3239
3242
  );
3240
- const isFullHeader = header === "full";
3241
- const gnbColumnWidth = useRail ? GNB_RAIL_WIDTH + GNB_MENU_WIDTH : GNB_MENU_WIDTH;
3243
+ const effectiveHeader = header === "full" || gnbState.forceFull ? "full" : "fix";
3244
+ const ctx = useMemo(
3245
+ () => ({
3246
+ type,
3247
+ header: effectiveHeader,
3248
+ useRail,
3249
+ folded: currentFolded,
3250
+ setFolded,
3251
+ reportGnbState
3252
+ }),
3253
+ [type, effectiveHeader, useRail, currentFolded, setFolded, reportGnbState]
3254
+ );
3255
+ const isFullHeader = effectiveHeader === "full";
3256
+ const gnbColumnWidth = (useRail ? GNB_RAIL_WIDTH : 0) + (gnbState.menuVisible ? GNB_MENU_WIDTH : 0);
3242
3257
  const frameStyle = isFullHeader ? {
3243
3258
  gridTemplateColumns: `${currentFolded ? 0 : gnbColumnWidth}px 1fr`,
3244
- gridTemplateRows: `${GNB_HEADER[header].topHeight} 1fr`,
3259
+ gridTemplateRows: `${GNB_HEADER[effectiveHeader].topHeight} 1fr`,
3245
3260
  // 메뉴가 미끄러지는 동안 페이지 열도 같은 길이로 따라와야 한 덩어리로 읽힌다.
3246
3261
  // (fix 는 SGnb 가 제 폭을 직접 전환하므로 프레임이 손댈 게 없다)
3247
3262
  transition: `grid-template-columns ${GNB_FOLD_MS}ms ease-out`
@@ -3251,7 +3266,7 @@ forwardRef(function SLayout2({
3251
3266
  {
3252
3267
  ref,
3253
3268
  "data-type": type,
3254
- "data-header": header,
3269
+ "data-header": effectiveHeader,
3255
3270
  className: cn(
3256
3271
  "box-border h-full overflow-hidden",
3257
3272
  isFullHeader ? "grid" : "flex flex-row",
@@ -3284,9 +3299,10 @@ var SGnb = forwardRef(function SGnb2({
3284
3299
  }, ref) {
3285
3300
  const layout = useContext(SLayoutContext);
3286
3301
  const type = typeProp ?? layout?.type ?? "box";
3287
- const header = headerProp ?? layout?.header ?? "fix";
3288
3302
  const folded = foldedProp ?? layout?.folded ?? false;
3289
3303
  const useRail = useRailProp ?? layout?.useRail ?? false;
3304
+ const railHasLeaf = useRail && items.some((item) => !item.children || item.children.length === 0);
3305
+ const header = railHasLeaf ? "full" : headerProp ?? layout?.header ?? "fix";
3290
3306
  const H = GNB_HEADER[header];
3291
3307
  const M = GNB_MENU[type];
3292
3308
  const C = GNB_COLORS[color];
@@ -3474,7 +3490,11 @@ var SGnb = forwardRef(function SGnb2({
3474
3490
  };
3475
3491
  const railItems = useRail ? items : [];
3476
3492
  const menuItems = useRail ? items.find((item) => item.value === activeRail)?.children ?? [] : items;
3477
- const bodyWidth = useRail ? GNB_RAIL_WIDTH + GNB_MENU_WIDTH : GNB_MENU_WIDTH;
3493
+ const showMenu = !useRail || menuItems.length > 0;
3494
+ const bodyWidth = (useRail ? GNB_RAIL_WIDTH : 0) + (showMenu ? GNB_MENU_WIDTH : 0);
3495
+ useEffect(() => {
3496
+ layout?.reportGnbState({ forceFull: railHasLeaf, menuVisible: showMenu });
3497
+ }, [layout, railHasLeaf, showMenu]);
3478
3498
  const railFolded = folded && header === "fix";
3479
3499
  const rootWidth = header === "full" ? "100%" : folded ? GNB_COMMON.foldWidth : bodyWidth;
3480
3500
  const rootStyle = {
@@ -3484,6 +3504,13 @@ var SGnb = forwardRef(function SGnb2({
3484
3504
  ...style
3485
3505
  };
3486
3506
  const hideAfterSlide = (hidden) => `visibility 0s linear ${hidden ? `${GNB_FOLD_MS}ms` : "0s"}`;
3507
+ const foldInline = !!logo;
3508
+ const sideFoldStyle = {
3509
+ opacity: railFolded ? 0 : 1,
3510
+ position: railFolded ? "absolute" : "static",
3511
+ visibility: railFolded ? "hidden" : "visible",
3512
+ transition: `opacity ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(railFolded)}`
3513
+ };
3487
3514
  return /* @__PURE__ */ jsxs(
3488
3515
  "div",
3489
3516
  {
@@ -3495,12 +3522,14 @@ var SGnb = forwardRef(function SGnb2({
3495
3522
  "data-color": color,
3496
3523
  ...rest,
3497
3524
  children: [
3498
- /* @__PURE__ */ jsxs(
3525
+ /* @__PURE__ */ jsx(
3499
3526
  "div",
3500
3527
  {
3501
3528
  className: "relative box-border flex shrink-0 items-center overflow-hidden",
3502
3529
  style: {
3503
3530
  height: H.topHeight,
3531
+ // 인라인 간격은 컨테이너 gap 대신 각 요소 margin 으로 준다 — 런처↔폴드(8px)와
3532
+ // 폴드↔로고(top-gap)를 다르게 두기 위함. 접히면 런처·로고가 흐름에서 빠져 margin 도 무의미해진다.
3504
3533
  padding: `0 ${railFolded ? GNB_RAIL.padding : H.topPaddingX}`,
3505
3534
  borderBottom: `1px solid ${C.topBorder}`,
3506
3535
  backgroundColor: C.panelBg,
@@ -3509,49 +3538,90 @@ var SGnb = forwardRef(function SGnb2({
3509
3538
  // 아이콘 버튼은 자체 ghostButton 토큰을 쓰므로, 이 색은 logo 슬롯 상속용
3510
3539
  color: C.itemColorDefault
3511
3540
  },
3512
- children: [
3513
- (onLauncherClick || logo) && /* @__PURE__ */ jsxs(
3514
- "div",
3515
- {
3516
- className: "absolute inset-y-0 flex items-center",
3517
- style: {
3518
- left: H.topPaddingX,
3519
- gap: H.topGap,
3520
- opacity: railFolded ? 0 : 1,
3521
- transition: `opacity ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(railFolded)}, transform ${GNB_FOLD_MS}ms ease-out`,
3522
- // 메뉴와 같은 방향으로 빠지되, 좁은 레일 안에서 꼬리가 비치지 않게 페이드가 거리를 대신한다
3523
- transform: railFolded ? `translateX(-${H.topPaddingX})` : "none",
3524
- visibility: railFolded ? "hidden" : "visible"
3525
- },
3526
- children: [
3527
- onLauncherClick && /* @__PURE__ */ jsx(
3528
- SGhostButton,
3529
- {
3530
- icon: "launcher",
3531
- size: "xs",
3532
- intent: color === "dark" ? "inverse" : "default",
3533
- ariaLabel: "app launcher",
3534
- onClick: onLauncherClick,
3535
- className: "shrink-0"
3536
- }
3537
- ),
3538
- logo && /* @__PURE__ */ jsx("div", { className: "flex items-center", style: { height: H.logoHeight }, children: logo })
3539
- ]
3540
- }
3541
- ),
3542
- /* @__PURE__ */ jsx(
3543
- SGhostButton,
3544
- {
3545
- icon: "sidebar",
3546
- size: "xs",
3547
- intent: color === "dark" ? "inverse" : "default",
3548
- ariaLabel: "toggle navigation",
3549
- ariaPressed: folded,
3550
- onClick: handleFoldToggle,
3551
- className: "ml-auto shrink-0"
3552
- }
3553
- )
3554
- ]
3541
+ children: foldInline ? (
3542
+ // 런처(있을 )·폴드·로고를 흐름에 나란히 둔다 폴드 버튼이 (런처와) 로고 사이에 선다.
3543
+ // 접히면 런처·로고가 페이드하며 흐름에서 빠지고(sideFoldStyle), 폴드 버튼만 남아 레일 가운데에 선다.
3544
+ /* @__PURE__ */ jsxs(Fragment, { children: [
3545
+ onLauncherClick && /* @__PURE__ */ jsx(
3546
+ SGhostButton,
3547
+ {
3548
+ icon: "launcher",
3549
+ size: "xs",
3550
+ intent: color === "dark" ? "inverse" : "default",
3551
+ ariaLabel: "app launcher",
3552
+ onClick: onLauncherClick,
3553
+ className: "shrink-0",
3554
+ style: { ...sideFoldStyle, marginRight: "8px" }
3555
+ }
3556
+ ),
3557
+ /* @__PURE__ */ jsx(
3558
+ SGhostButton,
3559
+ {
3560
+ icon: "sidebar",
3561
+ size: "xs",
3562
+ intent: color === "dark" ? "inverse" : "default",
3563
+ ariaLabel: "toggle navigation",
3564
+ ariaPressed: folded,
3565
+ onClick: handleFoldToggle,
3566
+ className: "shrink-0"
3567
+ }
3568
+ ),
3569
+ /* @__PURE__ */ jsx(
3570
+ "div",
3571
+ {
3572
+ className: "flex shrink-0 items-center",
3573
+ style: { height: H.logoHeight, marginLeft: H.topGap, ...sideFoldStyle },
3574
+ children: logo
3575
+ }
3576
+ )
3577
+ ] })
3578
+ ) : (
3579
+ // 런처·로고는 좌측 그룹(흐름에서 빼 폭 축소 중 폴드 버튼을 밀어내지 않는다),
3580
+ // 폴드 버튼은 우측 끝(ml-auto)이라 폭·패딩이 줄면 저절로 레일 가운데에 선다.
3581
+ /* @__PURE__ */ jsxs(Fragment, { children: [
3582
+ (onLauncherClick || logo) && /* @__PURE__ */ jsxs(
3583
+ "div",
3584
+ {
3585
+ className: "absolute inset-y-0 flex items-center",
3586
+ style: {
3587
+ left: H.topPaddingX,
3588
+ gap: H.topGap,
3589
+ opacity: railFolded ? 0 : 1,
3590
+ transition: `opacity ${GNB_FOLD_MS}ms ease-out, ${hideAfterSlide(railFolded)}, transform ${GNB_FOLD_MS}ms ease-out`,
3591
+ // 메뉴와 같은 방향으로 빠지되, 좁은 레일 안에서 꼬리가 비치지 않게 페이드가 거리를 대신한다
3592
+ transform: railFolded ? `translateX(-${H.topPaddingX})` : "none",
3593
+ visibility: railFolded ? "hidden" : "visible"
3594
+ },
3595
+ children: [
3596
+ onLauncherClick && /* @__PURE__ */ jsx(
3597
+ SGhostButton,
3598
+ {
3599
+ icon: "launcher",
3600
+ size: "xs",
3601
+ intent: color === "dark" ? "inverse" : "default",
3602
+ ariaLabel: "app launcher",
3603
+ onClick: onLauncherClick,
3604
+ className: "shrink-0"
3605
+ }
3606
+ ),
3607
+ logo && /* @__PURE__ */ jsx("div", { className: "flex items-center", style: { height: H.logoHeight }, children: logo })
3608
+ ]
3609
+ }
3610
+ ),
3611
+ /* @__PURE__ */ jsx(
3612
+ SGhostButton,
3613
+ {
3614
+ icon: "sidebar",
3615
+ size: "xs",
3616
+ intent: color === "dark" ? "inverse" : "default",
3617
+ ariaLabel: "toggle navigation",
3618
+ ariaPressed: folded,
3619
+ onClick: handleFoldToggle,
3620
+ className: "ml-auto shrink-0"
3621
+ }
3622
+ )
3623
+ ] })
3624
+ )
3555
3625
  }
3556
3626
  ),
3557
3627
  /* @__PURE__ */ jsxs(
@@ -3587,7 +3657,7 @@ var SGnb = forwardRef(function SGnb2({
3587
3657
  children: /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col p-0", style: { gap: GNB_RAIL.gap }, children: railItems.map(renderRailItem) })
3588
3658
  }
3589
3659
  ),
3590
- /* @__PURE__ */ jsx(
3660
+ showMenu && /* @__PURE__ */ jsx(
3591
3661
  "nav",
3592
3662
  {
3593
3663
  "aria-label": ariaLabel,