@hjmds/design-contracts 0.8.2
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.
- package/README.md +208 -0
- package/dist/alert-dialog.d.ts +102 -0
- package/dist/alert-dialog.d.ts.map +1 -0
- package/dist/alert-dialog.js +136 -0
- package/dist/alert-dialog.js.map +1 -0
- package/dist/base-recipes.d.ts +184 -0
- package/dist/base-recipes.d.ts.map +1 -0
- package/dist/base-recipes.js +129 -0
- package/dist/base-recipes.js.map +1 -0
- package/dist/behaviors.d.ts +1254 -0
- package/dist/behaviors.d.ts.map +1 -0
- package/dist/behaviors.js +972 -0
- package/dist/behaviors.js.map +1 -0
- package/dist/bottom-navigation-defaults.d.ts +13 -0
- package/dist/bottom-navigation-defaults.d.ts.map +1 -0
- package/dist/bottom-navigation-defaults.js +10 -0
- package/dist/bottom-navigation-defaults.js.map +1 -0
- package/dist/bottom-navigation.d.ts +114 -0
- package/dist/bottom-navigation.d.ts.map +1 -0
- package/dist/bottom-navigation.js +223 -0
- package/dist/bottom-navigation.js.map +1 -0
- package/dist/breadcrumb.d.ts +97 -0
- package/dist/breadcrumb.d.ts.map +1 -0
- package/dist/breadcrumb.js +99 -0
- package/dist/breadcrumb.js.map +1 -0
- package/dist/calendar.d.ts +285 -0
- package/dist/calendar.d.ts.map +1 -0
- package/dist/calendar.js +297 -0
- package/dist/calendar.js.map +1 -0
- package/dist/card.d.ts +39 -0
- package/dist/card.d.ts.map +1 -0
- package/dist/card.js +50 -0
- package/dist/card.js.map +1 -0
- package/dist/carousel.d.ts +180 -0
- package/dist/carousel.d.ts.map +1 -0
- package/dist/carousel.js +172 -0
- package/dist/carousel.js.map +1 -0
- package/dist/catalog.d.ts +7158 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +220 -0
- package/dist/catalog.js.map +1 -0
- package/dist/collection.d.ts +123 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +211 -0
- package/dist/collection.js.map +1 -0
- package/dist/color-references.d.ts +46 -0
- package/dist/color-references.d.ts.map +1 -0
- package/dist/color-references.js +39 -0
- package/dist/color-references.js.map +1 -0
- package/dist/colors.d.ts +56 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +90 -0
- package/dist/colors.js.map +1 -0
- package/dist/command-palette.d.ts +240 -0
- package/dist/command-palette.d.ts.map +1 -0
- package/dist/command-palette.js +97 -0
- package/dist/command-palette.js.map +1 -0
- package/dist/component-contracts.d.ts +137 -0
- package/dist/component-contracts.d.ts.map +1 -0
- package/dist/component-contracts.js +53 -0
- package/dist/component-contracts.js.map +1 -0
- package/dist/component-definitions.d.ts +135 -0
- package/dist/component-definitions.d.ts.map +1 -0
- package/dist/component-definitions.js +142 -0
- package/dist/component-definitions.js.map +1 -0
- package/dist/component-recipes.d.ts +3583 -0
- package/dist/component-recipes.d.ts.map +1 -0
- package/dist/component-recipes.js +1503 -0
- package/dist/component-recipes.js.map +1 -0
- package/dist/component-references.d.ts +416 -0
- package/dist/component-references.d.ts.map +1 -0
- package/dist/component-references.js +140 -0
- package/dist/component-references.js.map +1 -0
- package/dist/content-state.d.ts +104 -0
- package/dist/content-state.d.ts.map +1 -0
- package/dist/content-state.js +116 -0
- package/dist/content-state.js.map +1 -0
- package/dist/counter-badge-recipe.d.ts +80 -0
- package/dist/counter-badge-recipe.d.ts.map +1 -0
- package/dist/counter-badge-recipe.js +44 -0
- package/dist/counter-badge-recipe.js.map +1 -0
- package/dist/counter-badge.d.ts +12 -0
- package/dist/counter-badge.d.ts.map +1 -0
- package/dist/counter-badge.js +21 -0
- package/dist/counter-badge.js.map +1 -0
- package/dist/data-table.d.ts +203 -0
- package/dist/data-table.d.ts.map +1 -0
- package/dist/data-table.js +182 -0
- package/dist/data-table.js.map +1 -0
- package/dist/date-picker.d.ts +268 -0
- package/dist/date-picker.d.ts.map +1 -0
- package/dist/date-picker.js +168 -0
- package/dist/date-picker.js.map +1 -0
- package/dist/description-list.d.ts +68 -0
- package/dist/description-list.d.ts.map +1 -0
- package/dist/description-list.js +85 -0
- package/dist/description-list.js.map +1 -0
- package/dist/design-system-provider.d.ts +80 -0
- package/dist/design-system-provider.d.ts.map +1 -0
- package/dist/design-system-provider.js +147 -0
- package/dist/design-system-provider.js.map +1 -0
- package/dist/evidence.d.ts +49 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +133 -0
- package/dist/evidence.js.map +1 -0
- package/dist/file-picker.d.ts +183 -0
- package/dist/file-picker.d.ts.map +1 -0
- package/dist/file-picker.js +224 -0
- package/dist/file-picker.js.map +1 -0
- package/dist/floating-action-button.d.ts +143 -0
- package/dist/floating-action-button.d.ts.map +1 -0
- package/dist/floating-action-button.js +149 -0
- package/dist/floating-action-button.js.map +1 -0
- package/dist/form.d.ts +143 -0
- package/dist/form.d.ts.map +1 -0
- package/dist/form.js +206 -0
- package/dist/form.js.map +1 -0
- package/dist/foundations.d.ts +300 -0
- package/dist/foundations.d.ts.map +1 -0
- package/dist/foundations.js +238 -0
- package/dist/foundations.js.map +1 -0
- package/dist/grid.d.ts +75 -0
- package/dist/grid.d.ts.map +1 -0
- package/dist/grid.js +133 -0
- package/dist/grid.js.map +1 -0
- package/dist/icon-button-recipe.d.ts +99 -0
- package/dist/icon-button-recipe.d.ts.map +1 -0
- package/dist/icon-button-recipe.js +53 -0
- package/dist/icon-button-recipe.js.map +1 -0
- package/dist/icon.d.ts +41 -0
- package/dist/icon.d.ts.map +1 -0
- package/dist/icon.js +147 -0
- package/dist/icon.js.map +1 -0
- package/dist/image.d.ts +82 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +100 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +118 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +118 -0
- package/dist/layout.js.map +1 -0
- package/dist/link.d.ts +57 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +142 -0
- package/dist/link.js.map +1 -0
- package/dist/load-more.d.ts +64 -0
- package/dist/load-more.d.ts.map +1 -0
- package/dist/load-more.js +124 -0
- package/dist/load-more.js.map +1 -0
- package/dist/mentions.d.ts +67 -0
- package/dist/mentions.d.ts.map +1 -0
- package/dist/mentions.js +107 -0
- package/dist/mentions.js.map +1 -0
- package/dist/number-field.d.ts +208 -0
- package/dist/number-field.d.ts.map +1 -0
- package/dist/number-field.js +247 -0
- package/dist/number-field.js.map +1 -0
- package/dist/otp-field.d.ts +152 -0
- package/dist/otp-field.d.ts.map +1 -0
- package/dist/otp-field.js +117 -0
- package/dist/otp-field.js.map +1 -0
- package/dist/pagination.d.ts +181 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +226 -0
- package/dist/pagination.js.map +1 -0
- package/dist/password-field.d.ts +187 -0
- package/dist/password-field.d.ts.map +1 -0
- package/dist/password-field.js +120 -0
- package/dist/password-field.js.map +1 -0
- package/dist/popover.d.ts +152 -0
- package/dist/popover.d.ts.map +1 -0
- package/dist/popover.js +137 -0
- package/dist/popover.js.map +1 -0
- package/dist/progress-recipe.d.ts +43 -0
- package/dist/progress-recipe.d.ts.map +1 -0
- package/dist/progress-recipe.js +16 -0
- package/dist/progress-recipe.js.map +1 -0
- package/dist/recipes.d.ts +31 -0
- package/dist/recipes.d.ts.map +1 -0
- package/dist/recipes.js +46 -0
- package/dist/recipes.js.map +1 -0
- package/dist/responsive.d.ts +27 -0
- package/dist/responsive.d.ts.map +1 -0
- package/dist/responsive.js +66 -0
- package/dist/responsive.js.map +1 -0
- package/dist/result.d.ts +111 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +97 -0
- package/dist/result.js.map +1 -0
- package/dist/selection-helpers.d.ts +16 -0
- package/dist/selection-helpers.d.ts.map +1 -0
- package/dist/selection-helpers.js +52 -0
- package/dist/selection-helpers.js.map +1 -0
- package/dist/semantic-colors.d.ts +275 -0
- package/dist/semantic-colors.d.ts.map +1 -0
- package/dist/semantic-colors.js +84 -0
- package/dist/semantic-colors.js.map +1 -0
- package/dist/sheet.d.ts +51 -0
- package/dist/sheet.d.ts.map +1 -0
- package/dist/sheet.js +69 -0
- package/dist/sheet.js.map +1 -0
- package/dist/showcase.d.ts +153 -0
- package/dist/showcase.d.ts.map +1 -0
- package/dist/showcase.js +210 -0
- package/dist/showcase.js.map +1 -0
- package/dist/side-panel.d.ts +199 -0
- package/dist/side-panel.d.ts.map +1 -0
- package/dist/side-panel.js +111 -0
- package/dist/side-panel.js.map +1 -0
- package/dist/slider.d.ts +138 -0
- package/dist/slider.d.ts.map +1 -0
- package/dist/slider.js +150 -0
- package/dist/slider.js.map +1 -0
- package/dist/splitter.d.ts +113 -0
- package/dist/splitter.d.ts.map +1 -0
- package/dist/splitter.js +99 -0
- package/dist/splitter.js.map +1 -0
- package/dist/statistic.d.ts +41 -0
- package/dist/statistic.d.ts.map +1 -0
- package/dist/statistic.js +76 -0
- package/dist/statistic.js.map +1 -0
- package/dist/steps.d.ts +196 -0
- package/dist/steps.d.ts.map +1 -0
- package/dist/steps.js +160 -0
- package/dist/steps.js.map +1 -0
- package/dist/tag.d.ts +128 -0
- package/dist/tag.d.ts.map +1 -0
- package/dist/tag.js +93 -0
- package/dist/tag.js.map +1 -0
- package/dist/timeline.d.ts +147 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +127 -0
- package/dist/timeline.js.map +1 -0
- package/dist/toast.d.ts +164 -0
- package/dist/toast.d.ts.map +1 -0
- package/dist/toast.js +529 -0
- package/dist/toast.js.map +1 -0
- package/dist/tokens.d.ts +9 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +9 -0
- package/dist/tokens.js.map +1 -0
- package/dist/tooltip.d.ts +44 -0
- package/dist/tooltip.d.ts.map +1 -0
- package/dist/tooltip.js +88 -0
- package/dist/tooltip.js.map +1 -0
- package/dist/tour.d.ts +218 -0
- package/dist/tour.d.ts.map +1 -0
- package/dist/tour.js +211 -0
- package/dist/tour.js.map +1 -0
- package/dist/transfer-list.d.ts +207 -0
- package/dist/transfer-list.d.ts.map +1 -0
- package/dist/transfer-list.js +193 -0
- package/dist/transfer-list.js.map +1 -0
- package/dist/tree-select.d.ts +78 -0
- package/dist/tree-select.d.ts.map +1 -0
- package/dist/tree-select.js +132 -0
- package/dist/tree-select.js.map +1 -0
- package/dist/tree.d.ts +206 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +223 -0
- package/dist/tree.js.map +1 -0
- package/dist/upload-item.d.ts +173 -0
- package/dist/upload-item.d.ts.map +1 -0
- package/dist/upload-item.js +156 -0
- package/dist/upload-item.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/docs/affix.md +63 -0
- package/docs/anchor.md +59 -0
- package/docs/ant-design-coverage.md +119 -0
- package/docs/app-provider.md +50 -0
- package/docs/app-rn-adoption.md +227 -0
- package/docs/architecture.md +320 -0
- package/docs/authoring-brief.md +87 -0
- package/docs/border-beam.md +66 -0
- package/docs/bottom-navigation.md +127 -0
- package/docs/breadcrumb.md +82 -0
- package/docs/calendar.md +154 -0
- package/docs/carousel.md +130 -0
- package/docs/cascader.md +93 -0
- package/docs/catalog-decision-status.md +306 -0
- package/docs/color-picker.md +72 -0
- package/docs/command-palette.md +116 -0
- package/docs/confirm-popover.md +93 -0
- package/docs/consistency-audit.md +383 -0
- package/docs/consumer-release-gate.md +90 -0
- package/docs/content-state.md +156 -0
- package/docs/context-panel.md +88 -0
- package/docs/cross-platform-core-normalization.md +118 -0
- package/docs/data-table.md +79 -0
- package/docs/date-picker.md +84 -0
- package/docs/description-list.md +75 -0
- package/docs/design-system-provider.md +139 -0
- package/docs/dropdown.md +78 -0
- package/docs/expansion-roadmap.md +285 -0
- package/docs/file-picker.md +59 -0
- package/docs/floating-action-button.md +111 -0
- package/docs/form.md +134 -0
- package/docs/generated/component-maturity.md +103 -0
- package/docs/generated/renderer-evidence.json +5597 -0
- package/docs/generated/renderer-evidence.md +132 -0
- package/docs/generated/showcase-manifest.json +3732 -0
- package/docs/icon.md +22 -0
- package/docs/identity.md +121 -0
- package/docs/image.md +84 -0
- package/docs/implementation-0.5.md +85 -0
- package/docs/layout-primitives.md +107 -0
- package/docs/layout.md +83 -0
- package/docs/library-reference-decisions.md +123 -0
- package/docs/link.md +67 -0
- package/docs/load-more.md +31 -0
- package/docs/mentions.md +82 -0
- package/docs/migration-0.2.md +108 -0
- package/docs/migration-0.3.md +72 -0
- package/docs/migration-0.5.md +82 -0
- package/docs/migration-0.6.md +197 -0
- package/docs/notification.md +54 -0
- package/docs/number-field.md +82 -0
- package/docs/otp-field.md +103 -0
- package/docs/pagination.md +139 -0
- package/docs/password-field.md +98 -0
- package/docs/popover.md +124 -0
- package/docs/promotion-candidates.md +169 -0
- package/docs/qr-code.md +69 -0
- package/docs/rating.md +58 -0
- package/docs/responsive-grid.md +90 -0
- package/docs/result.md +96 -0
- package/docs/showcase.md +97 -0
- package/docs/side-panel.md +71 -0
- package/docs/slider.md +79 -0
- package/docs/splitter.md +55 -0
- package/docs/statistic.md +16 -0
- package/docs/steps.md +115 -0
- package/docs/tag.md +57 -0
- package/docs/time-picker.md +88 -0
- package/docs/timeline.md +146 -0
- package/docs/toast.md +127 -0
- package/docs/tooltip.md +58 -0
- package/docs/tour.md +136 -0
- package/docs/transfer-list.md +94 -0
- package/docs/tree-select.md +101 -0
- package/docs/tree.md +123 -0
- package/docs/upload-item.md +68 -0
- package/docs/utility.md +53 -0
- package/docs/virtual-list.md +70 -0
- package/docs/watermark.md +58 -0
- package/package.json +402 -0
package/docs/toast.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Toast contract
|
|
2
|
+
|
|
3
|
+
Toast는 사용자가 응답해야만 진행되는 modal UI가 아니라, 무시해도 안전한 짧은 알림입니다.
|
|
4
|
+
필수 선택·삭제 확인·시간 안에 응답해야 하는 작업은 `AlertDialog`를 사용합니다.
|
|
5
|
+
|
|
6
|
+
HJM은 Radix Primitives에서 provider/viewport anatomy, visible timer pause, focus·Escape·swipe
|
|
7
|
+
수명주기를 참고하고 React Aria/Spectrum에서 bounded queue, stable id close handle, 5초 최소
|
|
8
|
+
timeout과 actionable persistent 기본값을 참고했습니다. 외부 component나 prop 이름은 공개하지
|
|
9
|
+
않고 다음 세 계층으로 번역했습니다.
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
ToastDescriptor 제품이 만든 현지화 copy·stable id·action
|
|
13
|
+
↓
|
|
14
|
+
ToastSession queued → visible → closing → closed 순수 상태
|
|
15
|
+
↓
|
|
16
|
+
ToastStore bounded FIFO·dedupe update·visible slot·provider teardown
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Descriptor
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const result = store.publish({
|
|
23
|
+
id: "profile-save",
|
|
24
|
+
title: "저장했어요",
|
|
25
|
+
description: "프로필 변경 사항을 반영했습니다.",
|
|
26
|
+
tone: "success",
|
|
27
|
+
priority: "normal",
|
|
28
|
+
closeLabel: "알림 닫기",
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- `id`는 제품이 소유하는 trim된 stable string입니다. 같은 id를 다시 publish하면 기본적으로
|
|
33
|
+
기존 위치를 유지한 채 copy·tone·action을 갱신합니다.
|
|
34
|
+
- `description`과 icon-only close의 현지화된 `closeLabel`은 필수입니다.
|
|
35
|
+
- `tone`은 시각 의미이고 `priority`는 announcement 순서입니다. 위험 색이라고 자동으로
|
|
36
|
+
높은 priority가 되지 않습니다.
|
|
37
|
+
- 기본 announcement는 `title. description`이며 화면 copy보다 추가 문맥이 필요할 때만
|
|
38
|
+
`announcement`를 제공합니다.
|
|
39
|
+
- action은 무시해도 안전해야 합니다. label 자체로 동작이 명확하지 않으면
|
|
40
|
+
`accessibilityLabel`에 완전한 이름을 제공합니다.
|
|
41
|
+
- action이 있는 Toast는 기본 persistent입니다. 제품이 명시한 `durationMs`도 5000ms보다
|
|
42
|
+
짧아질 수 없으며, `null`은 명시적 persistent입니다.
|
|
43
|
+
|
|
44
|
+
`publish` 결과에는 stable id와 `visible | queued` 위치가 포함됩니다. 호출자는 반환된 함수
|
|
45
|
+
대신 `store.close(id)`를 사용해 더 이상 유효하지 않은 알림을 programmatic reason으로 닫습니다.
|
|
46
|
+
|
|
47
|
+
## Queue와 update
|
|
48
|
+
|
|
49
|
+
HJM의 “조용한 화면 위에 중요한 순간만 선명하게” 원칙에 따라 기본 store는 visible 1개와
|
|
50
|
+
pending 20개로 제한됩니다. visible slot은 exit가 완료될 때까지
|
|
51
|
+
유지하며, 비워진 slot에는 pending의 첫 항목을 FIFO로 올립니다. pending capacity가 가득 차면
|
|
52
|
+
기본 `discard-oldest`가 가장 오래 기다린 항목을 `queue-overflow`로 정산하고 최신 정보를
|
|
53
|
+
받습니다. 감사·거래처럼 오래된 알림 보존이 필요하면 renderer가 `discard-newest`를 선택합니다.
|
|
54
|
+
|
|
55
|
+
stable id 중복 정책은 다음 두 축을 분리합니다.
|
|
56
|
+
|
|
57
|
+
- `duplicatePolicy: update | ignore`: 같은 id의 내용을 갱신할지 무시할지
|
|
58
|
+
- `timerUpdatePolicy: preserve | restart`: 이미 visible인 수명을 유지할지 새로 시작할지
|
|
59
|
+
|
|
60
|
+
기본 `update + preserve`는 진행 상태를 같은 자리에서 바꾸되 반복 publish로 알림이 화면에
|
|
61
|
+
무한히 남지 않게 합니다. queued 항목은 아직 시간이 흐르지 않았으므로 update 후에도 전체
|
|
62
|
+
duration을 갖습니다. `closing` id는 exit가 끝날 때까지 새 publish를 무시해 한 visual instance와
|
|
63
|
+
두 lifecycle이 겹치지 않게 합니다.
|
|
64
|
+
|
|
65
|
+
## 순수 timer와 pause
|
|
66
|
+
|
|
67
|
+
코어에는 `setTimeout`, `Date`, DOM, React 또는 React Native dependency가 없습니다. renderer가
|
|
68
|
+
자기 monotonic clock으로 `advanceTime(elapsedMs)`를 호출합니다.
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
queued(waiting, 5000) ─ show ─→ visible(running, 5000)
|
|
72
|
+
├─ pointer/focus/window/gesture → paused
|
|
73
|
+
└─ elapsed=5000 → closing(timeout)
|
|
74
|
+
└─ exit complete → closed
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- pending queue에서 기다린 시간은 duration에 포함하지 않습니다.
|
|
78
|
+
- pointer hover, keyboard focus, 앱/window 비활성화, swipe gesture pause reason은 Set으로
|
|
79
|
+
누적합니다. 모든 reason이 resume된 뒤에만 timer가 다시 흐릅니다.
|
|
80
|
+
- `pauseAll("window")` 상태에서 새 항목이 visible로 승격돼도 paused 상태를 상속합니다.
|
|
81
|
+
- renderer는 Reduce Motion으로 exit를 즉시 처리해도 반드시 `completeExit(id)`를 한 번 호출합니다.
|
|
82
|
+
|
|
83
|
+
## exact-once와 dismiss reason
|
|
84
|
+
|
|
85
|
+
`invokeAction`은 한 revision에서 한 번만 callback을 실행합니다. 기본 action은 Toast를
|
|
86
|
+
`action` reason으로 닫으며, `dismissOnAction: false`면 알림은 남지만 같은 action은 다시
|
|
87
|
+
실행되지 않습니다. stable id update는 새 revision이므로 새 action을 한 번 실행할 수 있습니다.
|
|
88
|
+
|
|
89
|
+
`timeout | action | close-action | escape | swipe | programmatic | queue-overflow | interrupted`
|
|
90
|
+
reason 중 하나만 최종 `onDismiss`로 전달됩니다. visible Toast는 exit 완료 시 callback을
|
|
91
|
+
정산하고 queued overflow와 provider teardown은 visual exit 없이 즉시 정산합니다. 중복 action,
|
|
92
|
+
close, exit complete와 두 번째 provider dispose는 아무 상태도 바꾸지 않습니다.
|
|
93
|
+
|
|
94
|
+
## Renderer acceptance
|
|
95
|
+
|
|
96
|
+
Web renderer:
|
|
97
|
+
|
|
98
|
+
- viewport를 문서 root 근처에 하나만 두고 recipe placement와 logical start/end를 사용합니다.
|
|
99
|
+
- 기본은 화면 아래 중앙 `bottom`이며, 위 중앙 알림 배너는 `top`을 사용합니다. 모서리 배치는
|
|
100
|
+
`top-start | top-end | bottom-start | bottom-end`처럼 logical 방향으로만 지정합니다.
|
|
101
|
+
- `normal`은 polite status, `high`는 assertive alert 의미로 번역하되 visible root와 announcer가
|
|
102
|
+
같은 문장을 중복 발표하지 않게 합니다.
|
|
103
|
+
- `F8`은 현지화된 이름을 가진 viewport로 focus를 옮기며, 제품 도움말에서 이 hotkey를
|
|
104
|
+
발견할 수 있게 합니다. 마지막 Toast가 닫히면 이전 focus를 복원합니다. focus가 Toast
|
|
105
|
+
action/close에 들어오면 timer를 pause합니다.
|
|
106
|
+
- Escape는 현재 focus가 있는 Toast id만 닫습니다. 페이지의 다른 Toast를 함께 닫지 않습니다.
|
|
107
|
+
- hover, focus, document visibility, swipe 동안 대응 pause reason을 연결합니다.
|
|
108
|
+
|
|
109
|
+
Native renderer:
|
|
110
|
+
|
|
111
|
+
- safe-area inset을 recipe inset에 additive로 적용하고 visual root 전체를 하나의 접근성
|
|
112
|
+
요소로 병합하지 않습니다. announcement node와 action/close를 각각 독립 접근성 노드로
|
|
113
|
+
유지하고, 비상호작용 copy만 필요할 때 묶습니다.
|
|
114
|
+
- visible promotion 시 한 번만 announcement를 요청합니다. update announcement는 copy가
|
|
115
|
+
실제로 바뀌었을 때만 요청해 screen reader queue를 범람시키지 않습니다.
|
|
116
|
+
- swipe capability가 없는 플랫폼에서는 gesture를 노출하지 않습니다.
|
|
117
|
+
- app background/foreground와 accessibility focus를 `window`/`focus` pause reason에 연결합니다.
|
|
118
|
+
|
|
119
|
+
두 renderer 모두 tone icon과 `toneMark`를 렌더링해 색 없이 neutral/info/success/warning/danger를
|
|
120
|
+
구분하고, action·close는 44-unit target과 visible focus를 유지합니다. 실제 제품 fixture에서는
|
|
121
|
+
normal/high announcement, keyboard/screen reader, 200% zoom, Reduce Motion, 앱 background 복귀,
|
|
122
|
+
overflow와 update를 검증합니다.
|
|
123
|
+
|
|
124
|
+
공식 참고:
|
|
125
|
+
|
|
126
|
+
- [Radix Toast](https://www.radix-ui.com/primitives/docs/components/toast)
|
|
127
|
+
- [React Spectrum Toast](https://react-spectrum.adobe.com/Toast)
|
package/docs/tooltip.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Tooltip contract
|
|
2
|
+
|
|
3
|
+
`Tooltip`은 Web에서 이미 의미와 focus를 가진 interactive trigger에 짧은 보충 설명을
|
|
4
|
+
제공합니다. 중요한 정보, 오류, 행동, link를 Tooltip 안에 숨기지 않으며 Native에 억지로
|
|
5
|
+
동일한 hover UI를 만들지 않습니다. BurnTok Web renderer와 실제 알림 trigger에서 아래
|
|
6
|
+
수명주기·RTL·접근성 계약을 검증했으므로 catalog는 `web / beta`입니다.
|
|
7
|
+
|
|
8
|
+
## Public descriptor
|
|
9
|
+
|
|
10
|
+
- `content`는 현지화된 non-empty plain string입니다. `ReactNode`, HTML, button, link는
|
|
11
|
+
허용하지 않습니다.
|
|
12
|
+
- `placement`는 logical `top | bottom | start | end`, `align`은
|
|
13
|
+
`start | center | end`입니다. renderer가 collision 때문에 실제 side를 바꿀 수 있습니다.
|
|
14
|
+
- controlled/uncontrolled open state는 `open/defaultOpen/onOpenChange` discriminated union을
|
|
15
|
+
사용합니다. `onOpenChange` reason은 pointer, focus, leave, blur, Escape, trigger activation,
|
|
16
|
+
다른 Tooltip 열림을 구분합니다.
|
|
17
|
+
- trigger는 단일 interactive element입니다. Tooltip이 role, tabIndex, accessible name을
|
|
18
|
+
발명하지 않고 기존 ref/event/`aria-describedby`를 합성합니다.
|
|
19
|
+
|
|
20
|
+
## Timing and provider
|
|
21
|
+
|
|
22
|
+
- keyboard focus는 즉시 열고 pointer는 500ms 뒤 엽니다. touch hover는 무시합니다.
|
|
23
|
+
- 최근 Tooltip이 닫힌 뒤 300ms 동안 sibling Tooltip은 즉시 열립니다.
|
|
24
|
+
- Provider 안에서는 한 번에 하나만 보입니다. controlled owner가 close 요청을 거부해도 두
|
|
25
|
+
surface를 동시에 노출하지 않습니다.
|
|
26
|
+
- trigger와 Tooltip surface 중 하나라도 hover된 동안 유지합니다. 둘 사이를 대각선으로
|
|
27
|
+
이동하는 pointer corridor도 일시적인 leave로 닫지 않습니다.
|
|
28
|
+
- Escape는 닫고 trigger focus를 유지합니다. 같은 hover/focus 자극이 계속 남아 있어도
|
|
29
|
+
해당 입력이 한 번 완전히 해제되기 전에는 다시 열지 않습니다.
|
|
30
|
+
- trigger activation은 Tooltip만 닫고 원래 click을 취소하지 않습니다.
|
|
31
|
+
|
|
32
|
+
## Accessibility
|
|
33
|
+
|
|
34
|
+
- surface는 `role="tooltip"`, trigger는 surface ID를 기존 설명 ID와 함께
|
|
35
|
+
`aria-describedby`로 참조합니다.
|
|
36
|
+
- Tooltip은 focus를 받지 않고 tabbable descendant가 없습니다.
|
|
37
|
+
- 보충 설명만 제공하므로 trigger의 accessible name과 핵심 결과는 Tooltip 없이도
|
|
38
|
+
이해할 수 있어야 합니다.
|
|
39
|
+
- hover/focus로 나타난 내용은 dismissible, hoverable, persistent 조건을 모두 만족합니다.
|
|
40
|
+
|
|
41
|
+
## Positioning boundary
|
|
42
|
+
|
|
43
|
+
HJM은 preferred placement, arrow, spacing, collision padding, layer, motion만 소유합니다.
|
|
44
|
+
DOM 측정·portal·`visualViewport`·scroll ancestor·ResizeObserver·pointer grace polygon은 Web
|
|
45
|
+
renderer의 비공개 AnchoredOverlay가 소유합니다. 이 내부 도구는 role, focus, dismiss, content
|
|
46
|
+
anatomy를 알지 못하며 catalog의 public Popover로 노출하지 않습니다.
|
|
47
|
+
|
|
48
|
+
초기 측정 전 surface는 숨기고 detached anchor는 렌더링하지 않습니다. logical start/end는
|
|
49
|
+
RTL에서 변환하고 preferred side가 부족하면 opposite side로 flip한 뒤 cross-axis를 viewport
|
|
50
|
+
안으로 shift합니다. Reduce Motion에서는 transform 없이 exit를 정확히 한 번 완료합니다.
|
|
51
|
+
|
|
52
|
+
## 첫 제품 검증
|
|
53
|
+
|
|
54
|
+
첫 적용은 BurnTok Web `NotificationBell`입니다. 브라우저 `title`을 제거하고 기존
|
|
55
|
+
`AppIconButton`을 trigger로 유지해 hover/focus/Escape, controlled owner 거부, sibling
|
|
56
|
+
skip-delay/FIFO, detached anchor, 우상단 viewport flip·shift, trigger-local 및 runtime RTL을
|
|
57
|
+
검증했습니다. rename hint는 클릭 가능한 span을 실제 button으로 고친 뒤 두 번째 slice로
|
|
58
|
+
옮깁니다. `iframe title`처럼 접근성 이름인 속성은 Tooltip으로 바꾸지 않습니다.
|
package/docs/tour.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Tour contract
|
|
2
|
+
|
|
3
|
+
## 문제
|
|
4
|
+
|
|
5
|
+
새 화면·새 기능을 처음 마주친 사용자에게 화면의 여러 요소를 순서대로 짚어가며
|
|
6
|
+
"이건 이렇게 씁니다"를 설명한다. antd `Tour`가 이 문제를 커버한다.
|
|
7
|
+
|
|
8
|
+
## 대체재가 없다 (판정)
|
|
9
|
+
|
|
10
|
+
이 담당분의 다른 둘(FloatingActionButton, ConfirmPopover)과 달리, Tour가 푸는
|
|
11
|
+
문제는 이 저장소의 다른 어떤 컴포넌트로도 이미 완결돼 있지 않다. Popover는 클릭
|
|
12
|
+
한 번으로 여는 임의 콘텐츠이지 순서가 있는 다단계 설명이 아니고, Steps는 사용자가
|
|
13
|
+
이미 진행 중인 자기 자신의 흐름(온보딩 마법사)을 보여줄 뿐 화면 요소를 짚어주지
|
|
14
|
+
않으며, Sheet/Dialog는 한 번에 하나의 표면이지 화면 곳곳을 옮겨 다니지 않는다.
|
|
15
|
+
그래서 `docs/notification.md`·`docs/dropdown.md`·`docs/virtual-list.md`와 달리
|
|
16
|
+
이 문서는 "만들지 않는다"가 아니라 실제 계약을 연다.
|
|
17
|
+
|
|
18
|
+
## 일반화한 계약
|
|
19
|
+
|
|
20
|
+
### 앵커는 불투명한 식별자다 — DOM도 RN 노드도 모른다
|
|
21
|
+
|
|
22
|
+
`TourStepDescriptor.anchorId`는 제품이 이미 쓰고 있는 문자열 키일 뿐이다. 실제
|
|
23
|
+
요소를 찾아 측정하고, 화면 안으로 스크롤하고, 하이라이트/컷아웃을 그리는 일은
|
|
24
|
+
전부 렌더러의 몫이다. 이 모듈은 ref도 좌표도 받지 않는다 — Icon이 third-party
|
|
25
|
+
아이콘 컴포넌트 대신 semantic name만 받는 것과 같은 경계, Tooltip/Popover가 DOM
|
|
26
|
+
측정·portal·flip/shift를 비공개 `AnchoredOverlay`에 위임하는 것과 같은 경계다.
|
|
27
|
+
|
|
28
|
+
`anchorId`는 step끼리 겹칠 수 있다 — 검증기는 step `id`의 유일성만 강제하고
|
|
29
|
+
`anchorId` 중복은 허용한다. "여기를 탭하세요" 다음 "이제 꾹 눌러 순서를
|
|
30
|
+
바꾸세요"처럼 같은 요소를 두 단계에 걸쳐 설명하는 것은 유효한 사용이다. 이건
|
|
31
|
+
브리프가 요구한 "validator가 잘못 잡는 입력으로 먼저 시험하라"를 그대로 따른
|
|
32
|
+
결과다 — 처음에는 유일성을 강제할 뻔했지만, 그러면 이 legitimate한 두 번째
|
|
33
|
+
사용을 막게 된다.
|
|
34
|
+
|
|
35
|
+
### 단일 커서 — Steps와 같은 원리, 다른 이유로 다른 anatomy
|
|
36
|
+
|
|
37
|
+
`TourDescriptor.currentStepId` 하나가 진행 상태의 유일한 근거다. per-step 상태
|
|
38
|
+
배열을 받지 않는 이유는 Steps와 같다 — "두 단계가 동시에 current"인 상태를 애초에
|
|
39
|
+
표현 불가능하게 만든다.
|
|
40
|
+
|
|
41
|
+
**그러나 Tour는 `stepsRecipe`를 재사용하지 않는다.** `Steps`의 anatomy(마커 +
|
|
42
|
+
커넥터로 이어진 체인)는 모든 단계가 **한 화면에 동시에, 같은 트랙 위에** 보인다는
|
|
43
|
+
전제 위에 있다 — 그래서 인접 마커를 잇는 선(`connector`)이 의미를 가진다. Tour는
|
|
44
|
+
정반대다: 한 번에 한 단계만 보이고, 그 단계는 매번 화면의 **다른, 서로 인접하지
|
|
45
|
+
않은** 요소 옆에 뜬다. 두 앵커 사이에 커넥터를 그리면 아무것도 잇지 않는 선이
|
|
46
|
+
된다 — `docs/steps.md`가 "이 세 축을 억지로 채우면 유령 계약이 된다"고 말한 것과
|
|
47
|
+
같은 함정이다. 그래서 이 모듈은 Steps의 시각 recipe를 가져오지 않고, "단일
|
|
48
|
+
커서로 유도한다"는 **원리**만 독립적으로 다시 구현한다(`resolveTourAdvance`) —
|
|
49
|
+
브리프가 요구한 모듈 자급자족 원칙과도 맞는다(다른 저작 모듈들도 서로를 import하지
|
|
50
|
+
않는다).
|
|
51
|
+
|
|
52
|
+
대신 Tour의 카드는 Popover/Tooltip과 같은 문제를 공유한다 — "트리거(여기서는
|
|
53
|
+
앵커)에 붙어 뜨는 표면". 그래서 `tourRecipe.card`는 `floatingSurfaceContract`를
|
|
54
|
+
그대로 재사용한다.
|
|
55
|
+
|
|
56
|
+
### 포커스와 낭독 — "이 부분을 보세요"가 성립하지 않는 사용자에게
|
|
57
|
+
|
|
58
|
+
시각적 스포트라이트는 화면을 볼 수 있는 사용자에게만 의미가 있다. 스크린 리더
|
|
59
|
+
사용자에게 이 컴포넌트가 실제로 하는 일은 **카드에 적힌 문장**이지 앵커를
|
|
60
|
+
가리키는 화살표가 아니다. 그래서 계약은 두 가지를 명시한다.
|
|
61
|
+
|
|
62
|
+
1. 단계가 바뀔 때 포커스/접근성 포커스는 **앵커가 아니라 카드**로 이동한다.
|
|
63
|
+
화면을 볼 수 없는 사용자도 "무엇을 설명하는 중인지"를 알 유일한 방법이
|
|
64
|
+
카드이기 때문이다.
|
|
65
|
+
2. `composeAnnouncement`가 만든 문장이 그 카드의 접근 가능한 내용이다 — Steps의
|
|
66
|
+
`composeAccessibleName`과 같은 이유로(한국어 "5단계 중 2단계"는 영어 어순과
|
|
67
|
+
다르다) 제품이 조립하되, `resolveTourDescriptor`는 빈 문자열을 던진다.
|
|
68
|
+
|
|
69
|
+
배경은 inert 처리한다(Web `inert`/`aria-hidden`, Native
|
|
70
|
+
`importantForAccessibility="no-hide-descendants"`) — 그렇지 않으면 화면
|
|
71
|
+
낭독기가 아직 설명되지 않은 배경 요소로 사용자를 데려갈 수 있다.
|
|
72
|
+
|
|
73
|
+
### 탈출
|
|
74
|
+
|
|
75
|
+
Escape와 명시적 "건너뛰기" 액션은 어떤 단계에서도 항상 동작한다 — 이건
|
|
76
|
+
설정값이 아니라 고정 규칙이다(`tourBehaviorDefaults`는 축이 하나뿐이다:
|
|
77
|
+
`outsideDismiss: false`). Popover처럼 `dismissible`/`escapeDismiss`를 따로
|
|
78
|
+
끌 수 있게 하지 않는다 — 브리프의 "언제든 그만둘 수 있어야 한다"는 요구가
|
|
79
|
+
협상 불가능하기 때문이다. 반대로 바깥 영역 클릭으로 조용히 끝나는 것은 허용하지
|
|
80
|
+
않는다(`TourCloseReason`에 `"outside"`가 없다) — 다단계 설명 도중 실수로 화면
|
|
81
|
+
바깥을 건드려 안내 전체가 사라지는 사고를 막는다.
|
|
82
|
+
|
|
83
|
+
그만둔 상태를 기억할지(다시 안 보여주기)는 **제품 몫**이다. 이 모듈은
|
|
84
|
+
`TourCloseReason`으로 "왜 끝났는지"만 보고하고, 그 결과를 어디에 저장할지는
|
|
85
|
+
갖지 않는다.
|
|
86
|
+
|
|
87
|
+
### 세션이 아니라 순수 함수로 — AlertDialog보다 Popover에 가깝다
|
|
88
|
+
|
|
89
|
+
AlertDialog는 `idle→busy→error/closing→closed`라는 상태를 가진 세션
|
|
90
|
+
(`createAlertDialogSession`)을 갖는다 — 되돌릴 수 없는 비동기 side effect를
|
|
91
|
+
정확히 한 번 실행하고 정산해야 하기 때문이다. Tour에는 그런 비동기 side effect가
|
|
92
|
+
없다 — 버튼을 누르면 다음 카드로 넘어가거나 닫힐 뿐이다. 그래서 이 모듈은
|
|
93
|
+
Popover처럼 순수 판정 함수(`resolveTourAdvance`, `validateTourOpenState`)만
|
|
94
|
+
공개하고 상태 객체를 만들지 않는다. "정확히 한 번 정산"이 필요한 건 여기서는
|
|
95
|
+
평범한 콜백 호출 하나로 이미 충분하다 — 재시도할 실패한 side effect가 없기
|
|
96
|
+
때문이다.
|
|
97
|
+
|
|
98
|
+
## HJM 기본값
|
|
99
|
+
|
|
100
|
+
- `outsideDismiss: false` 고정.
|
|
101
|
+
- 카드는 `floatingSurfaceContract`, 배경은 `backdrop.modal`(`backdrop.veil`이
|
|
102
|
+
아니다) — Tour는 Dialog/Sheet처럼 배경 상호작용을 막으므로(`outsideDismiss:
|
|
103
|
+
false`), 배경이 계속 조작 가능한 곳에서 쓰는 옅은 `veil`이 아니라 실제로 막는
|
|
104
|
+
곳에서 쓰는 `modal` 톤을 쓴다(Sheet/Dialog/SidePanel/CommandPalette와 동일
|
|
105
|
+
선택).
|
|
106
|
+
- 단계 전환 모션은 `motionPreset.context`(320ms, Reduce Motion에서 opacity) —
|
|
107
|
+
서로 인접하지 않은 화면 부위 사이를 옮겨 다니는 "맥락 전환"이지, 같은 자리에서
|
|
108
|
+
일어나는 작은 상태 변화가 아니다.
|
|
109
|
+
|
|
110
|
+
## 플랫폼 번역
|
|
111
|
+
|
|
112
|
+
- Web: 카드는 포커스를 받을 수 있는 컨테이너(`tabIndex={-1}`)이고, 단계가 바뀔
|
|
113
|
+
때마다 `.focus()`한다. 배경은 `inert`. Escape는 항상 닫는다.
|
|
114
|
+
- Native: 단계 변경 시 카드로 접근성 포커스를 이동한다(플랫폼 focus API). 배경은
|
|
115
|
+
`importantForAccessibility="no-hide-descendants"`.
|
|
116
|
+
- Reduce Motion: 카드는 위치 이동 없는 opacity 교차로 나타나고, 앵커 사이를
|
|
117
|
+
이동하는 스포트라이트 애니메이션은 두지 않는다(즉시 다음 위치로 전환).
|
|
118
|
+
|
|
119
|
+
## 공개한 축 / 배제한 축
|
|
120
|
+
|
|
121
|
+
| 축 | 상태 |
|
|
122
|
+
| --- | --- |
|
|
123
|
+
| 단일 커서(`currentStepId`) | 공개 |
|
|
124
|
+
| `next`/`previous`/`skip`/완료(`complete`) | 공개 |
|
|
125
|
+
| `TourCloseReason`(skip/escape/complete/programmatic/interrupted) | 공개 |
|
|
126
|
+
| 앵커 측정·하이라이트 지오메트리 | **배제** — 렌더러 소유(위 "앵커는 불투명한 식별자다" 참고) |
|
|
127
|
+
| `outside` dismiss | **배제** — 실수로 안내가 끊기는 사고를 막는다 |
|
|
128
|
+
| 단계별 개별 status 배열 | **배제** — Steps와 같은 이유로 유효하지 않은 조합(두 단계가 동시에 current)을 표현 불가능하게 한다 |
|
|
129
|
+
| Steps의 시각 recipe(마커/커넥터) 재사용 | **배제** — 위 "단일 커서" 절의 판정 참고. 카드 anatomy는 대신 Popover/Tooltip의 `floatingSurfaceContract`를 재사용한다 |
|
|
130
|
+
| "다시 보지 않기" 영속화 | **배제** — 제품이 `TourCloseReason`을 받아 직접 저장할 몫 |
|
|
131
|
+
| 비동기 busy/error 상태 | **배제** — Tour에는 되돌릴 수 없는 side effect가 없다. AlertDialog의 세션 패턴을 가져올 이유가 없다 |
|
|
132
|
+
|
|
133
|
+
## 검증 화면
|
|
134
|
+
|
|
135
|
+
아직 없음. `planned → beta` 승격은 실제 제품 vertical slice 이후 리드가
|
|
136
|
+
진행한다. 유력 후보: Yajalal 홈 화면 첫 진입 안내(검색 → 즐겨찾기 → 알림).
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# TransferList contract
|
|
2
|
+
|
|
3
|
+
## 문제
|
|
4
|
+
|
|
5
|
+
두 목록 사이로 항목을 옮긴다(권한 있는 사용자 ↔ 권한 없는 사용자, 후보 ↔ 확정 명단).
|
|
6
|
+
Ant Design `Transfer`와 `adapted` crosswalk를 따른다(HJM은 `checkbox`/`checkAll` 같은
|
|
7
|
+
antd 전용 prop 이름을 옮기지 않고 Collection 기본 계약으로 다시 번역한다).
|
|
8
|
+
|
|
9
|
+
## 일반화한 계약
|
|
10
|
+
|
|
11
|
+
### 하나의 항목 집합, membership으로 나뉜 두 패널
|
|
12
|
+
|
|
13
|
+
두 패널을 서로 다른 배열로 관리하면 이동 시 두 배열이 어긋날 수 있다. 대신 CheckboxGroup의
|
|
14
|
+
`ReadonlySet<Id>` 관례를 그대로 따라 **전체 `items` 하나 + `targetKeys: ReadonlySet<Id>`
|
|
15
|
+
하나**로 값 하나를 유지한다 — target 패널은 `targetKeys`에 있는 항목, source 패널은 나머지
|
|
16
|
+
전부다. `resolveTransferListPanels`가 이 분리를 계산하고, 매번 `items`의 상대 순서를
|
|
17
|
+
보존한다(패널을 옮겨도 원래 목록 순서가 흔들리지 않는다).
|
|
18
|
+
|
|
19
|
+
### 각 패널의 선택은 값이 아니라 이동 전 임시 상태
|
|
20
|
+
|
|
21
|
+
두 패널 각각 다중 선택(체크박스)을 갖는다. 이 선택은 `targetKeys`처럼 커밋된 값이 아니라
|
|
22
|
+
"다음에 무엇을 옮길지"를 가리키는 임시 상태이므로 별도 `TransferListSelection`(패널별
|
|
23
|
+
`ReadonlySet<Id>`)으로 분리했다 — Select의 `selectedKey`와 Combobox의 `inputValue`가
|
|
24
|
+
독립 축인 것과 같은 이유다.
|
|
25
|
+
|
|
26
|
+
### 접근성 계약이 본체다 — 이동은 마우스 전용이 될 수 없다
|
|
27
|
+
|
|
28
|
+
이 컴포넌트가 실제로 계약해야 하는 것은 시각 레이아웃이 아니라 "옮기는 동작이 키보드로
|
|
29
|
+
완전히 가능한가"다.
|
|
30
|
+
|
|
31
|
+
- 각 행은 체크박스 토글이 자신의 유일한 동작이다(Collection 기본 계약의 "interactive
|
|
32
|
+
item 안에 또 다른 button/link 금지"를 지킨다 — 행 안에 별도 이동 버튼을 넣지 않는다).
|
|
33
|
+
- 패널 사이에 공유 이동 버튼 두 개(→/←)가 있고, 각 버튼은 해당 패널에 선택된 항목이
|
|
34
|
+
있을 때만 활성화된다. `moveTransferListSelection`은 버튼이 조작하는 다중 선택 이동과
|
|
35
|
+
"선택 하나만 담아 호출"하는 단일 항목 이동을 같은 함수로 표현한다 — 별도 API를 늘리지
|
|
36
|
+
않는다.
|
|
37
|
+
- **이동 후 포커스가 어디로 가는지를 이 계약이 직접 정의한다.** 기존 이 저장소 어디에도
|
|
38
|
+
"목록에서 항목이 사라진 뒤 포커스가 어디로 가는가"를 정의한 선례가 없어(Tag 제거, Tree
|
|
39
|
+
노드 제거 계약 모두 이 질문을 다루지 않는다) 이 모듈이 그 첫 자리다.
|
|
40
|
+
`resolveTransferListFocusAfterMove`는 제거된 행의 자리로 밀려 올라온 항목에 포커스를
|
|
41
|
+
주고(제거된 인덱스와 같은 자리, 없으면 마지막 항목), 패널이 통째로 비면 `null`을
|
|
42
|
+
반환해 렌더러가 빈 상태 메시지나 이동 버튼으로 포커스를 넘기게 한다 — 포커스가
|
|
43
|
+
document body로 사라지는 경우를 만들지 않는다.
|
|
44
|
+
- **무엇이 낭독되는지도 이 계약이 정의한다.** `moveTransferListSelection`은 실제로 옮겨진
|
|
45
|
+
id 목록(`movedIds`, 원래 패널 순서대로)을 반환한다. HJM은 문자열을 조립하지 않는다
|
|
46
|
+
(Statistic 원칙) — 제품이 이 목록으로 "2명이 선택 명단으로 이동했습니다" 같은 문장을
|
|
47
|
+
만들어 live region에 알린다.
|
|
48
|
+
|
|
49
|
+
### 비활성 항목은 선택도, 이동도 되지 않는다
|
|
50
|
+
|
|
51
|
+
`toggleTransferListSelection`은 `toggleCheckboxSelection`을 그대로 재사용해 비활성 항목을
|
|
52
|
+
선택 자체에서 막는다. `moveTransferListSelection`은 그래도 선택 집합에 비활성 id가 있는
|
|
53
|
+
경우(예: 제품이 `defaultSelectedKeys`로 직접 넣은 경우)를 던지지 않고 건너뛴다 —
|
|
54
|
+
`toggleTreeCheckedSelection`의 비활성 스킵과 같은 관례다. 옮겨지지 않은 항목은 선택
|
|
55
|
+
상태에 그대로 남는다 — 이동 함수는 "옮겨진 id만" 선택에서 지운다.
|
|
56
|
+
|
|
57
|
+
### 패널 전체 선택은 DataTable의 tri-state를 그대로 일반화한다
|
|
58
|
+
|
|
59
|
+
`resolveTransferListSelectAllState`는 `resolveDataTableSelectAllState`가 테이블 헤더에
|
|
60
|
+
쓰는 것과 똑같은 규칙(비활성 행은 분모와 카운트 모두에서 제외)을 패널 하나에 적용한다.
|
|
61
|
+
`toggleTransferListSelectAll`은 `getCheckboxNextState`의 "mixed는 체크로 처리한다"는
|
|
62
|
+
기존 관례를 그대로 쓴다.
|
|
63
|
+
|
|
64
|
+
## 뺀 것
|
|
65
|
+
|
|
66
|
+
- **검색.** 각 패널 안에서 후보를 줄이는 문제는 이미 `SearchField`가 있다. 두 컴포넌트를
|
|
67
|
+
조합하는 것(검색 입력이 어떤 패널의 어떤 필터 상태를 갖는지)은 제품의 몫이지 이
|
|
68
|
+
계약이 아니다 — 검색 상태를 여기 넣으면 Combobox의 `queryValue`/`filtering`과 같은
|
|
69
|
+
문제를 또 다른 이름으로 계약하게 된다.
|
|
70
|
+
- **페이지네이션.** 같은 이유로 `Pagination`이 이미 있다. 긴 패널에서는 페이지네이션
|
|
71
|
+
대신 `LoadMore`를 조합하는 쪽이 로드맵의 다른 adaptive 컴포넌트(Select/Combobox)와
|
|
72
|
+
일관된다 — 이 판단도 이 계약에 넣지 않는다.
|
|
73
|
+
|
|
74
|
+
## HJM 기본값
|
|
75
|
+
|
|
76
|
+
- 행 anatomy: `collectionItemContract`를 그대로 쓴다(Menu/Select/Tree 행과 같은 44-unit
|
|
77
|
+
타겟, hover/selected 배경, focus indicator).
|
|
78
|
+
- 패널 프레임: 배경/보더/radius만 있는 가벼운 컨테이너(`transferListRecipe.panel`) —
|
|
79
|
+
`floatingSurfaceContract`의 shadow는 쓰지 않는다. 패널은 떠 있는 표면이 아니라 화면에
|
|
80
|
+
고정된 두 목록이다.
|
|
81
|
+
|
|
82
|
+
## 플랫폼 번역
|
|
83
|
+
|
|
84
|
+
Web/Native 모두 두 리스트박스 + 공유 이동 버튼이라는 같은 anatomy를 쓴다. Web은
|
|
85
|
+
`role="listbox"`에 `aria-multiselectable`, roving tabindex로 화살표 키 이동과 Space
|
|
86
|
+
토글을 구현한다. Native는 리스트 각각을 `accessibilityRole="list"`, 행을
|
|
87
|
+
`accessibilityState={{checked}}`로 노출하고 이동 버튼은 일반 버튼이다 — 스와이프
|
|
88
|
+
전용 이동 제스처는 열지 않는다(브리프의 "옮기는 동작이 마우스로만 가능하면 안 된다"는
|
|
89
|
+
Native의 "터치 전용"에도 같게 적용되므로, 버튼이 항상 스와이프의 대체 경로로 존재해야
|
|
90
|
+
한다).
|
|
91
|
+
|
|
92
|
+
## 검증 화면
|
|
93
|
+
|
|
94
|
+
아직 없음. `planned → beta` 승격은 실제 제품 vertical slice 이후 리드가 진행한다.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# TreeSelect — 판정 검증과 빠진 한 조각
|
|
2
|
+
|
|
3
|
+
## 문제
|
|
4
|
+
|
|
5
|
+
계층 데이터에서 하나 또는 여럿을 골라 Select처럼 트리거+값으로 커밋한다. antd
|
|
6
|
+
`TreeSelect`와 `direct` crosswalk를 따른다.
|
|
7
|
+
|
|
8
|
+
## 기존 판정 검증
|
|
9
|
+
|
|
10
|
+
`docs/tree.md`의 "TreeSelect 판정"은 이미 다음과 같이 결론지었다: TreeSelect는
|
|
11
|
+
Select의 표면(트리거+뜨는 목록, committed value) 위에 Tree의 collection(깊이/형제
|
|
12
|
+
발화, 화살표 판정, `expandedKeys` 재조정)을 얹은 것이지 세 번째 primitive가 아니다.
|
|
13
|
+
|
|
14
|
+
이 판정을 검증했다. **단일 선택(single-select) TreeSelect에는 정확히 맞다** — 노드
|
|
15
|
+
하나(리프든 카테고리든)를 값으로 고르는 데는 집계할 것이 없다. `SelectSelection`의
|
|
16
|
+
`selectedKey: Id | null`과 Tree의 평평한 항목 목록만으로 완결된다. 새 코드가 필요
|
|
17
|
+
없다.
|
|
18
|
+
|
|
19
|
+
**다중 선택(multiple-select, 체크박스) TreeSelect에는 판정이 불완전했다.** 어느
|
|
20
|
+
파일도 "부모 체크박스가 자식 중 일부만 체크됐을 때 세 번째 집계 상태(mixed)를
|
|
21
|
+
보여줘야 한다"는 문제를 풀지 않는다. 이것은 새 선택 모델이 아니라
|
|
22
|
+
`resolveDataTableSelectAllState`(`src/data-table.ts`)가 이미 한 단계(헤더 ↔ 행)에서
|
|
23
|
+
푼 문제를 재귀 깊이(부모 ↔ 모든 자손 리프)로 일반화한 것 — 헤더 체크박스가 이미
|
|
24
|
+
쓰는 그 `CheckboxState`(`boolean | "mixed"`)를 그대로 재사용한다. 이 문서와
|
|
25
|
+
`src/tree-select.ts`는 **그 한 조각만** 채운다.
|
|
26
|
+
|
|
27
|
+
## 일반화한 계약
|
|
28
|
+
|
|
29
|
+
### 체크된 키는 리프만 저장한다
|
|
30
|
+
|
|
31
|
+
`TreeCheckedKeys<Id> = ReadonlySet<Id>`는 **리프 노드 id만** 담을 수 있다.
|
|
32
|
+
`validateTreeCheckedSelection`은 부모 id나 존재하지 않는 id가 섞이면 던진다. 부모
|
|
33
|
+
자신의 id를 집합에 넣을 수 있게 하면 "이 부모 자체를 하나의 값으로 고른 것"과
|
|
34
|
+
"이 부모의 모든 자식을 골랐다는 표시"가 구분되지 않는 모호함이 생긴다 — antd가
|
|
35
|
+
`checkStrictly`라는 별도 옵션으로 풀어야 했던 바로 그 모호함이다. 측정된 요구 없이
|
|
36
|
+
그 모호함을 추측해서 풀지 않고, 애초에 표현 불가능하게 만들었다: 부모의 체크
|
|
37
|
+
상태는 **항상 유도**되고 **결코 저장되지 않는다** — DataTable 헤더가 `selectedKeys`
|
|
38
|
+
안의 한 행이 되는 일이 없는 것과 같은 이유다.
|
|
39
|
+
|
|
40
|
+
### 집계는 활성 자손 리프 커버리지로 계산한다
|
|
41
|
+
|
|
42
|
+
`resolveTreeCheckedStates`는 트리 전체를 한 번 훑어 모든 노드의 tri-state를 계산한다
|
|
43
|
+
(`resolveTreeDescriptor`가 depth/position을 한 번에 계산하는 것과 같은 모양). 각
|
|
44
|
+
노드는 부모로 올라가며 `{ enabled 자손 리프 수, 그중 체크된 수 }`를 합산하고,
|
|
45
|
+
`enabled === 0` 이거나 `checked === 0`이면 `false`, `checked === enabled`면 `true`,
|
|
46
|
+
그 사이면 `"mixed"`다.
|
|
47
|
+
|
|
48
|
+
**비활성 리프는 분모와 분자 모두에서 제외된다** —
|
|
49
|
+
`resolveDataTableSelectAllState`의 "Disabled rows are excluded from both the
|
|
50
|
+
denominator and the count"를 그대로 재귀에 일반화했다. 이게 없으면 비활성이면서
|
|
51
|
+
체크 안 된 리프 하나가 있는 순간 그 위의 모든 조상이 영원히 "mixed"에 갇힌다 — 나머지
|
|
52
|
+
형제가 전부 체크돼도 부모가 "전부 체크됨"으로 올라가지 못하는 오류다(테스트로
|
|
53
|
+
잠갔다). 비활성 리프 자신의 표시 상태(`checkedKeys.has(id)`)는 그대로 유지한다 —
|
|
54
|
+
비활성 체크박스도 시각적으로는 체크/미체크를 보여줄 수 있다.
|
|
55
|
+
|
|
56
|
+
### 부모를 고르면 자식이 함께 고르진다
|
|
57
|
+
|
|
58
|
+
`toggleTreeCheckedSelection`은 어떤 노드(리프든 부모든)를 토글하면 그 노드의 **활성
|
|
59
|
+
자손 리프 전체**를 "완전히 체크됨"의 반대로 맞춘다 — `getCheckboxNextState`가
|
|
60
|
+
이미 쓰는 "mixed는 체크로 취급" 관례와 같다. 비활성 리프는 cascade에서 건너뛴다
|
|
61
|
+
(`toggleCheckboxSelection`의 disabled guard와 같은 모양). 리프 자신을 토글하면
|
|
62
|
+
평범한 Checkbox 토글과 동일하게 동작한다. 대상 리프 자체가 비활성이면
|
|
63
|
+
`descendantLeaves`가 그 리프 하나뿐이고 cascade 루프가 건너뛰므로, 별도 분기 없이
|
|
64
|
+
자연스럽게 no-op이 된다(`toggleCheckboxSelection`이 비활성 항목에서 no-op하는 것과
|
|
65
|
+
같은 결과).
|
|
66
|
+
|
|
67
|
+
### 뺀 것 — cascade 정책 자체는 열지 않는다
|
|
68
|
+
|
|
69
|
+
antd `TreeSelect`의 `checkStrictly`(부모/자식을 독립적으로 체크할지, cascade할지
|
|
70
|
+
선택하는 옵션)는 만들지 않는다. 리프만 저장한다는 결정 자체가 "부모는 항상 유도"를
|
|
71
|
+
강제하므로 애초에 그 모호함이 성립하지 않는다 — 옵션을 열 필요가 없다. 만약 실제
|
|
72
|
+
제품이 "이 카테고리 자체를 자식과 무관하게 값으로 저장"해야 하는 화면을 요구하면
|
|
73
|
+
그건 이 계약 밖의 요구이며, 그때 이 문서를 갱신한다.
|
|
74
|
+
|
|
75
|
+
"select all"/"clear all" 같은 편의 UI도 만들지 않았다 — `toggleTreeCheckedSelection`을
|
|
76
|
+
루트 노드에 호출하면 이미 그 결과가 나온다(cascading to the root 테스트로 확인),
|
|
77
|
+
별도 API가 필요 없다.
|
|
78
|
+
|
|
79
|
+
## HJM 기본값
|
|
80
|
+
|
|
81
|
+
새 recipe를 만들지 않는다. 표면은 기존 셋을 그대로 합성한다:
|
|
82
|
+
|
|
83
|
+
- 트리거/필드 chrome — 기존 `selectRecipe`
|
|
84
|
+
- 팝업/시트 안의 각 행 — 기존 `treeRecipe.node`/`.toggle`/`.indentPerLevel`
|
|
85
|
+
(`src/tree.ts`, 다른 저작자가 이미 작성)
|
|
86
|
+
- 행의 체크 마크 — 기존 Checkbox recipe, 이 모듈이 유도한 `CheckboxState`로 구동
|
|
87
|
+
|
|
88
|
+
넷째 recipe를 선언하면 앞의 셋이 이미 가진 토큰을 다시 이름 붙이는 것에 불과하다.
|
|
89
|
+
|
|
90
|
+
## 플랫폼 번역
|
|
91
|
+
|
|
92
|
+
새 behaviorRegistry 항목도 만들지 않는다. open/dismiss reason, role, keyboard 모델은
|
|
93
|
+
`behaviorRegistry.select`를 그대로 쓴다. Web renderer는 각 행의 체크박스에
|
|
94
|
+
`aria-checked="mixed"`를 `resolveTreeCheckedStates`의 결과로 채우고, 토글 시
|
|
95
|
+
`toggleTreeCheckedSelection`을 호출해 다음 `checkedKeys`를 얻는다. Tree의 깊이/형제
|
|
96
|
+
발화, 화살표 키 판정은 `src/tree.ts`가 이미 정의한 그대로다 — TreeSelect가 다시
|
|
97
|
+
정의하지 않는다.
|
|
98
|
+
|
|
99
|
+
## 검증 화면
|
|
100
|
+
|
|
101
|
+
아직 없음. `planned → beta` 승격은 실제 제품 vertical slice 이후 리드가 진행한다.
|
package/docs/tree.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Tree contract
|
|
2
|
+
|
|
3
|
+
## 문제
|
|
4
|
+
|
|
5
|
+
계층으로 이루어진 데이터를 펼치고 접으며 탐색하고, 그 안에서 하나 또는 여럿을 고른다.
|
|
6
|
+
Ant Design `Tree`와 `direct` crosswalk를 따른다.
|
|
7
|
+
|
|
8
|
+
## Collection 기본 계약의 확장
|
|
9
|
+
|
|
10
|
+
로드맵의 「Collection 기본 계약」은 `section → item` 2단 고정 형태다
|
|
11
|
+
(`CollectionSource`, `src/collection.ts`). Tree는 임의 깊이가 본질이라 이 고정 2단
|
|
12
|
+
형태를 그대로 쓸 수 없다 — section은 애초에 선택 불가능한 라벨일 뿐인데, Tree의 모든
|
|
13
|
+
노드(자식이 있든 없든)는 선택 가능한 완전한 item이어야 한다. section을 "자식이 있는
|
|
14
|
+
노드"로 재해석하면 Select/Menu의 "section은 값이 아니다"라는 불변식이 깨진다.
|
|
15
|
+
|
|
16
|
+
그래서 무엇을 재사용하고 무엇을 새로 만들지 판정했다.
|
|
17
|
+
|
|
18
|
+
**재사용한 것.**
|
|
19
|
+
|
|
20
|
+
- **필드 자체.** `TreeNodeDescriptor`는 `CollectionItemDescriptor`에서 Menu 전용
|
|
21
|
+
`shortcut`/`tone`을 뺀 나머지(`id`/`label`/`textValue`/`description?`/`disabled?`)에
|
|
22
|
+
`children?`만 얹은 타입이다(`Omit<CollectionItemDescriptor<Id>, "shortcut" | "tone"> &
|
|
23
|
+
{ children?: ... }`). 새 필드 어휘를 만들지 않았다.
|
|
24
|
+
- **평탄화 뒤의 기본 검증.** `flattenTreeNodes`로 전체 트리를 깊이 우선으로 편 뒤,
|
|
25
|
+
`validateTreeNodes`가 그 결과를 그대로 `validateCollection({ items: flattened })`에
|
|
26
|
+
넘긴다. `TreeNodeDescriptor`가 `CollectionItemDescriptor`의 초집합이라 별도 변환 없이
|
|
27
|
+
통과하고, "라벨/textValue 비어있지 않음", "전체에서 id 유일함" 같은 규칙을 다시 쓰지
|
|
28
|
+
않는다(테스트로 확인: 서로 다른 깊이의 두 노드가 같은 id를 쓰면 이 재사용 경로가
|
|
29
|
+
던진다).
|
|
30
|
+
- **위/아래·Home/End·typeahead.** `getCollectionNavigationTarget`과
|
|
31
|
+
`getCollectionTypeaheadMatch`를 그대로 쓴다(`getVisibleTreeNavigationTarget`/
|
|
32
|
+
`getVisibleTreeTypeaheadMatch`는 얇은 래퍼일 뿐이다). 현재 보이는(접힌 하위트리를
|
|
33
|
+
제외한) 노드 배열을 `{ items }`로 넘기면 disabled skip, 경계에서 멈춤, typeahead
|
|
34
|
+
매칭까지 그대로 맞는다 — "보이는 노드만의 선형 목록"이라는 점에서 Menu의 항목
|
|
35
|
+
목록과 다르지 않기 때문이다.
|
|
36
|
+
- **선택 모드.** `TreeSelectionModel<Id>`는 `CollectionSelectionModel<Id>`를 그대로
|
|
37
|
+
가리킨다. `expansion-roadmap.md`가 Tree를 Menu/Select/DataTable과 함께 이 모델을
|
|
38
|
+
공유해야 하는 컴포넌트로 이미 명시하고 있다.
|
|
39
|
+
|
|
40
|
+
**새로 만든 것.** 깊이(`depth`)·형제 위치(`position`/`siblingCount`)·부모 연결
|
|
41
|
+
(`parentId`)·펼침 유도(`expanded`)·가시성(`visible`)을 만드는 `resolveTreeDescriptor`의
|
|
42
|
+
재귀 walk, 좌/우 화살표 판정(`getTreeArrowKeyIntent`/`getTreeArrowResult`), 빈 배열
|
|
43
|
+
`children` 거부, 그리고 재사용 가능한 값으로 정리해 남겨진 것 없이 삭제·복원하는
|
|
44
|
+
`reconcileTreeExpansion`(`reconcileCheckboxSelection`과 같은 모양). 이 넷은 antd
|
|
45
|
+
`Tree`에도, 이 저장소의 다른 어떤 컴포넌트에도 없는, Tree만의 문제다.
|
|
46
|
+
|
|
47
|
+
## 일반화한 계약
|
|
48
|
+
|
|
49
|
+
### 펼침 상태
|
|
50
|
+
|
|
51
|
+
`expandedKeys: ReadonlySet<Id>`(controlled) / `defaultExpandedKeys?`(uncontrolled) —
|
|
52
|
+
CheckboxGroup의 `ReadonlySet` 관례를 따른다. 순수 함수들(`resolveTreeDescriptor` 등)은
|
|
53
|
+
Carousel의 `currentKey`와 같은 이유로 이미 해석된 구체적 `ReadonlySet<Id>`만 받는다 —
|
|
54
|
+
controlled/uncontrolled 분기는 렌더러가 끝낸다.
|
|
55
|
+
|
|
56
|
+
### 선택
|
|
57
|
+
|
|
58
|
+
`TreeSelectionModel<Id>`(= `CollectionSelectionModel<Id>`)로 `none|single|multiple`을
|
|
59
|
+
그대로 받는다. 새 모델을 만들지 않았다.
|
|
60
|
+
|
|
61
|
+
### 비활성 노드는 선택만 막는다
|
|
62
|
+
|
|
63
|
+
`disabled`는 `CollectionItemDescriptor`처럼 선택 자격만 가린다. 펼침/접힘은 `disabled`와
|
|
64
|
+
무관하게 항상 가능하다 — 비활성 노드도 그 아래 무엇이 있는지는 볼 수 있어야 한다(선택
|
|
65
|
+
못 하는 것과 탐색 못 하는 것은 다른 문제다).
|
|
66
|
+
|
|
67
|
+
### 접근성: 깊이와 형제 위치를 낭독에 남긴다
|
|
68
|
+
|
|
69
|
+
Web `tree`/`treeitem`/`group` 시맨틱(다열 데이터가 필요해지면 렌더러가 `treegrid`로
|
|
70
|
+
바꿀 수 있지만, 이 계약이 보장하는 키보드/aria 축은 둘 다 같다) 위에 `resolveTreeDescriptor`가
|
|
71
|
+
각 노드에 `depth`(1-based), `position`/`siblingCount`(형제 안에서, 전체 편평 목록이
|
|
72
|
+
아니라 — ARIA `aria-level`/`aria-posinset`/`aria-setsize`가 정의하는 것과 같은 단위),
|
|
73
|
+
그리고 제품이 조립한 `accessibleName`을 붙인다. `hasChildren`/`expanded`를 함께 넘겨
|
|
74
|
+
"펼쳐짐"/"접힘" 문구를 붙일지, 리프에서 아예 뺄지는 제품이 정한다(Steps/Timeline/
|
|
75
|
+
Carousel과 같은 이유로 어순·조사를 여기서 조립하지 않는다).
|
|
76
|
+
|
|
77
|
+
### 화살표 키는 방향에 따라 뜻이 바뀐다
|
|
78
|
+
|
|
79
|
+
`getTreeArrowKeyIntent(key, direction)`는 `ArrowRight`/`ArrowLeft`를 `expand`/`collapse`로
|
|
80
|
+
바꾸되 `rtl`에서는 뒤집는다 — `getSelectionNavigationIntent`가 CheckboxGroup/RadioGroup
|
|
81
|
+
방향키에 이미 적용하는 것과 같은 번역이다. `getTreeArrowResult`는 WAI-ARIA tree 패턴을
|
|
82
|
+
그대로 따른다: 접힌 채 자식이 있으면 펼치고, 이미 펼쳐졌으면 첫 자식으로 포커스
|
|
83
|
+
이동(둘 다 이미 `resolved` 배열에 있는 `parentId`/`position`으로 찾는다, 트리를 다시
|
|
84
|
+
훑지 않는다) — collapse는 그 반대(펼쳐졌으면 접고, 리프거나 이미 접혔으면 부모로).
|
|
85
|
+
|
|
86
|
+
### 한 노드는 하나의 tab stop이다
|
|
87
|
+
|
|
88
|
+
펼침/접힘 chevron은 별도 버튼이 아니라 장식이다. 화살표 키(또는 행 클릭)가 펼침을
|
|
89
|
+
바꾸고, chevron 자체는 포커스를 받지 않는다 — "선택 행 전체가 하나의 target이며 내부에
|
|
90
|
+
또 다른 button/link를 넣지 않는다"는 이 저장소의 기존 규칙(`docs/architecture.md`의
|
|
91
|
+
선택 입력 절)을 그대로 따른다.
|
|
92
|
+
|
|
93
|
+
### 뺀 것
|
|
94
|
+
|
|
95
|
+
- **드래그 재정렬.** 측정된 요구가 없고, "어느 부모 아래 몇 번째로 옮겼는가"를
|
|
96
|
+
검증하는 계약은 지금 계약보다 훨씬 크다. 실제 화면이 나오면 그때 연다.
|
|
97
|
+
- **비동기 자식 지연 로딩(각 노드별 loading 상태).** 트리 전체의
|
|
98
|
+
`AsyncCollectionState`(`TreeAsyncState`, Select/Combobox와 같은 타입)는 열어 두지만,
|
|
99
|
+
"이 노드의 자식만 아직 로딩 중"이라는 노드별 축은 넣지 않았다 — 브리프도 요구하지
|
|
100
|
+
않았고, 실사용처가 확인되지 않은 상태에서 새 축을 추가하면 유령 계약이 된다(Steps의
|
|
101
|
+
판단과 같은 이유).
|
|
102
|
+
|
|
103
|
+
## HJM 기본값
|
|
104
|
+
|
|
105
|
+
- `node` 슬롯은 새 시각을 만들지 않고 `collectionItemContract`를 그대로 쓴다(Menu/Select
|
|
106
|
+
항목과 같은 44-unit 행, hover/selected 배경, focus indicator).
|
|
107
|
+
- `indentPerLevel`은 `spacing.lg`(20) — 깊이 하나당 이 만큼 들여쓴다.
|
|
108
|
+
- toggle 아이콘은 새 글리프를 만들지 않고 기존 `chevronEnd`(접힘)/`chevronDown`(펼침)을
|
|
109
|
+
쓴다. `chevronEnd`는 논리 방향이라 RTL에서 자동으로 뒤집힌다.
|
|
110
|
+
|
|
111
|
+
## 검증 화면
|
|
112
|
+
|
|
113
|
+
아직 없음. `planned → beta` 승격은 실제 제품 vertical slice 이후 리드가 진행한다.
|
|
114
|
+
|
|
115
|
+
## TreeSelect 판정
|
|
116
|
+
|
|
117
|
+
TreeSelect는 Tree를 계약한 지금도 **만들지 않는다.** 한 줄 판정: TreeSelect는 "Select의
|
|
118
|
+
표면(트리거+뜨는 목록, 하나 또는 여럿을 committed value로 커밋) 위에 Tree의 collection을
|
|
119
|
+
올린 것"이라 Tree와 별개의 순회/발화 문제를 새로 풀지 않는다 — 지금 이 문서가 정의한
|
|
120
|
+
depth/sibling 발화, 화살표 판정, `expandedKeys` 재조정을 그대로 가져다 Select의
|
|
121
|
+
popup/sheet 표면에 얹으면 된다. 새 recipe나 새 상태 축이 필요해 보이지 않으므로, 측정된
|
|
122
|
+
제품 요구가 나오기 전까지는 `src/tree-select.ts`를 만들지 않는다(`docs/dropdown.md`·
|
|
123
|
+
`docs/notification.md`와 같은 판단).
|