@designbasekorea/ui-wc 0.15.0 → 0.17.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.
@@ -0,0 +1,206 @@
1
+ # 페이지 유형과 작업에 따른 컴포넌트 사용법
2
+
3
+ 이 문서는 컴포넌트 선택과 조합의 기준이다. API 예제는 이 저장소의 ui-wc 0.16.0 소스와 React wrapper 기준이며, 다른 설치 버전에서는 해당 버전의 exports·타입을 먼저 확인한다. 아래 React 예제는 `@designbasekorea/ui-wc/react`의 실제 wrapper를 사용한다. Vanilla에서는 `db-*` 요소에 property를 설정하고 대응 `db-*` 이벤트를 구독한다.
4
+
5
+ ## 1. 페이지 유형부터 선택
6
+
7
+ | 화면 | 기본 구성 | 선택 기준 |
8
+ |---|---|---|
9
+ | 서비스 소개·마케팅 | Navbar → 소개/기능/근거/CTA → Footer | 섹션/페이지 탐색과 가입·문의가 중심. 앱용 사이드바를 기본으로 넣지 않는다. |
10
+ | 문서·가이드 | Navbar + Sidebar `type="navigation"` + 본문 | 문서 그룹과 현재 문서 위치가 중심. 하위 문서의 펼침과 현재 페이지 선택을 구분한다. |
11
+ | 카탈로그·리소스 탐색 | Sidebar `type="category"` + PageHeader + FilterBar + 결과 | 분류 탐색과 검색/필터가 중심. 선택한 분류와 나머지 필터가 같은 결과 집합을 제어한다. |
12
+ | SaaS·관리자 | Sidebar `type="app"` + PageHeader + 업무 화면 | 프로젝트·계정·업무 메뉴와 라우팅이 중심. 필요한 경우 프로젝트 전환·하단 설정을 추가한다. |
13
+ | 편집기·작업 공간 | 앱 탐색 + 작업 도구 + 캔버스 + 속성 패널 | 페이지 탐색과 편집 도구를 구분. 필요에 따라 NavigationRail, ToolPanel, ContextBar 등을 확인한다. |
14
+
15
+ 같은 제품의 소개 페이지와 로그인 후 앱은 서로 다른 셸을 사용할 수 있다. 모든 페이지에 사이드바·카드·푸터를 동일하게 붙이지 않는다. 헤더를 이중으로 만들거나 여러 sticky 영역의 높이를 중복 계산하지 않는다. 사이트 셸의 상세 계약은 `SITE-SHELL.md`를 참고한다.
16
+
17
+ 툴·편집기는 [ToolLayout·ToolPanel 상세 사용법](TOOL-UI.md)을 함께 적용한다. 영역 슬롯, 패널 외형·탭·접기·닫기, 리사이즈·반응형, DesignToolbar/ContextBar와 상태 연결을 다룬다.
18
+
19
+ ## 2. Sidebar: 역할과 외형을 분리
20
+
21
+ | 옵션 | 의미 | 예 |
22
+ |---|---|---|
23
+ | `type="app"` | 앱 전체 탐색 | 대시보드, 프로젝트, 멤버, 설정 |
24
+ | `type="navigation"` | 페이지/문서 그룹 탐색 | 시작하기, 컴포넌트 가이드 |
25
+ | `type="category"` | 리소스 분류 | 템플릿 종류, 에셋 분류 |
26
+ | `variant="default"` | 기본 외형, 우측 경계 | 일반 앱 셸 |
27
+ | `variant="wrapper"` | 패딩·radius·border가 있는 외형 | 분리된 탐색 패널 |
28
+
29
+ `type`, `variant`, `theme`, `size`는 다른 축이다. `variant="app"`처럼 역할을 외형 속성에 넣지 않는다. `type`을 바꾸는 것만으로 라우팅·권한·데이터 필터가 구현되지는 않는다.
30
+
31
+ ```tsx
32
+ import { Sidebar } from '@designbasekorea/ui-wc/react';
33
+
34
+ export function AppNavigation({
35
+ pathname, navigate,
36
+ }: { pathname: string; navigate: (href: string) => void }) {
37
+ return <Sidebar type="app" variant="default" sections={[
38
+ { id: 'work', title: '워크스페이스', items: [
39
+ { id: 'projects', label: '프로젝트', href: '/projects', active: pathname === '/projects' },
40
+ { id: 'members', label: '멤버', href: '/members', active: pathname === '/members' },
41
+ ] },
42
+ ]} onItemClick={event => {
43
+ const href = event.detail.item.href;
44
+ if (href) navigate(href);
45
+ }} />;
46
+ }
47
+ ```
48
+
49
+ - `sections`는 `{ id, title?, items }`, 항목은 `{ id, label, href?, icon?, active?, children? }`를 사용한다. 아이콘은 공식 등록 이름을 확인한 후 추가한다.
50
+ - 현재 경로에서 `active`를 계산한다. 깊은 경로는 앱의 라우트 규칙에 맞춰 매칭한다. 그룹 펼침은 `expandedIds`/`onExpandedChange`, 접힘은 `collapsed`/`onToggle`로 별도 관리한다.
51
+ - 앱에서 실제로 필요할 때 `projects`, `selectedProjectId`, `onProjectChange`, `userProfile`, `userMenuItems`, `bottomSections`를 사용한다. 이벤트를 받은 앱이 데이터 범위와 화면을 변경한다.
52
+ - 보조 설정/탐색 패널은 `panels`와 항목의 `openPanel`을 연결한다. `activePanel="main"`은 닫힌 상태이며 `panelMode="auto|adjacent|replace"`는 보조 패널 배치다. 이를 모바일 전체 사이드바의 drawer 전환으로 혼동하지 않는다.
53
+ - `nav-top`, `nav`, `nav-bottom` 커스텀 슬롯은 기본 구조로 표현하기 어려운 콘텐츠에만 사용한다. 메뉴 전체를 수제 버튼으로 재작성하는 우회로로 쓰지 않는다.
54
+
55
+ ## 3. 비슷한 컨트롤의 선택 기준
56
+
57
+ | 사용자 의도 | 선택 | 데이터와 책임 |
58
+ |---|---|---|
59
+ | 자유 텍스트 입력·검색 | Input / Textarea | 입력 문자열, 검증, 검색 실행은 앱 |
60
+ | 여러 옵션 중 값 선택 | Select | `options: { value, label, disabled? }[]`, `value`, `onChange` |
61
+ | 몇 개의 짧은 옵션을 항상 노출 | SegmentControl | 같은 작업 맥락의 보기/조건 전환. `value`와 표시 결과를 앱이 연결 |
62
+ | 독립된 콘텐츠 패널 전환 | Tabs | 패널 구조·선택 상태와 연결. 단순 장식용 탭 금지 |
63
+ | 항목별 명령 실행 | Dropdown | `items: { id, label, disabled?, divider? }[]`, 안정적인 `id`로 분기 |
64
+ | 작은 설정 폼을 잠깐 펼침 | Dropdown content / Popover | 실제 지원하는 콘텐츠 슬롯 사용. 메뉴 항목 안에 폼을 억지로 삽입하지 않음 |
65
+ | 집중해서 완료할 짧은 작업 | Modal | 열림, 초안, 검증, 저장, 실패/닫기를 앱이 연결 |
66
+ | 검색·조건·요약·동작 배치 | FilterBar | 배치만 담당. 필터 계산/요청/초기화는 앱 |
67
+ | 선택한 행에 일괄 작업 | SelectionBar | 필터와 구분. 선택 ID·개수·해제·작업은 앱 |
68
+
69
+ Select는 폼 값 선택, Dropdown은 명령 메뉴를 우선한다. SegmentControl은 소수의 짧은 옵션에 적합하며 옵션이 많거나 라벨이 길면 Select를 검토한다. 현재 SegmentControl은 `tablist`/`tab` 역할을 사용하므로 폼 제출 필드를 대신한다고 가정하지 않는다.
70
+
71
+ ## 4. Input과 Select
72
+
73
+ - Input은 `label`·`name`·`value`와 `onInput(event.detail.value)`를 연결한다. `onChange`는 별도의 commit 이벤트이므로 입력 중 검색과 혼동하지 않는다. 한국어 조합 중 검색 확정/단축키가 입력을 방해하지 않는지도 확인한다.
74
+ - Select는 단일 선택의 `value`가 문자열, 다중 선택은 React에서 문자열 배열이다. `onChange`의 `detail.value`도 모드에 맞춰 처리한다. `event.target.value`를 native select처럼 추측하지 않는다.
75
+ - placeholder가 label을 대신하지 않는다. `required`, `disabled`, `readOnly`, `error`, `errorMessage`, `helperText`는 설치 버전의 타입에 맞춰 적용한다. 비밀번호·숫자 등의 입력 타입과 서버 검증도 구분한다.
76
+ - named Input/Select의 FormData, reset과 controlled state 동기화는 `TEXT-FIELDS.md`, `SELECT.md`를 참고한다. UI만 바꾼 뒤 제출 데이터가 빠지는 상태를 남기지 않는다.
77
+ - 입력과 버튼/셀렉트를 하나의 필드로 결합할 때는 InputGroup의 `slot="control"` 컨테이너를 사용한다. 레이블과 오류는 그룹에 한 번만 두고 보조 컨트롤에는 별도 접근 가능한 이름을 준다.
78
+
79
+ ## 5. FilterBar + 검색 + Select + SegmentControl
80
+
81
+ 직접 자식의 `search`, `filters`, `summary`, `actions` 슬롯을 사용한다. `variant="plain|outlined|filled"`, `size="s|m|l"`, `align="start|end"`를 지원한다. FilterBar에는 검색을 대신 실행하는 `onFilterChange` 같은 API가 없다.
82
+
83
+ ```tsx
84
+ import { useState } from 'react';
85
+ import { Button, FilterBar, Input, Select, SegmentControl } from '@designbasekorea/ui-wc/react';
86
+
87
+ type Project = { id: string; name: string; status: string };
88
+ export function ProjectResults({ projects }: { projects: Project[] }) {
89
+ const [query, setQuery] = useState('');
90
+ const [status, setStatus] = useState('all');
91
+ const [view, setView] = useState('list');
92
+ const visible = projects.filter(project =>
93
+ project.name.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()) &&
94
+ (status === 'all' || project.status === status));
95
+
96
+ return <>
97
+ <FilterBar variant="plain" size="m" aria-label="프로젝트 검색과 필터">
98
+ <div slot="search">
99
+ <Input label="프로젝트 검색" value={query}
100
+ onInput={event => setQuery(event.detail.value)} />
101
+ </div>
102
+ <div slot="filters">
103
+ <Select label="상태" value={status} options={[
104
+ { value: 'all', label: '전체 상태' },
105
+ { value: 'active', label: '진행 중' },
106
+ { value: 'done', label: '완료' },
107
+ ]} onChange={event => setStatus(String(event.detail.value))} />
108
+ </div>
109
+ <span slot="summary" role="status">{visible.length}개 프로젝트</span>
110
+ <div slot="actions">
111
+ <SegmentControl aria-label="프로젝트 보기" value={view} options={[
112
+ { value: 'list', label: '목록' }, { value: 'grid', label: '그리드' },
113
+ ]} onChange={event => setView(event.detail.value)} />
114
+ <Button variant="tertiary" disabled={!query && status === 'all'}
115
+ onPress={() => { setQuery(''); setStatus('all'); }}>필터 초기화</Button>
116
+ </div>
117
+ </FilterBar>
118
+ {visible.length ? (
119
+ view === 'list'
120
+ ? <ul>{visible.map(project => <li key={project.id}>{project.name}</li>)}</ul>
121
+ : <div className="project-grid">{visible.map(project =>
122
+ <article key={project.id}>{project.name}</article>)}</div>
123
+ ) : <p role="status">조건에 맞는 프로젝트가 없습니다. 검색어나 상태를 변경하세요.</p>}
124
+ </>;
125
+ }
126
+ ```
127
+
128
+ 예제의 그리드 클래스는 앱 CSS에서 유효한 토큰과 콘텐츠 최소 폭으로 구성한다. 서버 검색에서는 검색어·조건·정렬 변경 시 페이지를 초기화하고, 필요한 debounce와 이전 응답 취소/무시를 구현한다. 결과 개수도 같은 조건으로 계산한다. URL 동기화가 필요한 제품에서는 뒤로 가기·새로고침 시 필터 복원을 확인한다.
129
+
130
+ 필터가 많으면 주요 조건만 노출하고 추가 조건을 Popover/Dropdown content로 묶는다. 즉시 반영 방식과 '적용' 방식 중 하나를 선택하며, 적용 방식은 draft와 applied 상태를 분리한다. 좁은 화면에서 검색·조건·액션이 넘치거나 메뉴가 잘리지 않는지 실제 컨테이너 폭에서 확인한다.
131
+
132
+ ## 6. Dropdown: 명령 ID와 콘텐츠 모드
133
+
134
+ ```tsx
135
+ import { Dropdown } from '@designbasekorea/ui-wc/react';
136
+
137
+ export function ProjectMenu({ onRename, onArchive }: {
138
+ onRename: () => void; onArchive: () => void;
139
+ }) {
140
+ return <Dropdown label="프로젝트 작업" triggerVariant="tertiary"
141
+ placement="bottom-right" items={[
142
+ { id: 'rename', label: '이름 변경' },
143
+ { id: 'archive', label: '보관' },
144
+ ]} onChange={event => {
145
+ if (event.detail.id === 'rename') onRename();
146
+ if (event.detail.id === 'archive') onArchive();
147
+ }} />;
148
+ }
149
+ ```
150
+
151
+ `detail.value`는 이 구현에서 **표시 라벨 또는 ID**이며 항상 ID가 아니다. 번역해도 안정적인 명령 분기는 `detail.id`/`detail.item.id`로 처리한다. disabled·divider는 명령으로 실행하지 않는다.
152
+
153
+ 폼을 펼칠 때는 직접 자식 하나에 `slot="content"`를 주어 실제 컨트롤을 넣는다. 이 모드는 non-modal dialog이며 일반 Tab 이동을 유지한다. `open`을 제어하면 `onOpen`의 `detail.open`도 상태에 반영한다. 기본 메뉴를 다시 그린 가짜 Dropdown이나 Select 대용 명령 메뉴를 만들지 않는다.
154
+
155
+ ## 7. Modal: 제목·본문·footer와 저장 흐름
156
+
157
+ ```tsx
158
+ import { useState } from 'react';
159
+ import { Button, Input, Modal, ModalBody, ModalFooter } from '@designbasekorea/ui-wc/react';
160
+
161
+ export function RenameProject({ initialName, save }: {
162
+ initialName: string; save: (name: string) => Promise<void>;
163
+ }) {
164
+ const [open, setOpen] = useState(false);
165
+ const [name, setName] = useState(initialName);
166
+ const [saving, setSaving] = useState(false);
167
+ const [error, setError] = useState('');
168
+ async function submit() {
169
+ if (saving || !name.trim()) return;
170
+ setSaving(true); setError('');
171
+ try { await save(name.trim()); setOpen(false); }
172
+ catch { setError('저장하지 못했습니다. 다시 시도해 주세요.'); }
173
+ finally { setSaving(false); }
174
+ }
175
+ return <>
176
+ <Button variant="secondary" onPress={() => {
177
+ setName(initialName); setError(''); setOpen(true);
178
+ }}>이름 변경</Button>
179
+ <Modal title="프로젝트 이름 변경" size="s" open={open}
180
+ closeOnEscape={!saving} closeOnOutsideClick={!saving} showCloseButton={!saving}
181
+ onClose={() => { if (!saving) setOpen(false); }}>
182
+ <ModalBody>
183
+ <Input label="프로젝트 이름" value={name} disabled={saving}
184
+ onInput={event => setName(event.detail.value)} />
185
+ {error && <p role="alert">{error}</p>}
186
+ </ModalBody>
187
+ <ModalFooter>
188
+ <Button variant="tertiary" disabled={saving} onPress={() => setOpen(false)}>취소</Button>
189
+ <Button variant="primary" loading={saving} disabled={saving || !name.trim()}
190
+ onPress={submit}>저장</Button>
191
+ </ModalFooter>
192
+ </Modal>
193
+ </>;
194
+ }
195
+ ```
196
+
197
+ `title`이 기본 헤더를 제공하므로 같은 제목과 닫기 버튼을 다시 넣지 않는다. React는 ModalBody/ModalFooter, Vanilla는 본문 요소와 직접 자식 `<div slot="footer">`를 사용할 수 있다. `onClose` / `db-close`를 앱의 open 상태와 맞춘다. 실패하면 초안을 보존하고 성공 후 닫는다. 예제의 `save`는 소비 앱이 실제 저장에 연결한다.
198
+
199
+ 현재 Modal은 `headerVariant="default|tabs|segment"`를 제공한다. 헤더 전환은 `tabs`/`segments`, `selectedTabId`/`segmentValue`, `onTabChange`/`onSegmentChange`에 연결하며 본문 상태도 함께 전환한다. 장문의 복잡한 작업은 전체 페이지나 적절한 패널을 검토하고, 단순 파괴적 확인은 공식 confirm API를 먼저 확인한다.
200
+
201
+ ## 8. 예제 적용 후 확인
202
+
203
+ - 공식 패키지의 CSS·theme와 요소 등록이 로드되었는지 확인한다. SSR 프로젝트는 브라우저 전용 등록 위치를 구분한다.
204
+ - React의 onInput/onChange는 wrapper가 연결한 CustomEvent다. Vanilla 대응은 Input `db-input`, Select/SegmentControl/Dropdown `db-change`, Sidebar `db-item-click`, Modal `db-close`다. 같은 이벤트의 별칭 두 개를 동시에 구독해 이중 실행하지 않는다.
205
+ - 실제 화면에서 메뉴 열기/명령 실행, 선택 변경/결과 갱신, 모달 입력/실패/재시도/닫기, 포커스 진입/복귀와 Escape를 확인한다.
206
+ - 긴 라벨·빈 결과·작은 컨테이너·모바일에서 잘림과 overflow를 확인한다. 전체 앱 브레이크포인트와 각 컴포넌트의 container query 기준을 구분한다.
@@ -58,3 +58,10 @@ A controlled commit echo does not reset native history. A genuinely different pr
58
58
  Pointer ownership is scoped to the active pointer. Nested-editor or unrelated cancellation events must not cancel another gesture. A delayed lostpointercapture event is ignored while a newer capture is still held. Do not recreate the custom element or change its React key during a gesture; stable controlled properties allow the native editor to preserve its draft. Locale/theme changes should be applied through the existing providers.
59
59
 
60
60
  Tests: test-timeline-effects.mjs, test-timeline-key-selection.mjs, test-timeline-key-context.mjs and test-media-precision.mjs cover the actual native/React paths. The Tools gallery's Effects/property-keyframes example applies X/Y/scale/opacity to a live sample object.
61
+
62
+
63
+ ## Source channels and applied-effect ranges
64
+
65
+ Use `scope:'source'` for original layer X/Y/Size/Rotation (or your own source channel set), and `scope:'effect'` for applied effects. Legacy unscoped transform groups are source channels. The layer name now owns expansion; applied effects sit one level deeper and own separate range bars. A missing source group is normalized to editable default transform channels. See [media editor hierarchy and range contract](./MEDIA-EDITORS.md#source-layers-and-nested-effects).
66
+
67
+ `effect.start` / `effect.duration` define the clip-local activation interval. Keys remain clip-local even when this range changes. Persist the complete `clips` array from `db-change` for `action:'effect-range'`; add one history entry, not one per draft. The host renderer must evaluate enabled effects only during their activation intervals. Source transforms are independent of effect ranges.
@@ -61,3 +61,23 @@ The Effects/property-keyframes demo drives a live X/Y/scale/opacity sample. It u
61
61
  U, while the timeline is focused outside text inputs, expands/collapses the selected clip's effect properties. Click a key to select it; Shift toggles additive selection. Drag empty property-lane space to box-select visible keys across rows (Shift adds). Selected diamonds are filled and outlined. Drag a selected key to move the selection by one shared frame-quantized delta, bounded by clip edges and unselected neighbors. Delete/Backspace removes selected keys; the two-level Context Menu applies easing presets to their outgoing segments. Detailed property graphs still expose exact values and Bezier tangents. Escape rolls back timing drags or box selection. Timeline tool text is nonselectable; actual text-entry controls remain selectable. Value readouts target a dedicated class and never replace Button icon nodes.
62
62
 
63
63
  Alt/Option + key drag duplicates the entire selected key group, preserving original IDs, values and tangent data. Copies receive new IDs and a shared frame offset; landing on an existing key frame is rejected. Duplicate, delete and easing actions are available from the real ContextMenu with an Easing submenu. Plain dragging anywhere in blank effect content starts a marquee. Cmd/Ctrl+Z undoes committed timeline edits and Shift+Cmd/Ctrl+Z redoes them (up to 100 snapshots); optimistic moves and selection changes do not create history. External clip replacements reset native history; own controlled commit echoes do not. Undo/redo emits db-change actions undo/redo for consumers with their own history integration.
64
+
65
+
66
+ ### Label panel and composition boundary
67
+
68
+ `label-width` / React `labelWidth` defaults to 144px (120–480px). Drag any vertical divider on the ruler, track name, or effect property label. Arrow keys resize a focused divider, Shift uses larger steps, Home resets; Escape cancels pointer resizing. Persist `db-label-resize.detail.width` / `onLabelResize` alongside editor preferences. Long names use single-line ellipsis and retain their complete title for inspection.
69
+
70
+ Single-effect clips use one Effects disclosure; multiple effects retain per-effect grouping. `U` expands/collapses the active clip's effect controls.
71
+
72
+ `duration` remains the composition/playback boundary. Moving a clip beyond it preserves its start and full duration, including when controlled hosts reapply `clips`; the canvas expands to show the clip, and only the part after the boundary is faded. Playback remains bounded by `duration`; exporting/rendering outside the composition remains a host decision. Media source limits still apply to trimming. Collision protection remains active; Cmd/Ctrl insert moves following clips when required.
73
+
74
+
75
+ ### Source layers and nested effects
76
+
77
+ The track-name button owns the layer disclosure. Click its small chevron or name, or use `U`. There is no separate Applied Effects heading. Source channels appear directly beneath the layer, while applied effects have indented names, independently editable range bars and their own property disclosures.
78
+
79
+ `VideoClipEffect.scope` is `source` or `effect`. Explicit scope takes precedence; for compatibility an unscoped `kind:'transform'` group is treated as source. An applied transform effect must set `scope:'effect'`. When a clip has no source group, the `clips` setter normalizes one with stable IDs and X=0, Y=0, Size=100, Rotation=0 keys at clip-local time 0. These are ordinary returned document data and can be saved/edited with the same keyframe and undo APIs. If a host supplies its own source group, its channels are preserved without adding defaults.
80
+
81
+ Applied effects accept optional clip-local `start` and `duration`. Missing values cover the full parent clip. Drag the effect bar to move its range, drag either 3px grip to trim, or focus a grip and use arrows (Shift: ten frames). Range changes emit `db-input` while dragging and one `db-change` with `action:'effect-range'`, `clipId` and `effectId` on release. Escape cancels; Cmd/Ctrl+Z restores the transaction. Editing a range does not move the parent clip, siblings or property keys: key times remain clip-local. The rendering engine must apply an effect only during its enabled range. Ranges can extend beyond the composition; the engine still decides what to render/export.
82
+
83
+ `layer-gap` / React `layerGap` is 0–32px (default 0). Gaps appear only between parent tracks when there are multiple tracks, never between a source property and its effects. Save this presentation setting with editor preferences. Labels use ellipsis at every depth; the left-panel divider still resizes all source and effect rows together.
@@ -0,0 +1,200 @@
1
+ # 툴·편집기 UI: ToolLayout과 ToolPanel
2
+
3
+ ui-wc 0.16.0의 실제 구현·React wrapper 기준이다. 다른 버전은 설치된 타입·소스를 우선한다. 이 문서는 레이아웃과 컨트롤의 통합 지침이며 캔버스 엔진·문서 모델·저장 서버를 제공하는 것으로 해석하지 않는다.
4
+
5
+ ## 툴 화면의 기본 선택
6
+
7
+ 이미지/영상 편집기, 디자인 도구, 변환 도구처럼 작업 대상과 보조 패널이 동시에 필요한 화면은 **ToolLayout으로 영역을 배치하고 ToolPanel로 패널을 구성한다**. 수제 flex/aside/card로 같은 셸을 먼저 만든 뒤 버튼만 공식 컴포넌트로 바꾸지 않는다.
8
+
9
+ 설정 페이지·단순 CRUD 폼은 일반 앱 셸과 폼이 더 적절할 수 있다. SaaS 앱 내부의 편집기는 기존 Sidebar 탐색과 ToolLayout을 조합하되 앱 탐색과 파일/레이어 패널을 중복하지 않는다. `Sidebar type="app"`은 앱 탐색이고 ToolLayout의 `sidebar` 슬롯은 작업 보조 영역이다. 같은 이름이어도 같은 역할이 아니다.
10
+
11
+ ## 1. ToolLayout: 영역과 배치
12
+
13
+ 각 슬롯은 **직접 자식 하나**로 구성한다. 영역 내 요소가 여러 개면 컨테이너 하나로 묶는다. `main`은 필수이고 나머지는 필요한 것만 사용한다.
14
+
15
+ | 슬롯 | 책임 | 대표 콘텐츠 |
16
+ |---|---|---|
17
+ | `header` | 문서/프로젝트 수준의 동작 | 문서명, 저장 상태, 패널 복원, 내보내기 |
18
+ | `rail` | 도구/작업 공간의 짧은 탐색 | NavigationRail 또는 세로 DesignToolbar |
19
+ | `sidebar` | 작업 리소스 탐색 | 파일, 레이어, 에셋 ToolPanel |
20
+ | `main` | 핵심 작업 대상 | 캔버스, 미리보기, 비교 화면 |
21
+ | `inspector` | 현재 선택의 속성 | 크기, 정렬, 채우기, 효과 ToolPanel |
22
+ | `bottom` | 시간/보조 작업 | 타임라인, 콘솔, 작업 큐 ToolPanel |
23
+ | `status` | 작업 상태 요약 | 저장 결과, 선택 개수, 처리 상태 |
24
+
25
+ | variant | 외형과 용도 |
26
+ |---|---|
27
+ | `floating` | 패널 사이에 gap이 있는 분리된 표면 |
28
+ | `flush` | gap/padding 없이 경계선으로 이어지는 작업 화면 |
29
+ | `inset` | rail 옆에 하나의 연속 작업 표면이 들어간 구성 |
30
+
31
+ variant는 외형이며 업무 모드가 아니다. 전용 슬롯이 없는 `toolbar`/`canvas`/`properties` 같은 이름을 추측해 넣지 않는다. 툴바는 역할에 맞춰 `rail`, `header` 또는 `main` 내부에 배치한다.
32
+
33
+ ### 높이·스크롤·크기
34
+
35
+ - `fullHeight`는 `height:100%`이므로 부모가 실제 높이를 가져야 한다. 페이지 전체라면 앱의 높이 체인을 구성하고, 임베드라면 해당 컨테이너 높이를 지정한다. 앱 헤더가 있는데 무조건 `100vh`를 더하지 않는다.
36
+ - 패널 본문은 ToolPanel이 스크롤을 소유한다. 캔버스의 pan/zoom과 페이지 스크롤을 구분하고, 모든 부모에 `overflow:hidden`을 넣어 메뉴/포커스/내용을 잘라내지 않는다.
37
+ - `panelSizes={{sidebar, inspector, bottom}}`는 px 숫자다. `sidebarMin/Max`, `inspectorMin/Max`, `bottomMin/Max`, `mainMinSize`로 작업 영역의 가독성을 보호한다. 치수는 레이아웃 데이터이며 색상 같은 의미 토큰으로 강제 치환하지 않는다.
38
+ - CSS 공개 훅은 `--db-tool-rail-width`, `--db-tool-sidebar-width`, `--db-tool-inspector-width`, `--db-tool-bottom-height`, `--db-tool-gap`, `--db-tool-radius`다. gap/radius/색상은 유효한 시스템 토큰과 연결한다. `--db-tool-columns/rows/areas` 같은 내부 계산 결과는 덮어쓰지 않는다.
39
+ - `resizable` 기본은 true다. 리사이즈 핸들은 포인터와 키보드를 지원한다. 방향키는 8px, Shift+방향키는 32px, Home/End는 가능한 최소/최대다. 포인터 취소는 원래 크기로 돌아간다.
40
+ - `onPanelResize` detail은 `{panel, size, sizes, phase}`이고 phase는 `change|end|cancel`이다. 화면 상태는 이벤트에 맞추되 영구 저장은 보통 `end`에서 한다. `cancel`을 저장 성공으로 다루지 않는다. 재접속 복원을 약속했다면 설정 읽기까지 구현한다.
41
+
42
+ ### 닫기·접기·반응형은 다르다
43
+
44
+ - `sidebarHidden`, `inspectorHidden`, `bottomHidden`은 영역 전체를 숨긴다. 다시 여는 컨트롤은 항상 접근 가능한 header/rail 등에 둔다.
45
+ - Vanilla의 `setPanelOpen(panel, open)` / `togglePanel(panel)`은 `db-panel-toggle {panel,open}`를 발생시킨다. 속성/property만 바꾼다고 같은 이벤트가 발생한다고 가정하지 않는다.
46
+ - ToolPanel이 ToolLayout의 **직접 자식**이면 패널의 `db-close`를 Layout이 받아 해당 영역을 숨긴다. 이 경우 `onPanelToggle`로 controlled 상태를 동기화한다. ToolPanel을 div 안에 중첩했다면 이 자동 연결을 기대하지 말고 앱이 닫기를 처리한다.
47
+ - API로 닫을 때 포커스가 패널 내부에 있으면 `[data-tool-toggle="영역명"]` 또는 main으로 복귀한다. 선언적인 hidden 변경은 동일한 포커스 처리를 보장하지 않으므로 별도로 확인한다.
48
+ - 반응형은 뷰포트가 아닌 **ToolLayout 자신의 폭**을 사용한다. ≥1024px은 좌우 배치, 768–1023px은 inspector가 아래로, ≤767px은 sidebar와 inspector 모두 아래로 쌓인다. 자동 drawer 전환이 아니다. 필요한 모바일 전용 전환은 앱에서 실제 접근 가능한 UI로 구현한다.
49
+
50
+ ## 2. ToolPanel: 패널의 내부 구조
51
+
52
+ | API | 역할/주의 |
53
+ |---|---|
54
+ | `heading` | 라이브러리가 제목과 접기 트리거를 구성. 같은 헤더를 본문에 중복하지 않음 |
55
+ | `slot="actions"` | 헤더의 보조 동작을 컨테이너 하나에 배치. 제목 접기와 독립적으로 동작 |
56
+ | `slot="body"` | 본문 컨테이너 하나. 일반 필드·목록·중첩 section 배치 |
57
+ | `slot="footer"` | 본문 밖의 지속 동작/요약. 예: 선택 항목 내보내기 |
58
+ | `padding="none|s|m"` | 본문 패딩. 리스트/캔버스가 자체 inset을 소유하면 none 검토 |
59
+ | `appearance="default|section|card"` | 기본 패널 / 내부 독립 섹션 / 경계가 있는 카드 |
60
+ | `collapsible`, `collapsed` | 본문만 접기. 영역 전체 닫기와 구분 |
61
+ | `closable` | 닫기 의도 이벤트. 단독 ToolPanel은 앱이 숨김/제거를 처리 |
62
+ | `divider` | 섹션 하단 경계. 모든 패널에 중복 테두리를 추가하지 않음 |
63
+
64
+ 한 패널에 여러 `slot="body"` 자식을 나열하면 같은 grid 영역에 겹칠 수 있다. 하나의 body 컨테이너 아래에 필요한 항목을 넣는다. `actions`는 접혀도 남으므로 해당 동작이 숨겨진 필드를 변경한다면 결과가 이해되게 구성한다.
65
+
66
+ ### 탭과 독립 섹션
67
+
68
+ `tabs`는 `{id,label,disabled?}[]`, 선택은 `selectedTab`, 이벤트는 `onTabChange(detail.id)`다. 각 본문은 직접 자식 `slot="tab-<id>"`에 연결한다. 예를 들어 design 탭은 `tab-design`이다. Modal의 `detail.tabId`와 혼동하지 않는다.
69
+
70
+ 탭을 쓰면 공통 body와 tab 본문이 같은 grid 영역에 겹치지 않게 구성한다. 각 tab에 한 컨테이너를 두고, 안에 `appearance="section" collapsible` 패널을 중첩한다. 카드 안에 다시 모든 항목을 카드로 감싸지 않는다. collapse/tab 변경에서 자식 DOM은 보존되지만, 앱이 조건부 unmount하면 초안 보존은 앱의 책임이다.
71
+
72
+ ```tsx
73
+ import { useState } from 'react';
74
+ import { Input, ToolPanel } from '@designbasekorea/ui-wc/react';
75
+
76
+ export function ObjectInspector({ name, onRename, metadata }: {
77
+ name: string; onRename: (value: string) => void; metadata: string;
78
+ }) {
79
+ const [tab, setTab] = useState('design');
80
+ const [collapsed, setCollapsed] = useState(false);
81
+ return <ToolPanel heading="선택한 객체" padding="none"
82
+ tabs={[{ id: 'design', label: '디자인' }, { id: 'info', label: '정보' }]}
83
+ selectedTab={tab} onTabChange={event => setTab(event.detail.id)}>
84
+ <div slot="tab-design">
85
+ <ToolPanel heading="기본 속성" appearance="section" collapsible divider
86
+ collapsed={collapsed}
87
+ onCollapseChange={event => setCollapsed(event.detail.collapsed)}>
88
+ <div slot="body">
89
+ <Input label="객체 이름" value={name}
90
+ onInput={event => onRename(event.detail.value)} />
91
+ </div>
92
+ </ToolPanel>
93
+ </div>
94
+ <div slot="tab-info"><p>{metadata}</p></div>
95
+ </ToolPanel>;
96
+ }
97
+ ```
98
+
99
+ 실제 inspector는 선택 없음/단일/다중 상태를 구분한다. 이름·크기·채우기 등 필드 변경은 선택한 문서 객체에 반영하고 undo/redo와 저장 모델에 연결한다. 선택을 바꿨는데 이전 객체를 수정하거나 잠긴 객체에 편집이 적용되는 상태를 남기지 않는다. 선택이 없으면 객체 필드를 비활성화하거나 안내 상태를 보여준다.
100
+
101
+ ## 3. 연결된 ToolLayout 예제
102
+
103
+ 아래는 소비 앱이 제공한 실제 리소스·캔버스·속성·타임라인을 슬롯에 연결하는 셸이다. 가짜 편집 엔진이나 저장 성공을 포함하지 않는다. 닫기/복원·패널 크기 상태는 연결되어 있다.
104
+
105
+ ```tsx
106
+ import { useState, type ReactNode } from 'react';
107
+ import { Button, ToolLayout, ToolPanel, type ToolLayoutSizes } from '@designbasekorea/ui-wc/react';
108
+
109
+ export function EditorLayout({ resources, canvas, inspector, timeline,
110
+ initialSizes, commitSizes,
111
+ }: {
112
+ resources: ReactNode; canvas: ReactNode; inspector: ReactNode; timeline: ReactNode;
113
+ initialSizes: ToolLayoutSizes;
114
+ commitSizes: (sizes: ToolLayoutSizes) => void;
115
+ }) {
116
+ const [hidden, setHidden] = useState({ sidebar: false, inspector: false, bottom: false });
117
+ const [sizes, setSizes] = useState(initialSizes);
118
+ return <div className="editor-frame">
119
+ <ToolLayout variant="flush" fullHeight resizable panelSizes={sizes}
120
+ sidebarHidden={hidden.sidebar} inspectorHidden={hidden.inspector} bottomHidden={hidden.bottom}
121
+ onPanelToggle={event => {
122
+ const { panel, open } = event.detail;
123
+ setHidden(previous => ({ ...previous, [panel]: !open }));
124
+ }} onPanelResize={event => {
125
+ setSizes(event.detail.sizes);
126
+ if (event.detail.phase === 'end') commitSizes(event.detail.sizes);
127
+ }}>
128
+ <header slot="header" className="editor-header">
129
+ <span>편집 작업 공간</span>
130
+ <Button variant="tertiary" data-tool-toggle="sidebar" aria-expanded={!hidden.sidebar}
131
+ onPress={() => setHidden(previous => ({ ...previous, sidebar: !previous.sidebar }))}>리소스</Button>
132
+ <Button variant="tertiary" data-tool-toggle="inspector" aria-expanded={!hidden.inspector}
133
+ onPress={() => setHidden(previous => ({ ...previous, inspector: !previous.inspector }))}>속성</Button>
134
+ <Button variant="tertiary" data-tool-toggle="bottom" aria-expanded={!hidden.bottom}
135
+ onPress={() => setHidden(previous => ({ ...previous, bottom: !previous.bottom }))}>타임라인</Button>
136
+ </header>
137
+ <ToolPanel slot="sidebar" heading="리소스" closable>
138
+ <div slot="body">{resources}</div>
139
+ </ToolPanel>
140
+ <main slot="main" tabIndex={-1} aria-label="편집 캔버스">{canvas}</main>
141
+ <ToolPanel slot="inspector" heading="속성" closable>
142
+ <div slot="body">{inspector}</div>
143
+ </ToolPanel>
144
+ <ToolPanel slot="bottom" heading="타임라인" padding="none" closable>
145
+ <div slot="body">{timeline}</div>
146
+ </ToolPanel>
147
+ </ToolLayout>
148
+ </div>;
149
+ }
150
+ ```
151
+
152
+ ```css
153
+ /* 앱의 실제 높이 컨테이너 안에 배치한다. */
154
+ .editor-frame { height: 100%; min-height: 0; min-width: 0; }
155
+ .editor-header {
156
+ display: flex; flex-wrap: wrap; align-items: center;
157
+ gap: var(--db-gap-s); padding: var(--db-padding-s);
158
+ }
159
+ ```
160
+
161
+ `initialSizes`는 검증한 저장 설정 또는 앱의 기본 크기를 전달한다. `commitSizes`는 앱 설정 저장에 연결한다. 캔버스 엔진을 감싸는 컨테이너는 `min-width/min-height:0`과 엔진의 resize 처리를 확인한다. 숨긴 뒤 다시 열어도 캔버스 치수·선택·미저장 문서가 보존되어야 한다. 예제의 inspector에는 본문을 전달한다. ObjectInspector처럼 전체 ToolPanel을 전달한다면 바깥 inspector 패널을 중복하지 않도록 조합을 조정한다.
162
+
163
+ ## 4. 도구와 선택 문맥
164
+
165
+ | 컴포넌트 | 역할 | 앱이 연결할 것 |
166
+ |---|---|---|
167
+ | NavigationRail | 앱/작업 공간 이동 | 현재 영역과 라우팅. 커서 도구와 구분 |
168
+ | DesignToolbar | 자주 쓰는 편집 도구 묶음 | 선택 도구, 명령 실행, 키보드 단축키 |
169
+ | ToolButton | 개별 편집 동작·도구 옵션 | `toolId`, `active`, `options`, `value`, `onToolAction` |
170
+ | ContextBar | 선택 문맥에 따른 편집 도구 | 선택 객체에 맞는 항목 표시·변경 |
171
+ | SelectionBar | 여러 파일/행의 일괄 작업 | 선택 개수, 해제, 일괄 명령 |
172
+ | ZoomControl | 확대/축소·맞춤·전체 화면 | 퍼센트 → 실제 viewport scale, fit 대상/크기 |
173
+
174
+ DesignToolbar/ContextBar는 `items` 배열을 사용한다. item의 `id`와 `active/disabled`, 선택 옵션의 `value`를 문서 상태와 맞추고 `onToolAction`의 `{id,optionId?,option?}`로 분기한다. `shortcut`은 표시이며 실제 앱 단축키를 등록하는 기능으로 가정하지 않는다. 입력 중/한국어 조합 중 편집 단축키가 실행되지 않게 한다.
175
+
176
+ ```tsx
177
+ import { DesignToolbar } from '@designbasekorea/ui-wc/react';
178
+
179
+ export function EditorTools({ tool, onToolChange }: {
180
+ tool: 'select' | 'pan'; onToolChange: (tool: 'select' | 'pan') => void;
181
+ }) {
182
+ return <DesignToolbar aria-label="캔버스 도구" orientation="horizontal" items={[
183
+ { id: 'select', label: '선택 도구', text: '선택', active: tool === 'select' },
184
+ { id: 'pan', label: '이동 도구', text: '이동', active: tool === 'pan' },
185
+ ]} onToolAction={event => {
186
+ const id = event.detail.id;
187
+ if (id === 'select' || id === 'pan') onToolChange(id);
188
+ }} />;
189
+ }
190
+ ```
191
+
192
+ 아이콘을 추가할 때는 공식 등록 이름을 확인한다. `movable`을 쓰면 `toolbarPosition`과 `onPositionChange`를 연결하고 실제 배치 경계·포인터 취소·크기 변경을 확인한다. 떠 있는 툴바는 문서 콘텐츠 위에 위치하므로 컨테이너의 position/좌표계, canvas zoom, 포털 메뉴의 겹침을 함께 확인한다.
193
+
194
+ ## 5. 툴 화면 완료 기준
195
+
196
+ - panel 닫기와 section 접기를 각각 실행하고 복원 경로·포커스·controlled 상태를 확인한다.
197
+ - 패널 크기를 드래그/키보드로 바꾸고 취소·최소/최대·저장/복원을 확인한다. 크기 변경 후 캔버스 엔진도 새 영역에 맞춰야 한다.
198
+ - 탭과 속성 섹션 변경 후 입력값과 포커스가 의도대로 유지되는지 확인한다. 헤더의 actions 클릭이 패널 접기를 유발하면 안 된다.
199
+ - 선택 → 속성 변경 → 다른 선택 → undo/redo → 저장/재접속 중 해당 제품에 있는 흐름을 검증한다.
200
+ - 전체 창뿐 아니라 작은 임베드 폭, 좁은 높이에서도 main의 가독성, 각 패널 스크롤, 포털 메뉴, 하단 타임라인 접근성을 확인한다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@designbasekorea/ui-wc",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Supernormal Web Components 및 React wrapper — by Designbase",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",