임동욱 임동욱 08-20
fix: 디자인 테스트 페이지 레이아웃 — 사이드바·본문 스크롤 분리
본문이 길어지면 문서 전체가 스크롤되어 사이드바까지 함께 밀려 올라갔다. shell을 뷰포트 높이로
못박고 overflow를 끊은 뒤, 두 칸이 각자 overflow-y를 갖게 했다(AdminShell과 같은 방식).
좁은 화면에서는 종전처럼 위아래로 쌓이고 문서가 스크롤된다.

사이드바를 디자인 시스템으로 옮겼다 — 손으로 그린 테마 전환 버튼 묶음을 FoxSegmentedControl로,
항목 옆 개수를 FoxBadge로 바꿨다. 대체된 .themeSwitch·.themeButton·.navCount 스타일은 지웠다.

검증 — 1600×900 실측: shell 900·overflow hidden, 문서 전체 스크롤 없음, 사이드바(3237)·
본문(9432) 각각 자체 스크롤.

Co-Authored-By: Claude Opus 5 
@86cdcd1396c243f6843f717fbcd30d6e71535465
@fox/dev-test/dev-test-page.tsx
--- @fox/dev-test/dev-test-page.tsx
+++ @fox/dev-test/dev-test-page.tsx
@@ -1,6 +1,7 @@
 "use client";
 
 import { useSyncExternalStore } from "react";
+import { FoxBadge } from "../core/components/fox-badge";
 import { cx } from "../core/utils";
 import { COMPONENT_EXAMPLES } from "./component-registry";
 import { ComponentEmpty, ComponentView } from "./component-view";
@@ -103,7 +104,9 @@
                         onClick={() => select("token", section.id)}
                       >
                         <span>{section.label}</span>
-                        <span className={styles.navCount}>{section.tokens.length}</span>
+                        <FoxBadge type="pastel" color="neutral" size="sm">
+                          {String(section.tokens.length)}
+                        </FoxBadge>
                       </button>
                     </li>
                   ))}
@@ -128,7 +131,9 @@
                     onClick={() => select("component", example.id)}
                   >
                     <span>{example.name}</span>
-                    <span className={styles.navCount}>{example.variants.length}</span>
+                    <FoxBadge type="pastel" color="neutral" size="sm">
+                      {String(example.variants.length)}
+                    </FoxBadge>
                   </button>
                 </li>
               ))}
@fox/dev-test/dev-test.module.scss
--- @fox/dev-test/dev-test.module.scss
+++ @fox/dev-test/dev-test.module.scss
@@ -9,12 +9,15 @@
 .shell {
   display: grid;
   grid-template-columns: 1fr;
-  min-block-size: 100vh;
+  min-block-size: 100dvh;
   background: fox.color(background-default);
   color: fox.color(font-neutral-default);
 
   @include fox.pc {
+    // 화면을 넘지 않게 못박고 스크롤은 두 칸이 각자 갖는다.
+    block-size: 100dvh;
     grid-template-columns: 26rem 1fr;
+    overflow: hidden;
   }
 }
 
@@ -34,9 +37,7 @@
   background: fox.color(surface-neutral-gray);
 
   @include fox.pc {
-    position: sticky;
-    inset-block-start: 0;
-    block-size: 100vh;
+    min-block-size: 0;
     overflow-y: auto;
     border-block-end: 0;
     border-inline-end: fox.border(1) solid fox.color(border-neutral-subtle);
@@ -57,34 +58,6 @@
 .brandNote {
   color: fox.color(font-neutral-subtler);
   font-size: fox.font-size(label-xsm);
-}
-
-.themeSwitch {
-  display: flex;
-  gap: fox.gap(1);
-  padding: fox.padding(1);
-  border: fox.border(1) solid fox.color(border-neutral-default);
-  border-radius: fox.radius(max);
-  background: fox.color(surface-neutral-default);
-}
-
-.themeButton {
-  flex: 1;
-  padding: fox.padding(3) fox.padding(4);
-  border-radius: fox.radius(max);
-  color: fox.color(font-neutral-subtle);
-  font-size: fox.font-size(label-xsm);
-  font-weight: fox.font-weight(medium);
-
-  &[aria-pressed="true"] {
-    background: fox.color(button-primary-surface);
-    color: fox.color(button-primary-font);
-  }
-
-  &:focus-visible {
-    outline: fox.border(2) solid fox.color(border-theme-primary);
-    outline-offset: fox.border(2);
-  }
 }
 
 .nav {
@@ -160,12 +133,6 @@
   }
 }
 
-.navCount {
-  color: currentcolor;
-  font-size: fox.font-size(label-xsm);
-  opacity: 0.7;
-}
-
 // "추가 방법"은 컴포넌트가 아니라 안내다. 목록 맨 아래에 두고 점선 테두리로 구분해,
 // 등록된 컴포넌트 탭과 같은 층위로 읽히지 않게 한다.
 // `.navItem` 뒤에 와야 선택 상태(aria-current)를 덮어쓴다 — 특이도가 같아 순서가 정한다.
@@ -200,6 +167,11 @@
   padding: fox.spacing(top-md) fox.padding(7) fox.spacing(bottom-xlg);
 
   @include fox.pc {
+    min-block-size: 0;
+    overflow-y: auto;
+  }
+
+  @include fox.pc {
     padding: fox.spacing(top-lg) fox.padding(11) fox.spacing(bottom-xlg);
   }
 }
@fox/dev-test/theme-switch.tsx
--- @fox/dev-test/theme-switch.tsx
+++ @fox/dev-test/theme-switch.tsx
@@ -1,9 +1,9 @@
 "use client";
 
 import { useSyncExternalStore } from "react";
-import styles from "./dev-test.module.scss";
+import { FoxSegmentedControl } from "../core/components/fox-segmented-control";
+import { FoxSegmentedControlItem } from "../core/components/fox-segmented-control-item";
 
-/** `@fox/styles/_root.scss`가 읽는 수동 테마 선택 속성. */
 const THEME_ATTRIBUTE = "data-theme";
 
 type ThemeChoice = "system" | "light" | "dark";
@@ -14,10 +14,6 @@
   { value: "dark", label: "다크" },
 ];
 
-/**
- * `<html data-theme>`가 SSOT라 React state를 두지 않는다. 저장 키는 호스트 앱이
- * 소유하는 값이라 `@fox`가 알면 포터빌리티가 깨지므로 선택을 저장하지 않는다.
- */
 function subscribe(onChange: () => void): () => void {
   const observer = new MutationObserver(onChange);
   observer.observe(document.documentElement, {
@@ -49,18 +45,16 @@
   const current = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
 
   return (
-    <div className={styles.themeSwitch} role="group" aria-label="테마 전환">
+    <FoxSegmentedControl size="sm" name="dev-test-theme" label="테마 전환">
       {CHOICES.map(({ value, label }) => (
-        <button
+        <FoxSegmentedControlItem
           key={value}
-          type="button"
-          className={styles.themeButton}
-          aria-pressed={current === value}
-          onClick={() => apply(value)}
-        >
-          {label}
-        </button>
+          value={value}
+          label={label}
+          checked={current === value}
+          onChange={(next) => apply(next as ThemeChoice)}
+        />
       ))}
-    </div>
+    </FoxSegmentedControl>
   );
 }
Add a comment
List