@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,87 @@
|
|
|
1
|
+
# 컴포넌트 저작 브리프 — planned → 계약·recipe·행동 완성
|
|
2
|
+
|
|
3
|
+
이 문서는 planned 컴포넌트를 구현 가능한 상태로 끌어올리는 저작자(에이전트)의 작업
|
|
4
|
+
계약이다. 사람이 유지보수하는 문서이기도 하므로, 규칙의 이유를 함께 적는다.
|
|
5
|
+
|
|
6
|
+
## 먼저 통독할 것 (순서대로)
|
|
7
|
+
|
|
8
|
+
1. `docs/identity.md` — 이 시스템의 문장("조용한 화면 위에 중요한 순간만 선명하게")
|
|
9
|
+
2. `docs/architecture.md` — 층 구조와 경계
|
|
10
|
+
3. `docs/expansion-roadmap.md` — **공통 상태 축, Collection 기본 계약, maturity gate.**
|
|
11
|
+
특히 「무엇을 흡수하는가」 표 — 외부 시스템에서 무엇을 가져오고 무엇을 가져오지
|
|
12
|
+
않는지가 이 저장소의 헌법이다.
|
|
13
|
+
4. `docs/ant-design-coverage.md` — antd는 **reference inventory**일 뿐이다. 외형, token
|
|
14
|
+
값, prop 이름, 전용 자산을 복사하지 않는다. 같은 사용자 문제를 HJM의 의미로 다시
|
|
15
|
+
푼다.
|
|
16
|
+
5. 본보기 모듈 **둘**: `src/statistic.ts`(+`test/statistic.test.ts`, `docs/statistic.md`) —
|
|
17
|
+
표현 계약의 본보기. `src/load-more.ts` — 행동 계약(컨트롤러)의 본보기.
|
|
18
|
+
|
|
19
|
+
## 산출물 — 컴포넌트당 세 파일
|
|
20
|
+
|
|
21
|
+
| 파일 | 내용 |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `src/<name>.ts` | 계약 전부: descriptor 타입, defaults, validator, resolver, 시각 recipe 토큰, 행동 시나리오. **한 모듈에 자급자족**으로 담는다 |
|
|
24
|
+
| `test/<name>.test.ts` | vitest. validator가 거부해야 할 입력, resolver의 경계 입력, 시나리오 불변식 |
|
|
25
|
+
| `docs/<name>.md` | 로드맵이 정한 기록 형식: **문제 → 일반화한 계약 → HJM 기본값 → 플랫폼 번역 → 검증 화면** |
|
|
26
|
+
|
|
27
|
+
### 왜 recipe·행동을 공용 레지스트리에 직접 쓰지 않는가
|
|
28
|
+
|
|
29
|
+
`component-recipes.ts`, `behaviors.ts`, `catalog.ts`, `recipes.ts`, `index.ts`는 모든
|
|
30
|
+
컴포넌트가 지나는 **공유 파일**이다. 여러 저작자가 병렬로 이 파일들을 고치면 서로의
|
|
31
|
+
작업을 덮어쓴다. 그래서:
|
|
32
|
+
|
|
33
|
+
- 저작자는 **자기 모듈 파일 안에** recipe와 행동까지 export한다.
|
|
34
|
+
- 보고서에 **배선 명세**를 적는다 — catalog 한 줄(카테고리·platform·recipe 키·behavior
|
|
35
|
+
키), `index.ts`에 내보낼 심볼 목록, 공용 레지스트리로 옮길 것이 있으면 그 목록.
|
|
36
|
+
- 배선은 리드가 순차로 적용한다.
|
|
37
|
+
|
|
38
|
+
이 방식이면 저작자의 `pnpm check`는 자기 파일만으로 통과하고(아무도 아직 그 모듈을
|
|
39
|
+
참조하지 않으므로), 충돌이 구조적으로 불가능하다.
|
|
40
|
+
|
|
41
|
+
## 계약이 지켜야 할 것
|
|
42
|
+
|
|
43
|
+
- **상태 축은 로드맵의 표에서 고른다.** 필요한 축만 공개한다 — Link가 disabled를
|
|
44
|
+
지원하지 않는 것처럼, 지원하지 않기로 한 축은 문서에 이유와 함께 적는다.
|
|
45
|
+
- **collection이 있으면 Collection 기본 계약을 따른다** — stable string `id`, `label`과
|
|
46
|
+
`textValue`, `none|single|multiple`, `idle|loading|loadingMore|empty|error`.
|
|
47
|
+
새 데이터 모델을 만들지 않는다.
|
|
48
|
+
- **제품이 포맷한 문자열을 받는다.** 숫자·날짜·단위 포맷은 제품 소유다(Statistic 참고).
|
|
49
|
+
- **validator는 던진다.** 빈 label, 중복 id, 성립하지 않는 조합은 조용히 넘기지 않고
|
|
50
|
+
`TypeError`/`RangeError`로 거부한다. 그리고 **그 validator가 잘못 잡는 입력으로 먼저
|
|
51
|
+
시험한다** — 허락해야 할 것까지 막으면 잘못 만든 것이다.
|
|
52
|
+
- **접근성은 계약의 일부다.** 각 상태가 Web keyboard/aria와 RN accessibilityState로
|
|
53
|
+
어떻게 번역되는지 모듈이 명시한다. 색으로만 말하는 상태를 만들지 않는다.
|
|
54
|
+
- **런타임 의존성 금지.** 이 패키지는 React도 RN도 import하지 않는다. 타입과 순수
|
|
55
|
+
함수만 있다. renderer는 제품(BurnTok Web, Yajalal RN)이 소유한다.
|
|
56
|
+
|
|
57
|
+
## maturity에 대해
|
|
58
|
+
|
|
59
|
+
당신의 산출물로 컴포넌트는 **"계약+recipe 준비됨"**이 된다. catalog의 `planned → beta`
|
|
60
|
+
승격은 실제 제품 vertical slice 검증 후 리드가 한다 — 로드맵의 gate가 그렇게 정했고,
|
|
61
|
+
시각 recipe만으로 구현 완료를 주장하지 않는 것이 이 저장소의 원칙이다.
|
|
62
|
+
|
|
63
|
+
## 게이트
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd /Users/jimin/Desktop/hjm-design-system
|
|
67
|
+
pnpm typecheck && pnpm test
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`pnpm build`는 dist를 다시 쓰므로 저작자는 돌리지 않는다(dist는 리드가 배선 후 한 번에).
|
|
71
|
+
**기존 파일을 수정하지 않는다** — 이 저장소에는 커밋되지 않은 진행 중 변경이 있다.
|
|
72
|
+
당신이 만드는 세 파일 외에는 읽기 전용이다.
|
|
73
|
+
|
|
74
|
+
## 보고 형식
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
## <컴포넌트>
|
|
78
|
+
- 문제: <이 컴포넌트가 푸는 사용자 문제 한 문장>
|
|
79
|
+
- antd 대응: <source entry와 relationship — crosswalk와 일치해야 한다>
|
|
80
|
+
- 공개한 상태 축: <축과 값. 지원하지 않기로 한 축과 이유>
|
|
81
|
+
- 배선 명세:
|
|
82
|
+
- catalog: { name, category, platform, recipe: "<키>", behavior: "<키>" | 없음 }
|
|
83
|
+
- index 내보낼 심볼: <목록>
|
|
84
|
+
- 공용 레지스트리 이동 대상: <있으면>
|
|
85
|
+
- 판단이 갈렸던 자리: <대안과 택한 이유>
|
|
86
|
+
- 게이트: typecheck <결과> / test <N passed>
|
|
87
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# BorderBeam — 새 컴포넌트를 만들지 않는다
|
|
2
|
+
|
|
3
|
+
## 정정
|
|
4
|
+
|
|
5
|
+
이 문서의 이전 판은 `BorderBeam`이 Ant Design 컴포넌트가 아니라 Magic UI/Aceternity
|
|
6
|
+
계열의 장식 컴포넌트가 crosswalk에 잘못 섞여 들어온 것이라고 주장했다. **그 주장은
|
|
7
|
+
사실이 아니었다** — antd의 실제 Components Overview를 직접 확인하지 않고 일반
|
|
8
|
+
웹검색만으로 판단한 결과였고, 검색 결과가 더 대중적인 Magic UI/Aceternity 쪽으로
|
|
9
|
+
쏠려 있어 antd가 별도로 같은 이름의 컴포넌트를 추가했을 가능성을 놓쳤다.
|
|
10
|
+
|
|
11
|
+
리드가 공식 Overview를 직접 확인해 정정했다: `BorderBeam`은 antd "Other" 섹션에
|
|
12
|
+
실재하는 컴포넌트다. `Watermark`도 crosswalk에서 빠지지 않았다 —
|
|
13
|
+
`src/component-references.ts:123`에 `category: "feedback"`으로 이미 정확히 잡혀
|
|
14
|
+
있다(antd에서 Watermark는 Other가 아니라 Feedback 섹션이다). 섹션별 수(general 4 /
|
|
15
|
+
layout 7 / navigation 7 / data-entry 18 / data-display 21 / feedback 11 / other 5 =
|
|
16
|
+
73)도 저장소 테스트의 단정과 전부 일치한다. `component-references.ts:127`의
|
|
17
|
+
`BorderBeam` 행은 **정확하다** — 제거하지 않는다.
|
|
18
|
+
|
|
19
|
+
## 판정: 그럼에도 만들지 않는다
|
|
20
|
+
|
|
21
|
+
crosswalk이 맞다는 것과 이 컴포넌트를 지금 만들 것인가는 별개 질문이다. `BorderBeam`이
|
|
22
|
+
실제로 antd에 있다 해도, 그 실체 — 사용자 행동에 반응하지 않는 **상시 반복 이동
|
|
23
|
+
애니메이션**(컨테이너 테두리를 따라 빛줄기가 도는 효과) — 은 `docs/identity.md`와
|
|
24
|
+
정면으로 충돌한다.
|
|
25
|
+
|
|
26
|
+
- 첫 문장부터 "HJM은 장식으로 브랜드를 증명하지 않습니다."
|
|
27
|
+
- Motion 원칙: "Reduce Motion에서는 이동과 반복을 제거하고 즉시 전환 또는 짧은 opacity로
|
|
28
|
+
대체합니다", "bounce와 spring은 공간 관계를 설명할 때만 사용합니다."
|
|
29
|
+
- HJM답지 않은 패턴: "브랜드색을 장식 배경처럼 넓게 사용."
|
|
30
|
+
|
|
31
|
+
두 질문으로 검증했다:
|
|
32
|
+
|
|
33
|
+
1. **Reduce Motion에서 무엇이 남는가?** 이동 자체가 이 컴포넌트의 전부이므로, 이동을
|
|
34
|
+
제거하면 정적인 테두리 선(또는 아무것도) 만 남는다 — "즉시 전환"으로 대체할 상태
|
|
35
|
+
변화가 애초에 없다(열림/닫힘/포커스 같은 원인이 없다).
|
|
36
|
+
2. **장식이 없어도 화면이 같은 뜻을 전하는가?** 그렇다 — 이 컴포넌트는 어떤 상태·값·
|
|
37
|
+
선택도 표현하지 않는다. 있으나 없으나 화면이 말하는 내용은 똑같고, 차이는 순전히
|
|
38
|
+
"화려함"뿐이다.
|
|
39
|
+
|
|
40
|
+
두 질문 모두 "이 장식은 정보를 나르지 않는다"로 귀결됐다 — identity가 정확히 배제하는
|
|
41
|
+
자리(장식으로 증명하는 브랜드, 원인 없는 반복 모션)다. 이 세 근거는 crosswalk 출처
|
|
42
|
+
문제와 무관하게 그대로 성립한다.
|
|
43
|
+
|
|
44
|
+
## 결론
|
|
45
|
+
|
|
46
|
+
`src/border-beam.ts`, `test/border-beam.test.ts`는 만들지 않는다.
|
|
47
|
+
|
|
48
|
+
## 판정이 뒤집힐 조건
|
|
49
|
+
|
|
50
|
+
BurnTok/Yajalal 중 하나가 실제로 "강조해야 하는 순간"(예: 실시간 방송 중임을 알리는
|
|
51
|
+
라이브 표시, 당첨/축하 모멘트)에 은은한 강조 테두리가 필요하다고 판단하면, 그건
|
|
52
|
+
`BorderBeam`(상시 반복 장식)이 아니라 그 순간에 한정된 **의미 있는 강조 recipe**(예:
|
|
53
|
+
`feedback.attention` 톤의 짧은 1회성 pulse, Toast의 `priority: high`처럼 특정 상태에만
|
|
54
|
+
묶인 것)로 다시 계약해야 한다.
|
|
55
|
+
|
|
56
|
+
## 배선 명세 (리드 적용)
|
|
57
|
+
|
|
58
|
+
`src/component-references.ts:127`의 `BorderBeam` crosswalk 행은 **정확하므로 바꾸지
|
|
59
|
+
않는다.** `src/catalog.ts`의 `{ name: "BorderBeam", category: "utility", platform:
|
|
60
|
+
"web", status: "planned" }` 행만 대상이다 — 이 행은 "만들 계획"을 뜻하는 `planned`인데
|
|
61
|
+
실제로는 "만들지 않기로 확정"이라 상태가 거짓말을 하고 있다. 이 불일치를 어떻게
|
|
62
|
+
표현할지는 `docs/catalog-decision-status.md`에서 별도로 다룬다.
|
|
63
|
+
|
|
64
|
+
## 출처
|
|
65
|
+
|
|
66
|
+
- [Ant Design — Components Overview](https://ant.design/components/overview/)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# BottomNavigation contract
|
|
2
|
+
|
|
3
|
+
`BottomNavigation`은 콘텐츠 panel을 바꾸는 `Tabs`가 아니라 앱의 안정된 최상위 route를
|
|
4
|
+
이동합니다. 같은 destination 의미를 Web의 link와 React Native navigator tab으로 적응시키며,
|
|
5
|
+
route state는 제품 router 한 곳에서만 소유합니다. BurnTok Web/RN과 Yajalal RN의 실제
|
|
6
|
+
navigation renderer에서 route lifecycle·접근성·큰 글자·safe area를 검증해 catalog status는
|
|
7
|
+
`beta`입니다.
|
|
8
|
+
|
|
9
|
+
## Descriptor와 configuration
|
|
10
|
+
|
|
11
|
+
descriptor에는 2–6개 destination, router가 확정한 `selectedKey`, navigation landmark 이름만
|
|
12
|
+
둡니다. item icon은 label과 의미가 중복되므로 이름과 `decorative: true`만 허용하는
|
|
13
|
+
`BottomNavigationIconDescriptor`를 사용합니다. 크기·tone·weight·RTL 방향은 recipe와
|
|
14
|
+
semantic Icon registry가 소유하며 호출부에서 덮어쓸 수 없습니다.
|
|
15
|
+
route path, React component, navigation callback, 생성 action은 descriptor에 넣지 않습니다.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
const descriptor = {
|
|
19
|
+
accessibilityLabel: "주요 탐색",
|
|
20
|
+
selectedKey: currentRoute,
|
|
21
|
+
items: [
|
|
22
|
+
{ id: "home", label: "홈", icon: { name: "home" } },
|
|
23
|
+
{
|
|
24
|
+
id: "messages",
|
|
25
|
+
label: "메시지",
|
|
26
|
+
icon: { name: "notifications" },
|
|
27
|
+
badge: {
|
|
28
|
+
count: unreadCount,
|
|
29
|
+
accessibilityLabel: `읽지 않은 메시지 ${unreadCount}개`,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
} satisfies BottomNavigationDescriptor;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
시각·플랫폼 선택은 별도 `BottomNavigationConfiguration`으로 전달합니다.
|
|
37
|
+
|
|
38
|
+
- `presentation`: `bar | floating`
|
|
39
|
+
- `distribution`: `equal | center-gap`
|
|
40
|
+
- `density`: `compact | regular`
|
|
41
|
+
- `direction`: `ltr | rtl`
|
|
42
|
+
- `keyboardBehavior`: `hide | remain`
|
|
43
|
+
|
|
44
|
+
두 renderer 모두 `resolveBottomNavigationConfiguration(configuration, itemCount)`를 사용합니다.
|
|
45
|
+
`center-gap`은 짝수 destination에서만 유효합니다. 이 gap은 별도 primary action의 시각적 자리만
|
|
46
|
+
예약하며 action을 collection에 추가하지 않습니다.
|
|
47
|
+
|
|
48
|
+
Web renderer도 이름이 있는 실제 link landmark, `aria-current`, modifier click 보존,
|
|
49
|
+
keyboard viewport hide와 center-gap 배치를 first-party SSR·browser test로 검증하므로 Web
|
|
50
|
+
surface 역시 `beta`입니다. 제품 router와 결합한 브라우저 릴리스 증거는 stable 승격 전
|
|
51
|
+
debt로 남습니다.
|
|
52
|
+
|
|
53
|
+
## Route source of truth
|
|
54
|
+
|
|
55
|
+
`selectedKey`는 controlled/uncontrolled selection API가 아니라 read-only input입니다.
|
|
56
|
+
`resolveBottomNavigationActivation`은 `navigate | reselect` intent만 반환하고 값을 바꾸지 않습니다.
|
|
57
|
+
renderer는 router 또는 navigator에 intent를 전달하며, route 전환이 실제로 완료된 뒤 새
|
|
58
|
+
`selectedKey`를 받습니다. 인증 gate, preventDefault, navigation 실패가 있으면 기존 selected
|
|
59
|
+
상태를 그대로 유지합니다. 각 destination의 nested stack과 scroll state도 navigator가
|
|
60
|
+
보존하며 renderer가 screen을 조건부 remount하지 않습니다.
|
|
61
|
+
|
|
62
|
+
## Badge announcement
|
|
63
|
+
|
|
64
|
+
item의 기본 접근성 이름은 `item.accessibilityLabel ?? item.label`입니다. badge count가 0보다
|
|
65
|
+
크면 resolver가 이 이름과 `badge.accessibilityLabel`을 한 번만 합쳐
|
|
66
|
+
`resolvedAccessibilityLabel`로 반환합니다. visible badge는 `99+`처럼 CounterBadge 규칙으로
|
|
67
|
+
제한할 수 있지만 실제 접근성 copy는 제품이 현지화합니다.
|
|
68
|
+
|
|
69
|
+
resolved badge에는 `hiddenFromAccessibility: true`와 visible label만 있습니다. Web은 badge에
|
|
70
|
+
`aria-hidden="true"`, RN은 badge subtree에 `accessible={false}`와 해당 플랫폼의 descendant
|
|
71
|
+
hide 설정을 적용하고 item root에 `resolvedAccessibilityLabel`만 전달합니다. badge 자체에
|
|
72
|
+
status/live role이나 별도 accessibility label을 추가하면 같은 정보가 두 번 낭독되므로
|
|
73
|
+
금지합니다. polling으로 count가 바뀌어도 focus, selection, live announcement를 만들지 않습니다.
|
|
74
|
+
|
|
75
|
+
## Adaptive renderer semantics
|
|
76
|
+
|
|
77
|
+
### Web
|
|
78
|
+
|
|
79
|
+
- 컴포넌트 root 자체가 이름이 있는 `nav` landmark이고 list 안에 실제 link를 렌더링합니다.
|
|
80
|
+
`footer`/contentinfo를 자체 생성하지 않으므로 `Layout`의 `footer` slot에 합성해도
|
|
81
|
+
`<footer>` 안에 `<footer>`가 중첩되지 않습니다.
|
|
82
|
+
- 현재 link는 `aria-current="page"`를 사용합니다.
|
|
83
|
+
- browser의 Tab/Enter, modifier click, context menu, 새 탭 열기를 보존합니다.
|
|
84
|
+
- `tab`/`tablist` role, roving focus, 방향키 navigation을 적용하지 않습니다.
|
|
85
|
+
- SPA router를 사용해도 link의 기본 의미를 button으로 바꾸지 않습니다.
|
|
86
|
+
|
|
87
|
+
### React Native
|
|
88
|
+
|
|
89
|
+
- 각 destination은 현지화된 label과 `selected`/`disabled` state를 가집니다. Android와
|
|
90
|
+
지원되는 renderer는 tab role을 사용합니다. iOS에서 navigator가 tab role을 안정적으로
|
|
91
|
+
발표하지 못하면 button role과 selected state를 함께 제공하는 플랫폼 fallback을 허용합니다.
|
|
92
|
+
지원되지 않는 role 문자열을 억지로 주입하는 것보다 실제 VoiceOver 발표를 우선합니다.
|
|
93
|
+
- press 시 navigator의 preventable `tabPress`를 먼저 emit하고, 막히지 않았을 때만 navigate합니다.
|
|
94
|
+
- `tabLongPress`와 test ID 같은 navigator option을 renderer까지 전달합니다.
|
|
95
|
+
- navigator route collection과 scene lifecycle을 보존하며 별도 placeholder route를 만들지 않습니다.
|
|
96
|
+
|
|
97
|
+
## Visual and layout requirements
|
|
98
|
+
|
|
99
|
+
- inactive icon/label도 필수 정보이므로 `content.secondary` 이상을 사용합니다.
|
|
100
|
+
- `indicator` slot은 icon target과 badge의 안정된 layout anchor일 뿐이며 selected pill을 그리지
|
|
101
|
+
않습니다(`visual: none`, `background`/`border`: `null`). renderer는 이 null paint를 플랫폼의
|
|
102
|
+
transparent 값으로 번역합니다. selected 상태는 label weight와 icon
|
|
103
|
+
emphasis를 함께 바꿔 색 하나에 의존하지 않습니다. stroke를 제어할 수 있는 icon adapter는
|
|
104
|
+
`strokeWidth`, 그렇지 않은 adapter는 `scale` 중 최소 하나를 recipe 값으로 적용합니다.
|
|
105
|
+
- selected icon과 label의 색 intent는 모두 `content.brand`이며 renderer는 각각
|
|
106
|
+
`colors.selectedIcon`과 `colors.selectedLabel`을 소비합니다.
|
|
107
|
+
- keyboard focus ring은 item 바깥에 그려 selected icon/label evidence와 별개로 동시에 보이게
|
|
108
|
+
합니다.
|
|
109
|
+
- item target은 최소 44×44입니다. label은 항상 보이고 font scaling을 허용하며 고정 item 높이와
|
|
110
|
+
한 줄 clipping을 사용하지 않습니다. 모든 destination을 동시에 유지해야 하는 persistent chrome의
|
|
111
|
+
visual label은 최대 `1.4×`까지만 커지고, 원문 전체는 item의 접근성 이름으로 유지합니다.
|
|
112
|
+
- safe-area bottom inset은 recipe의 최소 padding에 더합니다. `max(base, inset)`으로 대체하지
|
|
113
|
+
않습니다.
|
|
114
|
+
- 기본 keyboard behavior는 `hide`입니다. software keyboard 위로 bottom navigation을 밀어
|
|
115
|
+
올려 입력 영역을 가리지 않습니다.
|
|
116
|
+
- RTL에서는 item 순서와 badge의 inline-end anchor가 함께 뒤집힙니다. icon 자체의 mirror 여부는
|
|
117
|
+
semantic Icon contract가 결정합니다.
|
|
118
|
+
- Reduce Motion에서는 transform을 제거해도 label weight evidence는 유지되며
|
|
119
|
+
selected/focus/route 상태와 press 결과는 같습니다.
|
|
120
|
+
|
|
121
|
+
## Centered primary action
|
|
122
|
+
|
|
123
|
+
BurnTok의 생성 버튼처럼 작업을 시작하는 control은 destination이 아닙니다. `center-gap`
|
|
124
|
+
distribution을 선택하고 outer Dock frame에서 기존 Button/IconButton 기반 action을 sibling으로
|
|
125
|
+
합성합니다. 이 action은 tab role, selected state, badge, `selectedKey`, destination count를
|
|
126
|
+
가질 수 없습니다. Web은 button, RN은 button role과 activate action을 사용하며 제품 router나
|
|
127
|
+
modal API를 직접 호출합니다.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Breadcrumb contract
|
|
2
|
+
|
|
3
|
+
**문제.** 깊은 계층 안 화면에서 지금 위치가 전체 구조 어디쯤인지 보여 주고, 상위 계층으로
|
|
4
|
+
곧장 돌아가게 합니다 — `구단 목록 › LG 트윈스 › 선수단`처럼 현재 화면까지의 경로입니다.
|
|
5
|
+
|
|
6
|
+
**일반화한 계약.** Collection 기본 계약의 stable `id`와 보이는 `label`만 가져옵니다.
|
|
7
|
+
`textValue`, 선택, 비동기 상태는 없습니다 — Breadcrumb는 선택하는 목록이 아니라 이미 온
|
|
8
|
+
경로를 보여 주는 목록입니다. 항목은 순서가 곧 계층이며, **배열의 마지막 항목만 현재
|
|
9
|
+
위치**이고 그 항목만 `destination`이 없습니다. 그 앞의 모든 항목은 `destination`이
|
|
10
|
+
필수입니다.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
const trail = {
|
|
14
|
+
items: [
|
|
15
|
+
{ id: "teams", label: "구단", destination: { kind: "internal", href: "/teams" } },
|
|
16
|
+
{ id: "lg", label: "LG 트윈스" },
|
|
17
|
+
],
|
|
18
|
+
} satisfies BreadcrumbDescriptor;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`validateBreadcrumbDescriptor`는 빈 trail, 중복 id, 빈 label, **마지막 항목의
|
|
22
|
+
destination**, **마지막이 아닌 항목의 missing destination**을 모두 거부합니다. 항목이
|
|
23
|
+
하나뿐인 trail(현재 화면만, 조상 없음)은 유효합니다.
|
|
24
|
+
|
|
25
|
+
## Link 목적지 재사용
|
|
26
|
+
|
|
27
|
+
Breadcrumb는 새 href 개념을 만들지 않습니다. 조상 항목의 `destination`은 `Link`의
|
|
28
|
+
`LinkDestination`(`internal | external`) 타입 그대로이고, `validateBreadcrumbDescriptor`는
|
|
29
|
+
각 조상 항목마다 `validateLinkDestination`을 그대로 호출합니다. 그래서 internal href가
|
|
30
|
+
`/`, `?`, `#`로 시작해야 한다거나 external href가 허용된 protocol만 써야 한다는 규칙은
|
|
31
|
+
Link 문서(`docs/link.md`)가 유일한 출처입니다. Breadcrumb 조상 항목은 Web에서는 실제
|
|
32
|
+
anchor, Native가 이 컴포넌트를 쓴다면 Expo Router Link로 렌더링될 항목이라는 뜻이며,
|
|
33
|
+
`Link`의 `disabled`/`onClick`/`onPress` 금지 규칙도 그대로 상속합니다.
|
|
34
|
+
|
|
35
|
+
## HJM 기본값
|
|
36
|
+
|
|
37
|
+
- 마지막 항목은 링크가 아니라 plain text이고 `aria-current="page"`로 표시합니다.
|
|
38
|
+
- 구분자(`/`, `›`)는 정보가 아니라 장식입니다. `breadcrumbRecipe.separator.decorative`는
|
|
39
|
+
항상 `true`이고 renderer는 이를 접근성 트리에서 숨깁니다(Web `aria-hidden`, 스크린
|
|
40
|
+
리더는 순서만 듣습니다).
|
|
41
|
+
- 구분자 아이콘은 `chevronEnd`처럼 Icon registry의 논리 방향 이름을 씁니다. RTL 미러링은
|
|
42
|
+
Icon 계약이 이미 소유하므로 Breadcrumb가 따로 방향을 계산하지 않습니다.
|
|
43
|
+
- **축약(`...`)을 넣지 않습니다.** 항목이 많을 때 가운데를 접는 것은 실제 화면에서
|
|
44
|
+
측정된 수요가 아직 없습니다. 필요해지면 별도 `collapsed` 축으로 명시적으로 추가하고,
|
|
45
|
+
지금은 renderer가 전체 trail을 그대로 그립니다.
|
|
46
|
+
- 크기는 Link의 inline 취급을 따릅니다 — 44-unit 최소 target을 강제하지 않고 밑줄과
|
|
47
|
+
focus indicator만 유지합니다. Breadcrumb 항목은 문장이 아니라 한 줄 경로이므로 독립된
|
|
48
|
+
standalone Link처럼 하나씩 별도 target으로 쓰기보다, 촘촘한 한 줄 trail로 배치됩니다.
|
|
49
|
+
|
|
50
|
+
## 플랫폼 번역 — 왜 Web 전용인가
|
|
51
|
+
|
|
52
|
+
Breadcrumb는 `platform: web`입니다. Native에서는 같은 문제("지금 어디에 있고 어떻게
|
|
53
|
+
돌아가는가")를 이미 두 가지가 풀고 있습니다.
|
|
54
|
+
|
|
55
|
+
- 플랫폼 back 제스처(스와이프, 하드웨어 back)가 바로 이전 화면으로 돌아가는 동작을
|
|
56
|
+
소유합니다.
|
|
57
|
+
- `TopBarRecipe`의 `title` 슬롯이 지금 위치를 한 줄로 보여 줍니다(`docs/bottom-navigation.md`
|
|
58
|
+
및 `topBarRecipe` 참고 — slot은 `root/leading/title/trailing`뿐이고 다단계 경로 trail을
|
|
59
|
+
위한 자리가 없습니다).
|
|
60
|
+
|
|
61
|
+
그래서 Native에 별도 Breadcrumb를 만들면 TopBar와 같은 정보를 두 번 계약하게 됩니다.
|
|
62
|
+
`breadcrumbBehaviorSpec.native`는 의도적으로 빈 `{ roles: [], states: [], actions: [] }`이며,
|
|
63
|
+
이는 미완성이 아니라 "이 컴포넌트는 Native에 존재하지 않는다"는 선언입니다.
|
|
64
|
+
|
|
65
|
+
Web에서는:
|
|
66
|
+
|
|
67
|
+
- `nav` landmark(`role="navigation"`) 하나가 전체 trail을 감싸고, 순서 있는 `list`/`listitem`
|
|
68
|
+
구조로 항목을 나열합니다.
|
|
69
|
+
- 조상 항목은 `Link`의 `web.roles: ["link"]`, `keyboard: ["Tab", "Enter"]`를 그대로
|
|
70
|
+
가져오므로 Breadcrumb 자체가 새 키보드 상호작용을 정의하지 않습니다.
|
|
71
|
+
- 현재 항목은 tab stop이 아니고 `aria-current="page"`만 갖습니다.
|
|
72
|
+
|
|
73
|
+
## 검증 화면
|
|
74
|
+
|
|
75
|
+
아직 없음. 이전 판정이 후보로 든 "야잘알의 구단 상세 → 선수단 → 선수 상세" 계층은
|
|
76
|
+
검증 결과 근거가 될 수 없다 — Breadcrumb는 `platform: "web"`인데 야잘알(`modules/app`,
|
|
77
|
+
`modules/app-rn`)은 Flutter/React Native 모바일 앱뿐이고 Web 화면 자체가 없다(Native
|
|
78
|
+
계층 이동은 이미 위에서 TopBar가 담당하기로 판정했다). BurnTok의 Web 앱
|
|
79
|
+
(`apps/web/src/app`)도 함께 확인했지만 지금 라우트는 대부분 2단 이하(`/c/[id]`,
|
|
80
|
+
`/ideas/[id]`, `/messages/[peerId]`, `/u/[id]`)라 3단 이상 계층 화면을 아직 찾지
|
|
81
|
+
못했다. `planned → beta` 승격은 실제 3단 이상 Web 화면이 나오고 키보드/스크린리더
|
|
82
|
+
검증을 거친 뒤 리드가 결정한다.
|
package/docs/calendar.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Calendar contract
|
|
2
|
+
|
|
3
|
+
## 문제
|
|
4
|
+
|
|
5
|
+
한 달의 날짜를 격자로 보여주고, 날짜마다 제품이 붙인 콘텐츠(경기 수, 점 표시 등)를 함께
|
|
6
|
+
드러내며, 오늘·선택된 날짜·선택 불가능한 날짜를 색이 아닌 방식으로 구분한다. Yajalal의
|
|
7
|
+
일정 탐색기(`schedule-explorer`)가 겨냥한 화면이 이 문제다.
|
|
8
|
+
|
|
9
|
+
## Calendar와 DatePicker의 경계 — 이 저작의 핵심 판정
|
|
10
|
+
|
|
11
|
+
antd는 `Calendar`(data-display, 상시 표시)와 `DatePicker`(data-entry, 트리거+오버레이)를
|
|
12
|
+
분리한다. 둘 다 "월 격자, 날짜 셀, 오늘, 선택, 비활성 날짜"라는 **같은 조각**을 공유한다.
|
|
13
|
+
그래서:
|
|
14
|
+
|
|
15
|
+
- 격자(월 표시, 날짜 셀, 오늘 표시, 선택 표시, 비활성 날짜, 방향키 이동)는 **공유 조각**이며
|
|
16
|
+
이 파일(`calendar.ts`)에 전부 담는다.
|
|
17
|
+
- `Calendar` = 이 격자 그 자체 + 날짜별 콘텐츠. 트리거도 오버레이도 없이 항상 화면에 있다.
|
|
18
|
+
- `DatePicker`(`date-picker.ts`)는 필드 트리거 + 오버레이(Web popover / Native Sheet) 안에
|
|
19
|
+
**이 격자를 그대로** 담는다. `resolveDatePickerGrid`는 `resolveCalendarGridDescriptor`를
|
|
20
|
+
그대로 호출할 뿐, 셀 의미를 다시 정의하지 않는다.
|
|
21
|
+
|
|
22
|
+
`Select`/`Combobox`가 `collection.ts`를 공유하는 것과 같은 구조다 — 다만 여기서는 "제공자"가
|
|
23
|
+
`Calendar`(격자 자체가 컴포넌트인 쪽)이고 `DatePicker`가 그 격자를 오버레이 안에 담는 소비자다.
|
|
24
|
+
|
|
25
|
+
## Yajalal 실사용처 재확인 — 전제가 이미 바뀌어 있었다
|
|
26
|
+
|
|
27
|
+
이 작업을 위임받을 때 "일정 탐색기가 월 달력 격자를 자체 구현하고 있다"는 전제가 있었다.
|
|
28
|
+
`modules/app-rn/src/features/schedule-explorer/model.ts`와 `ScheduleExplorerScreen.tsx`를
|
|
29
|
+
직접 읽은 결과, 그 전제는 **더 이상 사실이 아니다**:
|
|
30
|
+
|
|
31
|
+
- `buildCalendarCells`/`CalendarDayCell`는 여전히 존재하지만, 커밋
|
|
32
|
+
`0f4887c 비교 프리셋 제목 정정, 일정 탐색기 월 그리드를 날짜 레일로 교체`에서 **화면에
|
|
33
|
+
격자를 그리는 코드 자체가 삭제**됐다. 지금 남은 역할은 월 범위 쿼리(`monthRange`)와
|
|
34
|
+
§6 날짜 레일(`createScheduleDateRail`)의 입력을 만드는 내부 데이터 계산뿐이다.
|
|
35
|
+
- 실제로 렌더되는 것은 `MonthHeader`(월 이름 + 이전/다음 버튼, 격자 없음)와
|
|
36
|
+
`ScheduleDateRail`(7일 문맥의 가로 스크롤 레일, §6 계약)이다. 레일은 이 저작의 범위 밖으로
|
|
37
|
+
지정됐고, 레일 자체도 날짜 그리드가 아니라 완전히 다른 형태(승/패/응원/휴식 마크가 있는
|
|
38
|
+
1차원 트랙)다.
|
|
39
|
+
- 앱 전체를 훑어도(`날짜 선택`/`DatePicker` 검색) 값 하나를 고르는 압축 트리거형 UI는
|
|
40
|
+
어디에도 없다. `calendar` 아이콘이 쓰이는 다른 화면(FA, 선수 기록, 데일리 픽, 온보딩)은
|
|
41
|
+
모두 장식 아이콘일 뿐 날짜 격자가 아니다.
|
|
42
|
+
|
|
43
|
+
**결론.** 현재 Yajalal에는 Calendar나 DatePicker의 살아있는 vertical slice 후보가 없다.
|
|
44
|
+
그렇다고 Notification/Dropdown처럼 "만들지 않는다"로 판정하지는 않았다 — 그 두 문서의
|
|
45
|
+
판정 근거는 "문제 자체가 이미 다른 컴포넌트로 완결됐다"였고, 날짜 격자를 보여주거나
|
|
46
|
+
고르는 문제는 Select/Menu/Toast 어느 것으로도 흡수되지 않는 별개의 문제이기 때문이다(로드맵
|
|
47
|
+
Batch 3에도 명시적으로 planned로 예약돼 있다). 다만 `planned → beta` 승격에 필요한 실제
|
|
48
|
+
vertical slice는 **아직 없다** — 이 계약은 그 전 단계("계약+recipe 준비됨")만 완성한다.
|
|
49
|
+
후보가 다시 필요해지면 일정 탐색기가 아니라 (a) 언젠가 월 그리드가 되돌아오는 화면이거나
|
|
50
|
+
(b) 생년월일·계약일처럼 값 하나를 고르는 새 폼 필드가 될 것이다.
|
|
51
|
+
|
|
52
|
+
## 일반화한 계약
|
|
53
|
+
|
|
54
|
+
### Collection 기본 계약을 적용하지 않는다
|
|
55
|
+
|
|
56
|
+
격자는 목록이 아니라 2차원이다. `stable id / label / textValue / none|single|multiple
|
|
57
|
+
selection mode / idle|loading|loadingMore|empty|error` 중 어느 것도 날짜 셀에 억지로
|
|
58
|
+
채우지 않았다:
|
|
59
|
+
|
|
60
|
+
- 날짜 자체(`"YYYY-MM-DD"`)가 이미 stable id다. 별도 `id`/`label`/`textValue` 삼중주를
|
|
61
|
+
만들면 같은 값을 두 번 말하는 유령 필드가 된다.
|
|
62
|
+
- 날짜 셀은 검색·타이핑 대상이 아니다(Steps가 같은 이유로 `textValue`를 배제한 것과 같다).
|
|
63
|
+
- 격자는 서버에서 비동기로 채워지는 목록이 아니다. 어느 달의 날짜가 며칠까지 있는지는
|
|
64
|
+
제품이 항상 동기로 안다 — `idle|loading|loadingMore|empty|error`는 셀이 아니라 그 안의
|
|
65
|
+
`content`(경기 데이터)에 대한 것이고, 그건 제품이 격자 바깥에서 스스로 감싼다(Yajalal의
|
|
66
|
+
`AppStateRegion`처럼). Statistic이 값 포맷을 소유하지 않듯, Calendar도 그 로딩 상태를
|
|
67
|
+
소유하지 않는다.
|
|
68
|
+
|
|
69
|
+
### 날짜는 항상 문자열이다
|
|
70
|
+
|
|
71
|
+
`Date` 객체는 계약 어디에도 없다. `cells[].date`, `todayDate`, `selectedDate`,
|
|
72
|
+
`focusedMonth`는 전부 `"YYYY-MM-DD"` 또는 `"YYYY-MM"` 문자열이다 — Yajalal이 이미
|
|
73
|
+
`gameDate.slice(0, 10)`을 키로 쓰는 이유와 같다(시간대에 따라 같은 순간이 다른 날짜가
|
|
74
|
+
되는 문제를 원천 차단). `validateCalendarGridDescriptor`는 형태(정규식)만 검사하고
|
|
75
|
+
달력 산수(윤년, 월별 일수, 요일 계산)는 절대 하지 않는다 — 제품이 이미 그 계산을
|
|
76
|
+
가지고 있고(Yajalal의 `buildCalendarCells`), 시간대 의존적인 `Date` 연산을 이 패키지에
|
|
77
|
+
들이면 "런타임 의존성 금지"와 "제품이 포맷한 문자열을 받는다" 원칙을 동시에 어긴다.
|
|
78
|
+
|
|
79
|
+
### 격자 모양은 제품이 만들고, 의미는 HJM이 유도한다
|
|
80
|
+
|
|
81
|
+
`CalendarGridDescriptor`는 이미 계산된 7열 배열(`cells`, row-major)을 받는다. 어떤 날짜가
|
|
82
|
+
이 달에 속하는지, 앞뒤로 몇 칸이 비는지는 제품이 이미 안다. HJM은:
|
|
83
|
+
|
|
84
|
+
- 모양을 검증한다(7의 배수, 요일 라벨 7개, 날짜 형식, 중복 없음).
|
|
85
|
+
- 셀마다 `row`/`column`/`isToday`/`isSelected`/`selectable`을 유도한다(Steps가 `currentStepId`
|
|
86
|
+
하나에서 `pending/current/complete`를 유도하는 것과 같은 원칙 — 제품이 상태를 배열로 다시
|
|
87
|
+
넘기지 않는다).
|
|
88
|
+
- `date`가 없는 셀은 순수한 채움칸이다(이번 달 1일 전 요일들처럼). 채움칸은 절대 포커스,
|
|
89
|
+
선택, 접근성 이름을 갖지 않는다.
|
|
90
|
+
|
|
91
|
+
### 낭독은 제품이 조립한다
|
|
92
|
+
|
|
93
|
+
"8월 19일 수요일, 경기 2개, 선택됨" 같은 문장은 `composeAccessibleName`이 만든다 — Steps의
|
|
94
|
+
`composeAccessibleName`과 같은 이유(어순·조사는 언어마다 다르고, `content`의 의미는 제품만
|
|
95
|
+
안다). 빈 문자열을 반환하면 resolver가 던진다.
|
|
96
|
+
|
|
97
|
+
### 선택 불가능한 날짜는 포커스를 잃지 않는다
|
|
98
|
+
|
|
99
|
+
`getCollectionNavigationTarget`(Menu/Select/Combobox)은 disabled 항목을 건너뛴다. Calendar는
|
|
100
|
+
**건너뛰지 않는다** — WAI-ARIA Date Picker Dialog 패턴처럼, 화살표 아래는 항상 정확히 한 주
|
|
101
|
+
아래로 이동해야 예측 가능하다. 비활성 날짜에 포커스가 앉는 것과 그 날짜를 **활성화**할 수
|
|
102
|
+
있는 것은 별개다: `resolveCalendarGridDescriptor`가 내주는 `selectable: false`가 그 경계를
|
|
103
|
+
표시하고, activate(Enter/Space/tap) 자체를 막는 책임은 renderer에 있다.
|
|
104
|
+
|
|
105
|
+
### 격자 밖으로 나가면 넘긴다, 감싸지 않는다
|
|
106
|
+
|
|
107
|
+
한 페이지(보이는 달)의 경계를 넘는 화살표 이동은 순환하지 않고 `{ overflow: "before" |
|
|
108
|
+
"after" }`를 돌려준다 — HJM은 인접 달의 모양을 모르기 때문이다. 제품이 이 신호를 받아
|
|
109
|
+
달을 넘기고(`focusedMonth` 변경) 해당 날짜에 포커스를 옮긴다. **월 이동은 별도 통제
|
|
110
|
+
축(`focusedMonth`/`onFocusedMonthChange`)이며, 선택(`selectedDate`)과 완전히 독립이다** —
|
|
111
|
+
달을 넘겨도 선택은 지워지지 않는다.
|
|
112
|
+
|
|
113
|
+
## HJM 기본값
|
|
114
|
+
|
|
115
|
+
- `today`는 지속되는 테두리(`border.focus`)로, `selected`는 채워진 배경
|
|
116
|
+
(`action.brand.background`)으로 표시한다 — 겹쳐도(오늘이면서 선택된 날) 둘 다 읽힌다.
|
|
117
|
+
- 비활성 날짜와 이번 달 밖 날짜는 각각 `disabledOpacity`/`outsideFocusedMonthOpacity`로
|
|
118
|
+
구분한다(같은 회색조가 아니라 서로 다른 강도).
|
|
119
|
+
- 셀 지름은 `control.minTouchTarget`(medium) 이상을 보장한다.
|
|
120
|
+
|
|
121
|
+
## 플랫폼 번역
|
|
122
|
+
|
|
123
|
+
- Web: `grid`/`row`/`gridcell` role과 roving tabindex 화살표 이동을 쓴다(같은 결과: 원하는
|
|
124
|
+
날짜에 도달). 채움칸은 `aria-hidden`.
|
|
125
|
+
- Native: 복합 grid role의 동등물이 없다 — Steps/Breadcrumb가 이미 같은 결론을 냈다. 각
|
|
126
|
+
날짜는 독립적으로 포커스·탭 가능한 요소이고, 월 이동은 두 플랫폼 모두 있는 이전/다음
|
|
127
|
+
버튼으로만 이뤄진다(화살표 스와이프 같은 제스처 기반 이동은 이 계약에 없다).
|
|
128
|
+
- 두 플랫폼 모두 "오늘·선택·비활성"을 같은 세 가지 비-색 신호(테두리/배경/투명도)로
|
|
129
|
+
전달한다. 픽셀 parity가 아니라 같은 action/result/announcement라는 `beta →
|
|
130
|
+
stable(adaptive)` gate의 기준을 그대로 따랐다 — Calendar 자체는 `adaptive`가 아니라
|
|
131
|
+
`shared`로 분류하지만(오버레이 선택이 없으므로), 접근성 경로가 플랫폼마다 다르다는 점은
|
|
132
|
+
같다.
|
|
133
|
+
|
|
134
|
+
## 공개한 축 / 배제한 축
|
|
135
|
+
|
|
136
|
+
| 축 | 상태 |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| `today` / `selected` / `disabled` / `outsideFocusedMonth` | 공개 |
|
|
139
|
+
| 월 이동(`focusedMonth`, 선택과 독립) | 공개 |
|
|
140
|
+
| 방향키 격자 이동(일/주 단위, Home/End) | 공개 — Web roving tabindex |
|
|
141
|
+
| range 선택(시작~끝 날짜 구간) | **배제** — 측정된 요구가 없다. Yajalal FA 조회, 경기 일정
|
|
142
|
+
어디에도 기간 선택 UI가 없다. 필요해지면 `CalendarSelection`을 확장하지 않고 별도
|
|
143
|
+
`CalendarRangeSelection` 타입을 새로 여는 쪽을 권한다 — 단일 선택 소비자의 타입을
|
|
144
|
+
좁히지 않기 위해서다. |
|
|
145
|
+
| 요일 시작(일요일/월요일) 로직 | **배제** — 제품이 `weekdayLabels`와 `cells` 순서로 이미
|
|
146
|
+
결정해서 넘긴다. HJM이 로케일별 첫 요일을 판단하지 않는다. |
|
|
147
|
+
| PageUp/PageDown 월·년 이동 단축키 | **배제** — 화살표 overflow 신호 + 명시적 이전/다음
|
|
148
|
+
버튼이 이미 월 이동을 커버한다. 측정된 단축키 요구가 나오면 추가한다. |
|
|
149
|
+
| 다중 월 동시 표시(antd `Calendar`의 연간 뷰 등) | **배제** — 한 번에 한 달만 다룬다. |
|
|
150
|
+
|
|
151
|
+
## 검증 화면
|
|
152
|
+
|
|
153
|
+
아직 없다. 위 「Yajalal 실사용처 재확인」에서 밝혔듯 현재 살아있는 vertical slice 후보가
|
|
154
|
+
없다 — `planned → beta` 승격은 실제 제품 화면이 나온 뒤 리드가 진행한다.
|