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
... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 |
"use client"; |
| 2 | 2 |
|
| 3 | 3 |
import { useSyncExternalStore } from "react";
|
| 4 |
+import { FoxBadge } from "../core/components/fox-badge";
|
|
| 4 | 5 |
import { cx } from "../core/utils";
|
| 5 | 6 |
import { COMPONENT_EXAMPLES } from "./component-registry";
|
| 6 | 7 |
import { ComponentEmpty, ComponentView } from "./component-view";
|
... | ... | @@ -103,7 +104,9 @@ |
| 103 | 104 |
onClick={() => select("token", section.id)}
|
| 104 | 105 |
> |
| 105 | 106 |
<span>{section.label}</span>
|
| 106 |
- <span className={styles.navCount}>{section.tokens.length}</span>
|
|
| 107 |
+ <FoxBadge type="pastel" color="neutral" size="sm"> |
|
| 108 |
+ {String(section.tokens.length)}
|
|
| 109 |
+ </FoxBadge> |
|
| 107 | 110 |
</button> |
| 108 | 111 |
</li> |
| 109 | 112 |
))} |
... | ... | @@ -128,7 +131,9 @@ |
| 128 | 131 |
onClick={() => select("component", example.id)}
|
| 129 | 132 |
> |
| 130 | 133 |
<span>{example.name}</span>
|
| 131 |
- <span className={styles.navCount}>{example.variants.length}</span>
|
|
| 134 |
+ <FoxBadge type="pastel" color="neutral" size="sm"> |
|
| 135 |
+ {String(example.variants.length)}
|
|
| 136 |
+ </FoxBadge> |
|
| 132 | 137 |
</button> |
| 133 | 138 |
</li> |
| 134 | 139 |
))} |
--- @fox/dev-test/dev-test.module.scss
+++ @fox/dev-test/dev-test.module.scss
... | ... | @@ -9,12 +9,15 @@ |
| 9 | 9 |
.shell {
|
| 10 | 10 |
display: grid; |
| 11 | 11 |
grid-template-columns: 1fr; |
| 12 |
- min-block-size: 100vh; |
|
| 12 |
+ min-block-size: 100dvh; |
|
| 13 | 13 |
background: fox.color(background-default); |
| 14 | 14 |
color: fox.color(font-neutral-default); |
| 15 | 15 |
|
| 16 | 16 |
@include fox.pc {
|
| 17 |
+ // 화면을 넘지 않게 못박고 스크롤은 두 칸이 각자 갖는다. |
|
| 18 |
+ block-size: 100dvh; |
|
| 17 | 19 |
grid-template-columns: 26rem 1fr; |
| 20 |
+ overflow: hidden; |
|
| 18 | 21 |
} |
| 19 | 22 |
} |
| 20 | 23 |
|
... | ... | @@ -34,9 +37,7 @@ |
| 34 | 37 |
background: fox.color(surface-neutral-gray); |
| 35 | 38 |
|
| 36 | 39 |
@include fox.pc {
|
| 37 |
- position: sticky; |
|
| 38 |
- inset-block-start: 0; |
|
| 39 |
- block-size: 100vh; |
|
| 40 |
+ min-block-size: 0; |
|
| 40 | 41 |
overflow-y: auto; |
| 41 | 42 |
border-block-end: 0; |
| 42 | 43 |
border-inline-end: fox.border(1) solid fox.color(border-neutral-subtle); |
... | ... | @@ -57,34 +58,6 @@ |
| 57 | 58 |
.brandNote {
|
| 58 | 59 |
color: fox.color(font-neutral-subtler); |
| 59 | 60 |
font-size: fox.font-size(label-xsm); |
| 60 |
-} |
|
| 61 |
- |
|
| 62 |
-.themeSwitch {
|
|
| 63 |
- display: flex; |
|
| 64 |
- gap: fox.gap(1); |
|
| 65 |
- padding: fox.padding(1); |
|
| 66 |
- border: fox.border(1) solid fox.color(border-neutral-default); |
|
| 67 |
- border-radius: fox.radius(max); |
|
| 68 |
- background: fox.color(surface-neutral-default); |
|
| 69 |
-} |
|
| 70 |
- |
|
| 71 |
-.themeButton {
|
|
| 72 |
- flex: 1; |
|
| 73 |
- padding: fox.padding(3) fox.padding(4); |
|
| 74 |
- border-radius: fox.radius(max); |
|
| 75 |
- color: fox.color(font-neutral-subtle); |
|
| 76 |
- font-size: fox.font-size(label-xsm); |
|
| 77 |
- font-weight: fox.font-weight(medium); |
|
| 78 |
- |
|
| 79 |
- &[aria-pressed="true"] {
|
|
| 80 |
- background: fox.color(button-primary-surface); |
|
| 81 |
- color: fox.color(button-primary-font); |
|
| 82 |
- } |
|
| 83 |
- |
|
| 84 |
- &:focus-visible {
|
|
| 85 |
- outline: fox.border(2) solid fox.color(border-theme-primary); |
|
| 86 |
- outline-offset: fox.border(2); |
|
| 87 |
- } |
|
| 88 | 61 |
} |
| 89 | 62 |
|
| 90 | 63 |
.nav {
|
... | ... | @@ -160,12 +133,6 @@ |
| 160 | 133 |
} |
| 161 | 134 |
} |
| 162 | 135 |
|
| 163 |
-.navCount {
|
|
| 164 |
- color: currentcolor; |
|
| 165 |
- font-size: fox.font-size(label-xsm); |
|
| 166 |
- opacity: 0.7; |
|
| 167 |
-} |
|
| 168 |
- |
|
| 169 | 136 |
// "추가 방법"은 컴포넌트가 아니라 안내다. 목록 맨 아래에 두고 점선 테두리로 구분해, |
| 170 | 137 |
// 등록된 컴포넌트 탭과 같은 층위로 읽히지 않게 한다. |
| 171 | 138 |
// `.navItem` 뒤에 와야 선택 상태(aria-current)를 덮어쓴다 — 특이도가 같아 순서가 정한다. |
... | ... | @@ -200,6 +167,11 @@ |
| 200 | 167 |
padding: fox.spacing(top-md) fox.padding(7) fox.spacing(bottom-xlg); |
| 201 | 168 |
|
| 202 | 169 |
@include fox.pc {
|
| 170 |
+ min-block-size: 0; |
|
| 171 |
+ overflow-y: auto; |
|
| 172 |
+ } |
|
| 173 |
+ |
|
| 174 |
+ @include fox.pc {
|
|
| 203 | 175 |
padding: fox.spacing(top-lg) fox.padding(11) fox.spacing(bottom-xlg); |
| 204 | 176 |
} |
| 205 | 177 |
} |
--- @fox/dev-test/theme-switch.tsx
+++ @fox/dev-test/theme-switch.tsx
... | ... | @@ -1,9 +1,9 @@ |
| 1 | 1 |
"use client"; |
| 2 | 2 |
|
| 3 | 3 |
import { useSyncExternalStore } from "react";
|
| 4 |
-import styles from "./dev-test.module.scss"; |
|
| 4 |
+import { FoxSegmentedControl } from "../core/components/fox-segmented-control";
|
|
| 5 |
+import { FoxSegmentedControlItem } from "../core/components/fox-segmented-control-item";
|
|
| 5 | 6 |
|
| 6 |
-/** `@fox/styles/_root.scss`가 읽는 수동 테마 선택 속성. */ |
|
| 7 | 7 |
const THEME_ATTRIBUTE = "data-theme"; |
| 8 | 8 |
|
| 9 | 9 |
type ThemeChoice = "system" | "light" | "dark"; |
... | ... | @@ -14,10 +14,6 @@ |
| 14 | 14 |
{ value: "dark", label: "다크" },
|
| 15 | 15 |
]; |
| 16 | 16 |
|
| 17 |
-/** |
|
| 18 |
- * `<html data-theme>`가 SSOT라 React state를 두지 않는다. 저장 키는 호스트 앱이 |
|
| 19 |
- * 소유하는 값이라 `@fox`가 알면 포터빌리티가 깨지므로 선택을 저장하지 않는다. |
|
| 20 |
- */ |
|
| 21 | 17 |
function subscribe(onChange: () => void): () => void {
|
| 22 | 18 |
const observer = new MutationObserver(onChange); |
| 23 | 19 |
observer.observe(document.documentElement, {
|
... | ... | @@ -49,18 +45,16 @@ |
| 49 | 45 |
const current = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); |
| 50 | 46 |
|
| 51 | 47 |
return ( |
| 52 |
- <div className={styles.themeSwitch} role="group" aria-label="테마 전환">
|
|
| 48 |
+ <FoxSegmentedControl size="sm" name="dev-test-theme" label="테마 전환"> |
|
| 53 | 49 |
{CHOICES.map(({ value, label }) => (
|
| 54 |
- <button |
|
| 50 |
+ <FoxSegmentedControlItem |
|
| 55 | 51 |
key={value}
|
| 56 |
- type="button" |
|
| 57 |
- className={styles.themeButton}
|
|
| 58 |
- aria-pressed={current === value}
|
|
| 59 |
- onClick={() => apply(value)}
|
|
| 60 |
- > |
|
| 61 |
- {label}
|
|
| 62 |
- </button> |
|
| 52 |
+ value={value}
|
|
| 53 |
+ label={label}
|
|
| 54 |
+ checked={current === value}
|
|
| 55 |
+ onChange={(next) => apply(next as ThemeChoice)}
|
|
| 56 |
+ /> |
|
| 63 | 57 |
))} |
| 64 |
- </div> |
|
| 58 |
+ </FoxSegmentedControl> |
|
| 65 | 59 |
); |
| 66 | 60 |
} |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?