@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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# SidePanel contract
|
|
2
|
+
|
|
3
|
+
**문제.** 화면 가장자리에서 밀려 나오는 보조 콘텐츠 — 상세 편집, 필터, 보조
|
|
4
|
+
내비게이션 — 를 보여준다. antd `Drawer`는 `docs/ant-design-coverage.md`가 이미
|
|
5
|
+
`Sheet`(adaptive, 주로 하단)와 `SidePanel`(Web)로 **decomposed**하기로 선언해
|
|
6
|
+
뒀다(`src/component-references.ts`). 이 문서의 본체는 "SidePanel이 Sheet와 정확히
|
|
7
|
+
어디서 다른가"다 — 같은 곳은 재사용하고, 다른 곳만 새로 계약한다.
|
|
8
|
+
|
|
9
|
+
**Sheet와 같은 것 (재사용).**
|
|
10
|
+
|
|
11
|
+
- 열림 계약의 모양은 `SheetOpenState`와 동일한 controlled/uncontrolled 쌍이다
|
|
12
|
+
(`open`/`defaultOpen`/`onOpenChange`). `sheet.ts`를 제네릭하게 고치는 것도
|
|
13
|
+
검토했지만 그 파일은 이 배치의 다른 저작자·리드가 쓰는 공유 파일이라 건드릴 수
|
|
14
|
+
없고, 아래 `modal` 분기 로직 자체가 새로 필요하므로 그대로 가져와도 절약되는
|
|
15
|
+
것이 없다 — 그래서 `SidePanelOpenState`로 같은 모양을 다시 선언했다.
|
|
16
|
+
- `close-action`/`escape`/`programmatic`은 Sheet와 같은 의미다. `programmatic`은
|
|
17
|
+
controlled owner의 권한이라 `dismissible`/`busy`와 무관하게 항상 허용된다 —
|
|
18
|
+
`canDismissSidePanel`의 첫 줄이 `canDismissSheet`와 같다.
|
|
19
|
+
- Reduced Motion에서도 exit 콜백은 항상 한 번 발생한다(Sheet와 동일 원칙).
|
|
20
|
+
|
|
21
|
+
**Sheet와 다른 것 (새로 계약).**
|
|
22
|
+
|
|
23
|
+
1. **edge, not placement.** Sheet의 `placement` 기본값은 `bottom`이고 모바일
|
|
24
|
+
바텀시트가 기준이다. SidePanel은 애초에 가장자리에서 나온다는 것이 정체성이라
|
|
25
|
+
`edge: "start" | "end"`(논리 방향, RTL에서 뒤집힘)만 갖는다 — `top`/`bottom`은
|
|
26
|
+
Sheet의 자리로 남겨 두고 가져오지 않는다.
|
|
27
|
+
2. **`modal`은 Sheet에 없는 축이다.** Sheet는 항상 모달이라 이 축이 필요 없었다.
|
|
28
|
+
SidePanel은 리드가 지목한 대로 **비모달**(콘텐츠를 옆으로 밀거나 겹쳐 보여주되
|
|
29
|
+
나머지 페이지가 계속 상호작용 가능한 경우)이 실제로 다른 컴포넌트가 되는 지점이다.
|
|
30
|
+
- 비모달에는 배경을 클릭해서 닫을 대상이 없다 — 나머지 페이지가 그대로
|
|
31
|
+
살아있는 상호작용 표면이기 때문이다. 그래서 `outsideDismiss`를 `false`로
|
|
32
|
+
**기본값 처리**하지 않고, `SidePanelDismissPolicy`를 `modal` 판별 유니언으로
|
|
33
|
+
나눠 `modal: false` 분기에는 `outsideDismiss` 필드 자체가 없다(`never`).
|
|
34
|
+
`test/side-panel.test.ts`가 `@ts-expect-error`로 이 조합이 컴파일조차 되지
|
|
35
|
+
않음을 확인한다 — `SheetOpenState`가 controlled/uncontrolled를 섞지 않는 것과
|
|
36
|
+
같은 방식으로 "무효 조합을 타입이 막는다."
|
|
37
|
+
- focus trap과 스크롤 락은 `modal: true`일 때만 적용한다. `modal: false`는
|
|
38
|
+
Web 랜드마크(`role="dialog"` 대신 보조 영역)로 남아 포커스를 가두지 않고
|
|
39
|
+
열려 있는 동안에도 페이지 나머지가 tab 가능해야 한다. `behaviorRegistry`의
|
|
40
|
+
`web.focus` 필드는 값 하나만 가질 수 있어 기본 설정(`modal: true` →
|
|
41
|
+
`"trap"`)만 기록했고, `modal: false`의 예외는 여기 prose와 scenarios에
|
|
42
|
+
남긴다 — 이 계약이 이미 대표값 하나를 적고 나머지를 scenario로 미루는
|
|
43
|
+
자리(Select의 `activeDescendant`처럼)와 같은 방식이다.
|
|
44
|
+
- 콘텐츠를 실제로 밀어내는 layout(flex/grid로 형제 콘텐츠 폭을 줄이는 것)과
|
|
45
|
+
떠 있는 overlay로 렌더링하는 것 중 어느 쪽인지는 **이 계약에 없다** — 그건
|
|
46
|
+
DOM 배치 전략이라 renderer/제품 몫이다. 계약은 dismiss·focus 의미와 시각
|
|
47
|
+
토큰까지만 고정한다.
|
|
48
|
+
3. **`back`/`swipe`가 없다.** SidePanel은 Web 전용(`platform: "web"`)이라 Android
|
|
49
|
+
하드웨어 back도, 실측된 swipe-to-dismiss 요구도 없다. `SidePanelDismissReason`은
|
|
50
|
+
`close-action | escape | outside | programmatic` 네 값뿐이다.
|
|
51
|
+
4. **`createSheetLifecycle` 대응물이 없다.** 그 lifecycle counter는 Android
|
|
52
|
+
persistent native Modal이 dismiss 완료 시점을 스스로 알려주지 않는 문제를
|
|
53
|
+
풀기 위한 것이었다. Web에는 그 문제가 없다 — `sheetRecipe`/`dialogRecipe`가
|
|
54
|
+
이미 Web에 가정하는 평범한 `transitionend`/exit-callback 패턴으로 충분하다.
|
|
55
|
+
5. **radius가 다르다.** Sheet의 `content.radius`는 `xl`(공중에 뜬 카드처럼 둥근
|
|
56
|
+
모서리). SidePanel은 도킹된 가장자리에 그대로 붙는 서랍이라 `radius: null`
|
|
57
|
+
(뷰포트 경계에 flush) — Sheet 토큰을 그대로 베끼지 않은 자리 중 하나다.
|
|
58
|
+
|
|
59
|
+
**HJM 기본값.** `modal: true`가 기본이다 — Sheet와 가장 가까운, 가장 많이 검증된
|
|
60
|
+
경로를 기본으로 두고 비모달은 명시적 opt-in으로 남긴다(측정된 요구가 나타나면
|
|
61
|
+
그때 기본을 재검토한다). 폭은 `compact 320 / regular 400 / wide 560`이며 header
|
|
62
|
+
target은 `control.minTouchTarget`(44) 이상을 유지한다.
|
|
63
|
+
|
|
64
|
+
**플랫폼 번역.** Web 전용이다. `modal: true`는 `role="dialog"` + Tab trap +
|
|
65
|
+
스크롤 락, `modal: false`는 보조 landmark + 트랩 없음이다. `dismiss`는
|
|
66
|
+
`["escape", "outside"]`만 공개한다 — Sheet의 web dismiss 목록과 같은 관례로,
|
|
67
|
+
버튼 클릭(`close-action`)은 renderer가 직접 `requestClose`를 호출하는 일반
|
|
68
|
+
동작이라 "플랫폼이 감지하는 중단 벡터" 목록에 넣지 않는다.
|
|
69
|
+
|
|
70
|
+
**검증 화면.** 아직 실제 제품 vertical slice가 없다 — catalog는 `planned`으로
|
|
71
|
+
남고, `beta` 승격은 로드맵 gate(실제 화면 검증)를 통과한 뒤 리드가 진행한다.
|
package/docs/slider.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Slider contract
|
|
2
|
+
|
|
3
|
+
**문제.** 사용자가 범위 안에서 값 하나를 대략적으로, 연속 조작으로 고른다 — 만족도
|
|
4
|
+
점수, 필터 강도처럼 "정확히 몇" 보다 "이 근처"가 중요한 입력이다. 정확한 수를 타이핑하는
|
|
5
|
+
NumberField와는 반대 극단의 문제이며, 둘 다 같은 min/max/step 판정을 공유한다
|
|
6
|
+
([[number-field]] 참고, `validateNumericRangeConfig`/`clampToRange`/`snapToStep`은
|
|
7
|
+
`src/number-field.ts`에 있고 이 모듈이 그대로 import한다).
|
|
8
|
+
|
|
9
|
+
**일반화한 계약.**
|
|
10
|
+
|
|
11
|
+
- `label`(필수, 접근성 이름), `value`, `min`, `max`, `step?`(기본 1),
|
|
12
|
+
`valueText?`(제품이 포맷한 현재 값 문자열, 예 "75점", ".357").
|
|
13
|
+
- Slider는 항상 값을 갖는다 — `empty` 상태가 없다. NumberField와 달리 "아직 정하지
|
|
14
|
+
않음"을 표현할 필요가 실사용처에 없었고, 대략적인 선택이라는 상호작용 자체가 항상
|
|
15
|
+
현재 위치를 전제하기 때문이다.
|
|
16
|
+
- `range`(두 손잡이로 구간을 고르는 모드)는 이번 계약에 넣지 않는다. 실제로 이 범위
|
|
17
|
+
선택이 필요한 화면이 아직 없고, 값 하나짜리 controlled 축과 값 두 개짜리 controlled
|
|
18
|
+
축을 동시에 설계하면 검증되지 않은 API를 먼저 얹는 셈이 된다.
|
|
19
|
+
- `resolveSliderFillFraction`은 트랙 채움 비율(0..1)만 계산한다 — 표시 문자열은 만들지
|
|
20
|
+
않는다. `valueText`도 컴포넌트가 생성하지 않고 그대로 전달만 한다.
|
|
21
|
+
- 제품이 공급한 controlled/default 값은 범위 안이라면 step grid 밖이어도 그대로 표시한다.
|
|
22
|
+
사용자 입력만 `resolveSliderValue`에서 min-origin step으로 snap하며, span이 step으로
|
|
23
|
+
나누어떨어지지 않아도 정확한 `min`/`max` 끝점은 항상 보존한다.
|
|
24
|
+
- `getSliderStepTarget`이 모든 키보드/RN step intent(`increment`/`decrement`/
|
|
25
|
+
`increment-page`/`decrement-page`/`first`/`last`)를 하나의 함수로 판정해, 방향키든
|
|
26
|
+
Page키든 Home/End든 stepper와 같은 snap·clamp 규칙을 탄다.
|
|
27
|
+
- drag/input 중에는 `onValueChange`, pointer release·keyboard keyup·Native adjustable
|
|
28
|
+
action 완료에는 `onValueChangeEnd`를 보낸다. React Aria와 Chakra의 change/change-end
|
|
29
|
+
분리를 따르며, commit callback을 매 move마다 중복 호출하지 않는다.
|
|
30
|
+
- `resolveSliderValueFromOffset`은 실제 track offset을 값으로 바꾼 뒤 Web input과 같은
|
|
31
|
+
`resolveSliderValue`를 사용한다. RTL은 물리 track만 반전하며 increment/decrement의 논리
|
|
32
|
+
의미는 바꾸지 않는다.
|
|
33
|
+
|
|
34
|
+
**HJM 기본값.** 트랙 4px, thumb 지름 20px이지만 hit target은 44-unit
|
|
35
|
+
(`control.minTouchTarget`)을 유지한다 — 보이는 손잡이가 작아도 누르는 영역은 작지
|
|
36
|
+
않다. 채움 색(`trackFilled`)과 미채움 색은 각각 `content.brand`/`surface.sunken`이고,
|
|
37
|
+
값은 색만으로 말하지 않는다 — 접근성 발화가 항상 `valueText`(또는 raw value)를
|
|
38
|
+
동반한다. `dragged`는 `interaction` 축의 값으로 다루며 `pressed`와 시각적으로 구분한다.
|
|
39
|
+
|
|
40
|
+
**플랫폼 번역.**
|
|
41
|
+
|
|
42
|
+
- Web: role `slider`, keyboard `ArrowLeft/Right`(방향 무관 1 step), `ArrowUp/Down`(보조),
|
|
43
|
+
`Home`/`End`(min/max로 이동), `PageUp`/`PageDown`(step × 10, `sliderBehaviorDefaults.
|
|
44
|
+
pageMultiplier`). 스크린 리더 발화 순서는 라벨 → 현재 값(`valueText` 있으면 그 문자열,
|
|
45
|
+
없으면 raw 숫자) → 범위(min/max)이며 이는 `aria-label`/`aria-valuetext`/
|
|
46
|
+
`aria-valuemin`/`aria-valuemax`가 native하게 만드는 순서를 그대로 쓴다 — 컴포넌트가
|
|
47
|
+
별도 문구를 조립하지 않는다.
|
|
48
|
+
숨은 native range의 HTML `step`은 `any`로 둬 브라우저가 off-grid controlled 값이나
|
|
49
|
+
non-divisible `max`를 렌더 전에 바꾸지 못하게 한다. 실제 public `step` 판정은 input,
|
|
50
|
+
Arrow/Page/Home/End 모두 shared resolver가 수행한다.
|
|
51
|
+
- React Native: role `adjustable`, `accessibilityValue={{min, max, now, text}}`가 같은
|
|
52
|
+
라벨→값→범위 순서를 만든다. action은 `increment`/`decrement` 둘뿐이다 — RN
|
|
53
|
+
`adjustable`은 페이지 단위 이동 개념이 없어 Web의 PageUp/PageDown을 그대로 옮기지
|
|
54
|
+
않는다. drag 중 `disabled=true`로 전환되면 그 시점의 마지막 값을 한 번 commit하고
|
|
55
|
+
active gesture를 종료하며, 이후 stale move/release는 무시한다.
|
|
56
|
+
- Web/RN 모두 controlled `value` 또는 uncontrolled `defaultValue`를 받으며 둘 다 없으면
|
|
57
|
+
`min`에서 시작한다. mounted component가 두 mode 사이를 전환하는 것은 허용하지 않는다.
|
|
58
|
+
- `validation`(valid/invalid) 축은 공개하지 않는다 — 그 축은 NumberField 전용이다
|
|
59
|
+
([[number-field]]). Slider의 값은 범위 안에 있으면 항상 유효하다.
|
|
60
|
+
|
|
61
|
+
**검증 화면.** 아직 실제 제품 vertical slice가 없다 — catalog는 `planned`으로 남고,
|
|
62
|
+
`beta` 승격은 로드맵의 gate(실제 화면 검증)를 통과한 뒤 리드가 진행한다.
|
|
63
|
+
|
|
64
|
+
**초기 renderer 범위 밖.** range/multi-thumb, vertical orientation, marks/ticks, tooltip,
|
|
65
|
+
non-linear scale, drag acceleration은 실제 제품 요구 전까지 추가하지 않는다. Web은
|
|
66
|
+
`step="any"` native `input[type="range"]`를 44px interaction layer로 사용하고 public step은
|
|
67
|
+
shared resolver로 적용한다. Native는 core responder system만 사용해 외부 slider/native
|
|
68
|
+
module dependency를 만들지 않는다.
|
|
69
|
+
|
|
70
|
+
**참고 구현과 기준.** WAI-ARIA APG
|
|
71
|
+
[Slider pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider/)의 role/value/key contract,
|
|
72
|
+
Adobe React Aria [Slider](https://react-spectrum.adobe.com/Slider)의 continuous change와
|
|
73
|
+
change-end 분리 및 min-origin step, Chakra UI
|
|
74
|
+
[Slider](https://chakra-ui.com/docs/components/slider)의 hidden native input·controlled/
|
|
75
|
+
uncontrolled·`onValueChangeEnd`, MUI
|
|
76
|
+
[Slider](https://mui.com/material-ui/react-slider/)의 명시적 accessible label/value text를
|
|
77
|
+
참고했다. Native action은 React Native 공식
|
|
78
|
+
[Accessibility actions](https://reactnative.dev/docs/accessibility#accessibility-actions)의
|
|
79
|
+
`adjustable` + `increment`/`decrement` 조합으로 번역한다.
|
package/docs/splitter.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Splitter contract
|
|
2
|
+
|
|
3
|
+
**문제.** 사용자가 두 영역의 경계를 드래그(또는 키보드)로 옮겨 상대적인 크기를
|
|
4
|
+
정한다 — 코드 편집기의 파일 트리/에디터 폭, 목록/상세 폭처럼 데스크톱 웹 레이아웃
|
|
5
|
+
패턴이다. antd `Splitter` → HJM `Splitter`(`src/component-references.ts`,
|
|
6
|
+
`relationship: "direct"`).
|
|
7
|
+
|
|
8
|
+
**판정 기준 통과 여부.**
|
|
9
|
+
|
|
10
|
+
- **제품 의미**: 있다. 경계를 옮기는 것은 사용자가 "이 화면을 지금 이렇게 나눠
|
|
11
|
+
보겠다"는 명시적 의도이며, 값이 남는다(다시 방문했을 때 유지되길 기대한다) —
|
|
12
|
+
Stack/Grid처럼 개발자가 flex를 덜 쓰려는 것과 다르다.
|
|
13
|
+
- **접근성 계약**: 있다. WAI-ARIA `separator` role은 이미 `aria-valuenow`/
|
|
14
|
+
`aria-orientation`/키보드 리사이즈라는 실제 표준 패턴을 갖는다 — 계약을 새로
|
|
15
|
+
발명하는 게 아니라 이미 있는 표준 하나를 HJM 어휘로 옮기는 일이다.
|
|
16
|
+
- **플랫폼 번역**: 성립하지 않는다 — 그리고 그것도 판정이다. 지속적으로 드래그
|
|
17
|
+
가능한 분할 패널은 사실상 넓은 뷰포트/데스크톱 패턴이라 모바일 앱에 대응하는
|
|
18
|
+
관습이 없다. 그래서 이 컴포넌트는 `platform: "web"`(catalog가 이미 그렇게
|
|
19
|
+
분류해 뒀다)로 남긴다 — Native 계약을 만들지 않는다.
|
|
20
|
+
|
|
21
|
+
**일반화한 계약.**
|
|
22
|
+
|
|
23
|
+
- 사실 이 문제는 **"범위 안에서 숫자 하나를 고른다"**는, `NumberField`/`Slider`가
|
|
24
|
+
이미 푼 문제와 같다(`docs/slider.md`가 이미 NumberField와 이 관계를 적어
|
|
25
|
+
뒀다). 그래서 `SplitterDescriptor`는 새 숫자 판정을 만들지 않고
|
|
26
|
+
`src/number-field.ts`의 `validateNumericRangeConfig`/`clampToRange`/`snapToStep`/
|
|
27
|
+
`stepNumericValue`를 **그대로 호출**한다. `resolveSplitterDragValue`(드래그값
|
|
28
|
+
스냅)와 `getNextSplitterValue`(키보드 스텝)는 둘 다 그 함수들의 얇은 래퍼일
|
|
29
|
+
뿐이다.
|
|
30
|
+
- `axis`(`horizontal`/`vertical`, 패널이 나란한 방향)와 separator의
|
|
31
|
+
`aria-orientation`은 **반대**다 — 나란히 있는(가로) 패널의 경계는 세로 막대다.
|
|
32
|
+
`resolveSplitterSeparatorOrientation`이 이 반전을 한 곳에서만 계산해, 매
|
|
33
|
+
renderer가 각자 다시 헷갈리지 않게 한다.
|
|
34
|
+
- `label`은 필수 접근성 이름이고, `valueText`는 Slider와 같은 이유로 선택
|
|
35
|
+
사항이다 — 제품이 "35%"/"320px" 같은 포맷된 문자열을 줄 수도, 안 줄 수도
|
|
36
|
+
있다(안 주면 raw 숫자로 발표).
|
|
37
|
+
- **넣지 않은 것**: 패널을 완전히 접어 숨기는 collapse 기능(antd의 collapsible
|
|
38
|
+
화살표)과 분리선 여러 개(N-pane)를 이번 계약에 넣지 않았다. Collapse는 별도
|
|
39
|
+
reveal affordance와 `min` 경계를 우회하는 예외 상태가 필요해 계약 표면을 거의
|
|
40
|
+
두 배로 늘리고, N-pane은 값 하나가 아니라 정렬된 분리선 목록이 필요해 완전히
|
|
41
|
+
다른 자료구조가 된다. 둘 다 측정된 요구가 없다 — Slider가 두 손잡이 `range`
|
|
42
|
+
모드를 넣지 않은 것과 같은 판단이다.
|
|
43
|
+
|
|
44
|
+
**HJM 기본값.** Slider와 같은 "작은 손잡이, 큰 hit target" 문법을 재사용한다 —
|
|
45
|
+
보이는 선은 1px, 실제 드래그/포커스 가능 영역(`hitTarget`)은
|
|
46
|
+
`control.minTouchTarget`(44) 이상이다.
|
|
47
|
+
|
|
48
|
+
**플랫폼 번역.** Web만: `role="separator"` + `aria-orientation` + `aria-valuenow`/
|
|
49
|
+
`aria-valuemin`/`aria-valuemax`/(선택) `aria-valuetext`. 키보드는 Slider와 같은
|
|
50
|
+
어휘를 재사용한다 — 방향키 1 step, Home/End로 경계값 이동. Slider의
|
|
51
|
+
PageUp/PageDown(10배 이동)은 넣지 않았다 — 분할 패널 크기 조정은 그 정도로 큰
|
|
52
|
+
점프가 필요하다는 요구가 측정되지 않았다.
|
|
53
|
+
|
|
54
|
+
**검증 화면.** 아직 실제 제품 vertical slice가 없다 — catalog는 `planned`으로
|
|
55
|
+
남고, `beta` 승격은 로드맵 gate(실제 화면 검증)를 통과한 뒤 리드가 진행한다.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Statistic contract
|
|
2
|
+
|
|
3
|
+
Statistic은 숫자를 계산하거나 포맷하지 않습니다. 제품 adapter가 locale·단위·야구 규칙에 맞춰
|
|
4
|
+
완성한 문자열을 넘기고 HJM은 반복되는 정보 위계와 읽기 순서만 소유합니다.
|
|
5
|
+
|
|
6
|
+
- 각 항목은 stable `id`, 보이는 `label`, 포맷이 끝난 `value`를 가집니다.
|
|
7
|
+
- `prefix`, `suffix`, `hint`는 선택 사항이며 빈 문자열은 허용하지 않습니다.
|
|
8
|
+
- 숫자 value는 tabular glyph를 사용하고 줄 수를 제한하지 않아 큰 글자·긴 단위가 잘리지 않게 합니다.
|
|
9
|
+
- trend의 `direction`과 `tone`은 분리합니다. 예를 들어 투구 수 증가는 `up + danger`, 순위 숫자
|
|
10
|
+
증가는 제품 의미에 따라 `up + neutral`일 수 있습니다.
|
|
11
|
+
- trend에는 arrow/minus mark와 현지화된 visible label이 모두 필요해 색만으로 의미를 전달하지
|
|
12
|
+
않습니다.
|
|
13
|
+
- `StatisticGroup`은 1–4열 선호와 stable id 검증만 제공합니다. Web/RN renderer는 실제 폭과 큰
|
|
14
|
+
글자에 맞춰 1열까지 wrap하며 value 줄 수를 강제로 자르지 않고, 각 Statistic을 독립 접근성
|
|
15
|
+
항목으로 남깁니다.
|
|
16
|
+
- Statistic 자체는 interactive하지 않습니다. 탐색이나 동작은 바깥 Link/Button이 소유합니다.
|
package/docs/steps.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Steps contract
|
|
2
|
+
|
|
3
|
+
## 문제
|
|
4
|
+
|
|
5
|
+
사용자가 여러 단계로 이루어진 흐름의 어디에 서 있는지 보여준다 — 지나온 단계가 몇 개인지,
|
|
6
|
+
지금 단계가 무엇인지, 남은 단계가 몇 개인지. Yajalal 온보딩(환영 → 구단 → 관심 선수 → 알림
|
|
7
|
+
→ 완료)과 BurnTok 가입 흐름이 같은 문제를 각자 화면에서 풀고 있다.
|
|
8
|
+
|
|
9
|
+
## 일반화한 계약
|
|
10
|
+
|
|
11
|
+
### collection 기본 계약과의 대응
|
|
12
|
+
|
|
13
|
+
각 step은 stable string `id`, 보이는 `label`, 선택적 `description`을 가진다(Collection 기본
|
|
14
|
+
계약의 최소 부분집합). `textValue`, `none|single|multiple` selection mode,
|
|
15
|
+
`idle|loading|loadingMore|empty|error` async state는 가져오지 않는다 — Steps는 검색/타이핑
|
|
16
|
+
탐색 대상이 아니고(제품이 문자로 찾지 않는다), 사용자가 선택하는 대상도 아니며(고르는 게
|
|
17
|
+
아니라 보여주기만 한다), 서버에서 비동기로 채워지는 목록도 아니다(제품이 항상 전체 flow를
|
|
18
|
+
동기로 안다). 이 세 축을 억지로 채우면 유령 계약이 된다.
|
|
19
|
+
|
|
20
|
+
### 단일 커서로 상태를 유도한다
|
|
21
|
+
|
|
22
|
+
로드맵의 공통 상태 축 표에는 `pending/current/complete/error`가 없다 — Steps 전용 축이다.
|
|
23
|
+
각 step은 이 네 값 중 하나만 가지지만, **제품이 각 step에 개별 status를 배열로 넘기지
|
|
24
|
+
않는다.** 대신 하나의 `currentStepId`(+ 선택적 `currentStepStatus: "current" | "error"`)만
|
|
25
|
+
받고, `resolveStepsDescriptor`가 배열 위치로 나머지를 유도한다.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
index < cursor → complete
|
|
29
|
+
index === cursor → currentStepStatus (기본 "current", 실패 시 "error")
|
|
30
|
+
index > cursor → pending
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
두 단계 이상 "current"이거나, cursor보다 앞선 단계가 아직 "pending"으로 남거나, cursor
|
|
34
|
+
너머의 단계가 "complete"인 상태는 애초에 표현할 수 없다 — 이 저장소의 다른 계약들
|
|
35
|
+
(`SheetOpenState`, `ComboboxCollectionState`, `LoadMoreState`)과 같은 이유다: 유효하지 않은
|
|
36
|
+
조합을 타입과 유도 규칙으로 만들 수 없게 한다. clickable을 공개하지 않기로 했으므로(아래
|
|
37
|
+
참고) 흐름은 항상 cursor 기준 선형이라 이 유도가 항상 맞다.
|
|
38
|
+
|
|
39
|
+
### 순서를 접근성 이름에 남긴다
|
|
40
|
+
|
|
41
|
+
마커가 숫자를 시각적으로만 보여주면(원 안의 "2") 화면낭독기는 라벨만 읽고 몇 번째인지
|
|
42
|
+
잃는다. `resolveStepsDescriptor`는 각 step에 `position`(1-based), `total`, 그리고 제품이
|
|
43
|
+
공급한 `composeAccessibleName({ position, total, label })`으로 만든 `accessibleName`을
|
|
44
|
+
붙인다. 한국어 "3단계 중 2단계"는 영어 "step 2 of 3"과 어순이 다르므로 HJM이 문장을
|
|
45
|
+
조립하지 않고 — 항상 조립은 하되(브리프 요구) 실제 어순·조사는 product composer에
|
|
46
|
+
맡긴다. resolver는 composer가 빈 문자열을 반환하면 던진다.
|
|
47
|
+
|
|
48
|
+
### 상태는 색 하나로 말하지 않는다
|
|
49
|
+
|
|
50
|
+
- 시각: `stepsRecipe.indicator.marks`가 pending/current는 숫자(마커 없음, `null`), complete는
|
|
51
|
+
기존 `check` 아이콘, error는 기존 `error` 아이콘을 쓴다. 넷 다 border·content 색이 달라
|
|
52
|
+
색맹 사용자도 테두리 유무·마크 모양으로 구분한다.
|
|
53
|
+
- 낭독: 각 resolved step은 `statusLabel`(제품이 공급한 `StepsStatusLabels`에서 상태별 문구)을
|
|
54
|
+
들고 있다. CheckboxGroup 계약과 같은 이유 — Native에는 Web `aria-current="step"`에 대응하는
|
|
55
|
+
공용 상태가 없으므로, 상태 문구를 renderer가 Web에서는 보조 텍스트로, Native에서는
|
|
56
|
+
`accessibilityHint`로 전달해야 한다. `accessibleName`(순서+라벨)이 주 이름이고
|
|
57
|
+
`statusLabel`은 보충이다 — 상태 단어가 이름 앞에 와서 라벨을 덮지 않는다.
|
|
58
|
+
|
|
59
|
+
## HJM 기본값
|
|
60
|
+
|
|
61
|
+
- `currentStepStatus` 기본값은 `"current"`(`stepsDefaults.currentStepStatus`). 실패를
|
|
62
|
+
표현하려는 제품만 명시적으로 `"error"`를 넘긴다.
|
|
63
|
+
- `complete` = success 톤(`semanticColors.feedback.success`), `error` = danger 톤
|
|
64
|
+
(`semanticColors.feedback.danger`) — badge/notice가 이미 쓰는 soft background + border +
|
|
65
|
+
foreground 3단 조합을 재사용한다.
|
|
66
|
+
- `current`는 배경을 채우지 않고 `semanticColors.content.brand` 테두리·글자만 쓴다.
|
|
67
|
+
identity.md가 명시한 대로 `primary` fill(버튼 등 주요 행동)과 `contentBrand`(포커스·선택·
|
|
68
|
+
현재 위치)를 분리하기 위해서다 — Steps 마커는 행동이 아니라 위치 표시이므로 contentBrand만
|
|
69
|
+
쓴다.
|
|
70
|
+
- `connector.tone`은 `reached`(brand) / `unreached`(border.default) 두 값뿐이다.
|
|
71
|
+
`isStepReached(status)`(`pending`만 false)로 renderer가 인접 segment 색을 고른다.
|
|
72
|
+
- 텍스트(라벨·description)와 마커 글자·아이콘(`indicator.content`), 도달한 connector 색은
|
|
73
|
+
각각 4.5:1 / 3:1을 만족하도록 recipe 색을 선택했다(test로 두 기준을 분리해 검증). `pending`
|
|
74
|
+
마커 테두리와 `connector.unreached`는 Divider·Badge가 이미 쓰는 공용 저대비 `border.default`/
|
|
75
|
+
alpha 톤을 그대로 재사용하는 장식적 tint라 대비 기준 대상이 아니다 — 상태 구분은 마커
|
|
76
|
+
글자·모양(숫자 vs check vs error)과 `statusLabel`이 이미 이중으로 전달한다.
|
|
77
|
+
|
|
78
|
+
## 플랫폼 번역
|
|
79
|
+
|
|
80
|
+
- Web: cursor step(`status === "current" | "error"`)에만 `aria-current="step"`을 단다.
|
|
81
|
+
마커 아이콘은 decorative(숨김)로 두고 root의 accessible name은 `accessibleName`
|
|
82
|
+
하나다. `statusLabel`은 visually-hidden 텍스트 또는 `aria-describedby`로 덧붙인다.
|
|
83
|
+
connector는 `aria-hidden`.
|
|
84
|
+
- Native(RN): root에 `accessibilityLabel=accessibleName`,
|
|
85
|
+
`accessibilityHint=statusLabel`(Web의 aria-current 동등물이 없어 hint로 상태를 전달 —
|
|
86
|
+
CheckboxGroup의 `aria-required`/`aria-invalid` 대응과 같은 처리). 마커·connector는
|
|
87
|
+
`accessibilityElementsHidden`/`importantForAccessibility="no"`.
|
|
88
|
+
- Reduce Motion: 상태 전환(예: current → complete) 애니메이션은 즉시 전환 또는 짧은 opacity로
|
|
89
|
+
대체한다. 이동·반복 모션은 두지 않는다(motion 원칙 그대로).
|
|
90
|
+
|
|
91
|
+
## 공개한 축 / 배제한 축
|
|
92
|
+
|
|
93
|
+
| 축 | 상태 |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `pending / current / complete / error`(유도) | 공개 |
|
|
96
|
+
| 순서(`position`/`total`) | 공개 |
|
|
97
|
+
| clickable(스텝 탭으로 이동) | **배제** — 브리프 지침이자 실제 사용처 둘 다 뒤로 가기는 별도 버튼(Yajalal `OnboardingScreen`의 "이전" `AppButton`)이다. 필요해지면 그때 추가한다. |
|
|
98
|
+
| vertical 방향 | **배제** — 두 사용처 모두 가로 진행바 하나만 필요해 antd 표면을 미리 복제하지 않는다. |
|
|
99
|
+
| dot type(압축 점 표현) | **배제** — 측정된 요구가 없다. numbered + check/error 마크 하나만 공개한다. |
|
|
100
|
+
| custom icon(스텝별 임의 아이콘 교체) | **배제** — 4개 상태 마크로 충분하고, 임의 아이콘 허용은 브랜드 표현의 일관성을 깨뜨린다. |
|
|
101
|
+
| navigation type(`navigation`/`inline` 등 antd variant) | **배제** — 위 clickable/vertical 배제와 같은 이유로 단일 표현만 남긴다. |
|
|
102
|
+
|
|
103
|
+
## 검증 화면
|
|
104
|
+
|
|
105
|
+
first-party Web·Native renderer와 상태 파생·환경 matrix 증거는 연결되어 surface는 `beta`다.
|
|
106
|
+
실제 제품 vertical slice는 아직 없으므로 `stable` 승격 gate는 닫혀 있다.
|
|
107
|
+
|
|
108
|
+
유력 후보였던 "Yajalal 온보딩(`OnboardingScreen`의 `AppProgress` 대체)"은 검증 결과
|
|
109
|
+
부정확했다 — `OnboardingScreen.tsx`는 지금도 그대로 `AppProgress`(연속 진행바,
|
|
110
|
+
`value={currentStepIndex + 1}` + `valueText="N / M"`)를 쓰고 있고, 이를 Steps로 바꾸는
|
|
111
|
+
결정된 계획은 어디에도 없다. 화면 자체(환영→구단→관심 선수→알림→완료, 각 단계에 이름이
|
|
112
|
+
있고 뒤로 가기 버튼이 있음)는 Steps가 실제로 풀 수 있는 문제와 모양이 맞으므로 잠재
|
|
113
|
+
후보로는 남기되, "이미 대체 대상으로 정해진 화면"처럼 적지 않는다 — 지금은 제품이 그
|
|
114
|
+
화면을 진행바로 계속 쓰고 있다는 사실만 정확하다. BurnTok 가입 흐름은 이번 재검증에서
|
|
115
|
+
직접 확인하지 않았다.
|
package/docs/tag.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Tag contract
|
|
2
|
+
|
|
3
|
+
**문제.** 화면에 반복해서 등장하는 한 조각의 정적 메타데이터 — `좌익수`, `A등급`,
|
|
4
|
+
`2026 시즌` — 에 일관된 이름과 톤을 붙입니다. 이 라벨은 누를 수 없고, 선택되지도 않고,
|
|
5
|
+
지워지지도 않습니다.
|
|
6
|
+
|
|
7
|
+
**일반화한 계약.** 필수 `label`과 선택적 `tone`만 받는 표현 계약입니다(Statistic처럼
|
|
8
|
+
제품이 완성한 문자열을 받습니다). id, collection 멤버십, 선택 state는 없습니다 — Tag는
|
|
9
|
+
Collection 기본 계약의 대상이 아니라 낱개 표시 단위입니다.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const positionTag = { label: "좌익수" } satisfies TagDescriptor;
|
|
13
|
+
const gradeTag = { label: "A등급", tone: "success" } satisfies TagDescriptor;
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**HJM 기본값.** `tone`은 `neutral | info | success | attention | brand` 다섯 가지 공용
|
|
17
|
+
의미로 좁힙니다. `warning`과 `danger`는 포함하지 않습니다 — 위험이나 경고를 알리는 것은
|
|
18
|
+
Notice/Badge의 역할이고, Tag는 사실을 나열할 뿐 위협하지 않습니다. 기본 tone은 `neutral`.
|
|
19
|
+
|
|
20
|
+
## Chip과의 경계
|
|
21
|
+
|
|
22
|
+
이 시스템에는 이미 `Chip`(beta)이 있습니다. Chip은 action/radio/checkbox 의미를 가진
|
|
23
|
+
**누를 수 있는** 입력입니다 — `chipBehavior`가 `selected`, `onPress`, keyboard activation을
|
|
24
|
+
계약으로 소유합니다. Tag는 그 반대입니다.
|
|
25
|
+
|
|
26
|
+
- `tagRecipe`에는 `states`, `focus`, `selectionIndicator`가 없습니다. 아무것도 눌리거나
|
|
27
|
+
포커스를 받지 않기 때문입니다.
|
|
28
|
+
- `closable`(지울 수 있는 태그)은 이 계약에 **없습니다**. 태그를 지우는 것은 선택 해제이고,
|
|
29
|
+
선택 해제는 Chip의 `selected` 축이 이미 소유한 문제입니다. Tag에 별도의 dismiss 축을
|
|
30
|
+
추가하면 같은 "선택 해제"를 두 컴포넌트가 서로 다른 이름으로 계약하게 됩니다.
|
|
31
|
+
- 모양도 다릅니다. `chipRecipe`와 `badgeRecipe`는 `radius: "full"`(pill)을 쓰지만
|
|
32
|
+
`tagRecipe`는 `radius: "sm"`의 사각형입니다. 정적 라벨을 pill로 그리면 누를 수 있다는
|
|
33
|
+
암묵적 신호를 주므로, 형태 자체로도 상호작용 가능성을 부정합니다.
|
|
34
|
+
- 야잘알의 `AppBadge`가 이 역할(포지션·등급·시즌 라벨)을 수행한 제품 근거에서 Tag 계약을
|
|
35
|
+
추출했습니다. Web/RN 공식 renderer는 이 계약을 직접 소비하며, 제품의 기존 이름을 Tag로
|
|
36
|
+
바꾸는 일 자체는 소비 앱의 후속 migration입니다.
|
|
37
|
+
|
|
38
|
+
## 플랫폼 번역
|
|
39
|
+
|
|
40
|
+
Web과 Native 모두 순수 text + background 조각입니다. 상호작용이 없으므로 별도의
|
|
41
|
+
keyboard, focus, accessibilityState 계약이 필요 없습니다 — 접근성 트리에는 보이는 텍스트
|
|
42
|
+
노드 하나로 충분합니다. 여러 Tag를 한 줄에 나열할 때 간격 조합은 나중에 Stack/Inline
|
|
43
|
+
recipe가 안정화되면 그쪽에 위임하고, 이 계약에는 넣지 않습니다.
|
|
44
|
+
|
|
45
|
+
기본 높이는 20px이지만 고정 높이가 아닙니다. Dynamic Type에서 한 줄 라벨의 고유 높이만큼
|
|
46
|
+
늘어나며, renderer는 라벨을 고정 프레임 안에서 자르지 않습니다.
|
|
47
|
+
|
|
48
|
+
## 현재 maturity와 남은 증거
|
|
49
|
+
|
|
50
|
+
Tag contract와 Web/Native surface는 모두 **beta**입니다. 두 first-party renderer의 canonical
|
|
51
|
+
default render proof가 `tagRecipe`의 정적 text/background anatomy를 실행하고, generated
|
|
52
|
+
manifest와 evidence registry가 이 상태를 함께 검증합니다.
|
|
53
|
+
|
|
54
|
+
beta는 전체 환경 인증을 뜻하지 않습니다. dark, RTL, 200% text/Dynamic Type, screen reader와
|
|
55
|
+
실제 device screenshot은 아직 scenario별 실행 proof가 없으며 generated evidence의 debt로
|
|
56
|
+
남습니다. 야잘알의 기존 `AppBadge` 사용처를 canonical Tag renderer로 마이그레이션하고 이
|
|
57
|
+
환경 증거까지 연결한 뒤 stable 승격을 검토합니다.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# TimePicker — 새 컴포넌트를 만들지 않는다
|
|
2
|
+
|
|
3
|
+
## 문제로 제기된 것
|
|
4
|
+
|
|
5
|
+
Ant Design `TimePicker`는 필드 트리거를 누르면 시·분(·초) 열이 나란히 있는 팝업이 뜨고,
|
|
6
|
+
각 열에서 스크롤/클릭으로 값을 고르는 컴포넌트다. crosswalk는 이미
|
|
7
|
+
`{ name: "TimePicker", category: "data-entry", targets: ["TimePicker"], relationship:
|
|
8
|
+
"direct" }`로 연결돼 있다(`src/component-references.ts:86`).
|
|
9
|
+
|
|
10
|
+
## 판정: 새 컴포넌트가 필요 없다 — 문제가 이미 Select로 완결된다
|
|
11
|
+
|
|
12
|
+
시·분은 **격자가 아니라 목록**이다(Calendar의 날짜 격자와 다르다 — 그건 2차원이라
|
|
13
|
+
Collection 기본 계약을 적용하지 않기로 했지만, 시·분은 처음부터 1차원 목록이라 정확히
|
|
14
|
+
그 계약이 맞는 자리다). 시(0~23)와 분(0~59)은 각각:
|
|
15
|
+
|
|
16
|
+
- stable id(`"00"`..`"23"`, `"00"`..`"59"`)
|
|
17
|
+
- 보이는 label(제품이 포맷 — 24시간제, 앞자리 0)
|
|
18
|
+
- 선택적 `disabled`(예: 영업시간 밖 시각을 회색 처리)
|
|
19
|
+
- `none|single` 선택 모드, 정적 목록(비동기 상태 불필요)
|
|
20
|
+
|
|
21
|
+
인 **정확히 `CollectionItemDescriptor` 하나**다. 그리고 "트리거 + 적응형 오버레이(Web
|
|
22
|
+
popover / Native Sheet) + 단일 committed key + disabled 항목 skip 없는 예측 가능한
|
|
23
|
+
방향키 이동"은 이미 `Select`가 `beta`로 검증한 계약 그대로다. 즉:
|
|
24
|
+
|
|
25
|
+
**시 Select 하나 + 분 Select 하나 = TimePicker.** 제품이 두 값을 `"HH:mm"` 문자열로
|
|
26
|
+
합치기만 하면 antd `TimePicker`가 푸는 사용자 문제(하루 중 시각 하나를 고른다)는 이미
|
|
27
|
+
완결된다. 새 recipe도, 새 behavior도, 새 상태 축도 필요하지 않다 — Dropdown이
|
|
28
|
+
`Menu`로 완전히 흡수된 것과 같은 자리다: 격자처럼 여기서만 필요한 새 조각
|
|
29
|
+
(Calendar의 `row`/`column`/`overflow` 같은)이 하나도 없다.
|
|
30
|
+
|
|
31
|
+
## 실사용처 확인
|
|
32
|
+
|
|
33
|
+
Yajalal 전체(`날짜/시간 선택`, `TimePicker`, 알림 설정 화면 `NotificationSettingsScreen.tsx`
|
|
34
|
+
등)를 검색했지만 하루 중 시각 하나를 고르는 UI 자체가 어디에도 없다. 측정된 요구가
|
|
35
|
+
없다는 점에서도 지금 새 계약을 여는 것을 정당화할 근거가 없다.
|
|
36
|
+
|
|
37
|
+
## 왜 압축된 단일 트리거(진짜 antd 형태)를 만들지 않았는가
|
|
38
|
+
|
|
39
|
+
두 개의 Select를 나란히 두는 것과 antd처럼 **트리거 하나 + 팝업 하나 안에 두 열이
|
|
40
|
+
동기화된 형태**는 시각적으로 다르다. 후자를 만들려면:
|
|
41
|
+
|
|
42
|
+
- 하나의 열림 상태가 두 열을 동시에 제어해야 하고,
|
|
43
|
+
- 트리거의 표시 문자열이 두 committed key를 조합해야 하며(Statistic/Calendar와 같은
|
|
44
|
+
"제품이 포맷한 문자열을 받는다" 원칙),
|
|
45
|
+
- **"selection"이 더 이상 자동 닫힘 사유가 아니어야 한다** — 시만 고르고 분을 아직
|
|
46
|
+
안 골랐는데 닫히면 안 되기 때문이다(DatePicker/Select는 값 하나를 고르면 바로
|
|
47
|
+
닫히지만, 여기서는 두 값이 다 맞아떨어져야 "완료"다).
|
|
48
|
+
|
|
49
|
+
이 세 가지는 실제로 작겠지만 **새로운 축**이다 — Select의 트리거/오버레이/커밋 계약을
|
|
50
|
+
그대로 복제하면서 "selection은 안 닫는다"는 예외 하나만 얹는 얇은 wrapper가 된다.
|
|
51
|
+
지금은 이 압축 형태를 요구하는 화면이 없으므로, 순전히 미관상의 이유로 그 wrapper를
|
|
52
|
+
먼저 만들지 않는다 — 로드맵의 "측정된 요구가 나타나면 그때 기본을 재검토한다" 원칙과
|
|
53
|
+
`docs/notification.md`/`docs/dropdown.md`가 이미 세운 관례를 그대로 따른다.
|
|
54
|
+
|
|
55
|
+
## 제품이 지금 composing할 때 지킬 것 (판정이 이대로 유지되는 동안)
|
|
56
|
+
|
|
57
|
+
- 값은 각 Select의 committed key를 그대로 쓰되, 최종 시각은 **문자열** `"HH:mm"`
|
|
58
|
+
(24시간제, 앞자리 0)로 합성한다. `Date` 객체를 어디에도 들이지 않는다 — DatePicker와
|
|
59
|
+
같은 이유(시간대·서머타임을 이 패키지가 가지면 안 된다).
|
|
60
|
+
- **초 단위는 넣지 않는다.** 측정된 요구가 없고, 두 Select를 세 Select로 늘리는 것도
|
|
61
|
+
같은 근거로 보류한다.
|
|
62
|
+
- **12시간제(오전/오후)는 넣지 않는다.** 24시간제 값 위에 표시만 로케일별로 다르게
|
|
63
|
+
하고 싶다면 그건 제품이 표시 문자열을 포맷하는 몫이다(Statistic 원칙) — 값 자체에
|
|
64
|
+
`period` 축을 추가하지 않는다.
|
|
65
|
+
|
|
66
|
+
## 판정이 뒤집힐 조건
|
|
67
|
+
|
|
68
|
+
실제 화면이 **트리거 하나 + 팝업 하나**의 압축된 형태를 구체적으로 요구하면(예: 경기
|
|
69
|
+
알림 시각 설정처럼 공간이 좁은 자리), 그때 얇은 `time-picker.ts`를 연다. 그 경우 권장
|
|
70
|
+
설계:
|
|
71
|
+
|
|
72
|
+
- `hours`/`minutes` 각 열은 `collection.ts`의 `CollectionItemDescriptor`/
|
|
73
|
+
`validateCollection`/`getCollectionNavigationTarget`/`getCollectionTypeaheadMatch`를
|
|
74
|
+
**그대로 재사용**한다 — 여기서 재정의할 것이 없다.
|
|
75
|
+
- 트리거/오버레이 축(`open`/`defaultOpen`/`onOpenChange`, 라벨)은 `DatePickerOpenState`/
|
|
76
|
+
`DatePickerLabel`과 같은 모양으로 새 파일에 다시 선언한다(자급자족 원칙 — DatePicker가
|
|
77
|
+
`collection.ts`의 Select 타입을 그대로 가져오지 않고 같은 모양을 다시 선언한 것과
|
|
78
|
+
같은 이유).
|
|
79
|
+
- `dismiss`에서 `"selection"`을 뺀다 — 여기서만 다른 예외이므로 반드시 문서에
|
|
80
|
+
남긴다. 대신 명시적 `"confirm"` 사유를 추가한다.
|
|
81
|
+
- 초·12시간제 축은 이 조건이 왔다고 해서 자동으로 열리지 않는다 — 그 축은 별도로
|
|
82
|
+
다시 측정한다.
|
|
83
|
+
|
|
84
|
+
## 배선 명세 (리드 참고)
|
|
85
|
+
|
|
86
|
+
catalog의 `{ name: "TimePicker", category: "input", platform: "adaptive", status:
|
|
87
|
+
"planned" }`(`src/catalog.ts:69`) 행은 바꿀 것이 없다 — recipe/behavior가 원래 없었고,
|
|
88
|
+
지금도 없다. `src/time-picker.ts`, `test/time-picker.test.ts`는 만들지 않았다.
|
package/docs/timeline.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Timeline contract
|
|
2
|
+
|
|
3
|
+
## 문제
|
|
4
|
+
|
|
5
|
+
이미 일어난 일들을 시간 순서로 보여준다 — 야잘알의 경기 플레이 기록(PBP, "1회 초 안타 →
|
|
6
|
+
도루 → 득점")과 구단의 영입·유출 이력이 같은 문제를 각자 화면에서 풀고 있다. 둘 다
|
|
7
|
+
"무슨 일이 언제 일어났는가"의 기록이며, 사용자가 다음에 무엇을 해야 하는지는 말하지
|
|
8
|
+
않는다.
|
|
9
|
+
|
|
10
|
+
## Steps와의 경계
|
|
11
|
+
|
|
12
|
+
같은 저장소에 이미 있는 `Steps`([[steps]])와 겉모습이 비슷해 보이기 쉽다 — 둘 다 순서가
|
|
13
|
+
있는 항목 목록에 마커와 커넥터를 그린다. 그러나 두 계약이 푸는 문제는 반대 방향이다.
|
|
14
|
+
|
|
15
|
+
| | Steps | Timeline |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| 무엇을 보여주는가 | 흐름의 어디에 서 있는지 | 이미 일어난 일의 기록 |
|
|
18
|
+
| "현재 위치" | 항상 있다(`currentStepId`) | 없을 수도 있다 — PBP는 마지막 항목이 "지금"이지만, 구단 영입 이력에는 커서 개념이 없다 |
|
|
19
|
+
| 상태 유도 | `pending/current/complete/error`를 커서 위치로 **유도**한다 | 유도하지 않는다 — 모든 항목이 이미 "일어난 일"이고, `tone`은 위치가 아니라 제품이 항목마다 직접 붙인다 |
|
|
20
|
+
| 앞으로 갈 곳 | 있다(pending 단계) | 없다 — 기록은 뒤로만 자란다 |
|
|
21
|
+
| connector 색 | `reached`/`unreached` 두 값(커서 기준) | 한 값뿐 — 커서가 없으니 "아직 안 닿음"이 성립하지 않는다 |
|
|
22
|
+
|
|
23
|
+
이 표가 실제로 다른 타입으로 이어진다: `TimelineItemDescriptor`에는 `StepItemDescriptor`의
|
|
24
|
+
`status` 유도가 없고, `timelineRecipe.connector.tone`은 `stepsRecipe.connector.tone`의
|
|
25
|
+
`reached | unreached` 레코드가 아니라 `ColorReference` 하나다. 두 계약이 겹쳤다면 둘 중
|
|
26
|
+
하나는 필요 없었을 것이다 — 겹치지 않으므로 둘 다 남긴다.
|
|
27
|
+
|
|
28
|
+
## 일반화한 계약
|
|
29
|
+
|
|
30
|
+
### collection 기본 계약과의 대응
|
|
31
|
+
|
|
32
|
+
각 항목은 stable string `id`와 보이는 `label`을 가진다(Collection 기본 계약의 최소
|
|
33
|
+
부분집합, [[collection]] 참고). `textValue`, `none|single|multiple` selection mode,
|
|
34
|
+
`idle|loading|loadingMore|empty|error` async 상태는 Steps와 같은 이유로 가져오지 않는다 —
|
|
35
|
+
Timeline은 검색/타이핑 탐색 대상이 아니고, 사용자가 고르는 대상도 아니다. 비동기 로딩은
|
|
36
|
+
이 계약이 직접 다루지 않는다 — 항목이 뒤로(과거로) 더 늘어나야 하면 기존 항목을 그대로
|
|
37
|
+
둔 채 다음 페이지만 요청하는 문제이므로, List가 그렇듯 `LoadMore`([[load-more]])와
|
|
38
|
+
합성한다. Timeline 자체가 `idle|loading|empty` 축을 새로 만들지 않는다 — 항목이 하나도
|
|
39
|
+
없는 순간은 제품이 Timeline을 아예 마운트하지 않고 `EmptyState`로 대신한다(Statistic
|
|
40
|
+
그룹이 빈 배열을 던지는 것과 같은 판단, [[statistic]]).
|
|
41
|
+
|
|
42
|
+
### 항목의 시각·설명은 제품이 포맷한 문자열이다
|
|
43
|
+
|
|
44
|
+
`timestamp`(예: "3회 초", "2024-01-15")와 `description`은 모두 선택 필드이고 제품이 이미
|
|
45
|
+
포맷을 끝낸 문자열이다. Timeline은 날짜·이닝 연산을 하지 않는다(Statistic이 숫자를
|
|
46
|
+
포맷하지 않는 것과 같은 경계, [[statistic]]).
|
|
47
|
+
|
|
48
|
+
### 순서를 접근성 이름에 남긴다
|
|
49
|
+
|
|
50
|
+
`resolveTimelineDescriptor`는 각 항목에 `position`(1-based), `total`, 그리고 제품이 공급한
|
|
51
|
+
`composeAccessibleName({ position, total, label })`으로 만든 `accessibleName`을 붙인다 —
|
|
52
|
+
Steps와 정확히 같은 이유(RN에는 순서를 알려주는 기본 semantics가 없고, 한국어/영어
|
|
53
|
+
어순이 다르다)로 같은 해법을 재사용한다. 다만 Steps처럼 상태 문구를 별도로 얹지 않는다
|
|
54
|
+
— Timeline에는 얹을 상태가 없다. `timestamp`/`description`은 그대로 통과시켜 renderer가
|
|
55
|
+
보충 텍스트로 쓴다.
|
|
56
|
+
|
|
57
|
+
### dot tone은 공용 의미만 갖는다
|
|
58
|
+
|
|
59
|
+
`TimelineItemTone`은 `neutral | info | success | attention` 넷뿐이다. `warning`과
|
|
60
|
+
`danger`는 뺐다 — Timeline 항목은 실패나 경고를 표시하는 자리가 아니라 이미 일어난
|
|
61
|
+
사실을 분류하는 자리이고, 두 사용처(PBP, 구단 이력) 모두 위험/경고를 표현할 필요가
|
|
62
|
+
없었다. 제품 전용 색(구단 색 등)은 adapter가 이 넷 중 하나로 먼저 매핑한다
|
|
63
|
+
(`identity.md`의 제품 매핑 원칙).
|
|
64
|
+
|
|
65
|
+
### 양방향 배치는 넣지 않는다
|
|
66
|
+
|
|
67
|
+
Ant Design Timeline의 `mode="alternate"`(항목이 좌우로 번갈아 배치)는 계약에 없다. 이건
|
|
68
|
+
넓은 Web 화면의 장식적 여유 공간을 쓰는 패턴이고, 좁은 세로 스크롤 목록인 Native에는
|
|
69
|
+
대응 개념이 없다 — 왼쪽/오른쪽이라는 방향 자체가 성립하지 않는다. 강제로 Native
|
|
70
|
+
버전을 만들면 항목마다 좌우 정렬이 바뀌는 것을 읽는 순서로 오인하게 만들 위험도 있다.
|
|
71
|
+
계약은 항상 한 방향(세로, 위→아래)만 표현한다.
|
|
72
|
+
|
|
73
|
+
## HJM 기본값
|
|
74
|
+
|
|
75
|
+
- `itemTone` 기본값은 `"neutral"`(`timelineDefaults.itemTone`).
|
|
76
|
+
- dot은 `diameter: 10`, `borderWidth: stroke.default`. `neutral`은 Steps의 `pending`
|
|
77
|
+
마커처럼 테두리가 없고(`border: null`) `content.secondary`로만 채운다. `info/success/
|
|
78
|
+
attention`은 Badge의 tone 3단 구성(테두리+채움)을 재사용해, 항목의 시각 표시가 색만이
|
|
79
|
+
아니라 테두리 유무로도 구분된다 — 다만 Steps의 마커와 달리 Timeline dot은 항목의
|
|
80
|
+
유일한 의미 전달자가 아니다(label 텍스트가 항상 있다), 그래서 숫자·체크·에러 같은
|
|
81
|
+
전용 글리프는 넣지 않는다.
|
|
82
|
+
- `connector.tone`은 항상 `semanticColors.border.default` 하나다. Steps의
|
|
83
|
+
`reached/unreached` 구분이 성립하려면 커서가 있어야 하는데 Timeline에는 없다.
|
|
84
|
+
- 텍스트(`label`/`timestamp`/`description`)는 Steps·Statistic과 같은 4.5:1 기준을
|
|
85
|
+
만족하는 기존 색 참조만 쓴다. dot의 `fill` 색은 3:1(non-text) 기준을 만족한다(test로
|
|
86
|
+
분리 검증).
|
|
87
|
+
|
|
88
|
+
## 플랫폼 번역
|
|
89
|
+
|
|
90
|
+
- Web: 목록 시맨틱을 실제로 사용한다 — `root`는 `<ol>`(순서가 있는 목록), 각 `item`은
|
|
91
|
+
`<li>`. dot과 connector는 `aria-hidden`(장식이며 label/timestamp 텍스트가 이미 정보를
|
|
92
|
+
전달한다). 각 항목의 접근 가능한 이름은 `accessibleName`(순서+label) 하나이고,
|
|
93
|
+
`timestamp`/`description`은 같은 항목 안의 보이는 텍스트로 함께 낭독된다 — 별도로
|
|
94
|
+
감추지 않는다.
|
|
95
|
+
- Native(RN): `<ol>/<li>` 동등물이 없으므로 각 item root에
|
|
96
|
+
`accessibilityLabel=accessibleName`을 명시적으로 달아 순서를 보존한다(Steps가 RN의
|
|
97
|
+
`aria-current` 부재를 `accessibilityHint`로 메우는 것과 같은 종류의 처리, 다만
|
|
98
|
+
Timeline은 보충 상태가 아니라 순서 자체를 보존하는 것이 목적이다). dot과 connector는
|
|
99
|
+
`accessibilityElementsHidden`/`importantForAccessibility="no"`.
|
|
100
|
+
- Reduce Motion: 항목이 새로 추가될 때(예: PBP 실시간 갱신) 등장 애니메이션은 즉시
|
|
101
|
+
전환 또는 짧은 opacity로 대체한다. 이동·반복 모션은 두지 않는다.
|
|
102
|
+
|
|
103
|
+
## 공개한 축 / 배제한 축
|
|
104
|
+
|
|
105
|
+
| 축 | 상태 |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| 순서(`position`/`total`) + 접근 가능한 이름 | 공개 |
|
|
108
|
+
| dot tone(`neutral/info/success/attention`) | 공개 — `warning`/`danger` 배제(위 근거) |
|
|
109
|
+
| `textValue`, selection mode, async 상태 | **배제** — Steps와 같은 이유(검색/선택/비동기 목록 대상이 아님). 비동기 페이지네이션이 필요하면 `LoadMore`와 합성한다 |
|
|
110
|
+
| `pending/current/complete/error` 상태 유도 | **배제** — Timeline에는 커서가 없다(위 Steps 경계 표) |
|
|
111
|
+
| 양방향(alternate) 배치 | **배제** — Web 장식이고 Native에 대응이 없다(위 근거) |
|
|
112
|
+
| 항목별 커스텀 아이콘 | **배제** — 측정된 요구가 없고, dot은 보조 표시일 뿐이라 임의 아이콘까지 허용할 필요가 없다 |
|
|
113
|
+
|
|
114
|
+
## 보조 배지와의 경계 — 새 축이 아니라 Tag와의 조합
|
|
115
|
+
|
|
116
|
+
야잘알 라이브 화면의 플레이 기록(`modules/app-rn/src/features/live/LiveScreen.tsx:797-821`,
|
|
117
|
+
`PlayLogRowView`)이 이 계약의 실사용처를 이미 보여 준다 — "일어난 일을 순서대로, 커서
|
|
118
|
+
없이" 보여주는 문제와 정확히 일치한다. 그런데 이 화면은 각 항목에 아웃 카운트
|
|
119
|
+
(`1사`/`2사`, 3아웃이면 배지 자체를 생략)를 짧은 배지로 붙인다
|
|
120
|
+
(`badge={<AppBadge label={outLabel} />}`, `LiveScreen.tsx:820`).
|
|
121
|
+
|
|
122
|
+
이걸 보고 처음 든 질문은 "`TimelineItemDescriptor`에 `badge` 필드를 추가해야 하는가"였다.
|
|
123
|
+
답은 아니다 — 이 배지가 요구하는 것은 **정적 메타데이터 한 조각, 상호작용 없음, tone
|
|
124
|
+
있음**이고, 이건 이미 `Tag`([[tag]])가 소유한 문제와 글자 그대로 같다
|
|
125
|
+
(`docs/tag.md`: "화면에 반복해서 등장하는 한 조각의 정적 메타데이터... 누를 수 없고,
|
|
126
|
+
선택되지도 않고, 지워지지도 않는다"). `TimelineItemDescriptor`에 별도 `badge`/`count` 축을
|
|
127
|
+
추가하면, 같은 "정적 라벨+톤"을 Tag와 Timeline 두 계약이 서로 다른 이름으로 갖게 된다 —
|
|
128
|
+
이 저장소가 반복해서 피해 온 바로 그 실수(Dropdown이 Menu와, Chip의 `closable`이 Tag의
|
|
129
|
+
`selected`와 같은 자리를 두 번 계약할 뻔했던 것)와 같은 자리다.
|
|
130
|
+
|
|
131
|
+
그래서 이 계약이 내리는 판단은: **Timeline은 항목별 보조 배지를 위한 새 필드를 열지
|
|
132
|
+
않는다.** 필요하면 렌더러가 `content` 슬롯 안에 `Tag`를 조합한다 — Carousel이 슬라이드
|
|
133
|
+
내부 콘텐츠를 렌더링하지 않고 제품에 맡기는 것과 같은 경계다. 지금 야잘알 코드가
|
|
134
|
+
`AppBadge`를 쓰는 것도 사실은 이 조합이 필요한 자리에 아직 `Tag`가 없어서 `Badge`를
|
|
135
|
+
대신 쓰고 있는 것이다(`docs/tag.md`가 `FaCenterScreen.tsx`의 등급 배지에도 같은 진단을
|
|
136
|
+
내렸다) — Tag가 승격되면 이 자리도 자연히 Tag로 옮겨갈 자리이지, Timeline이 새 축을
|
|
137
|
+
얻을 자리가 아니다.
|
|
138
|
+
|
|
139
|
+
## 검증 화면
|
|
140
|
+
|
|
141
|
+
`LiveScreen.tsx:797-821`의 PBP 플레이 기록이 실제로 존재하는 후보다 — "일어난 일,
|
|
142
|
+
순서, 커서 없음"이라는 경계는 정확히 일치한다. 다만 지금은 `timelineRecipe`의 dot/
|
|
143
|
+
connector 시각이 아니라 평평한 `AppListRow` 목록으로 그려져 있어, 시각 recipe 쪽
|
|
144
|
+
vertical slice는 아직 없다. `planned → beta` 승격은 실제 제품 vertical slice 이후
|
|
145
|
+
리드가 진행한다(로드맵 maturity gate). 구단 상세의 영입·유출 이력은 여전히 미확인
|
|
146
|
+
후보로 남긴다.
|