@hjmds/design-contracts 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/dist/agreement.d.ts +183 -0
  2. package/dist/agreement.d.ts.map +1 -0
  3. package/dist/agreement.js +137 -0
  4. package/dist/agreement.js.map +1 -0
  5. package/dist/anchor.d.ts +62 -0
  6. package/dist/anchor.d.ts.map +1 -0
  7. package/dist/anchor.js +43 -0
  8. package/dist/anchor.js.map +1 -0
  9. package/dist/asset.d.ts +94 -0
  10. package/dist/asset.d.ts.map +1 -0
  11. package/dist/asset.js +60 -0
  12. package/dist/asset.js.map +1 -0
  13. package/dist/base-recipes.d.ts +19 -0
  14. package/dist/base-recipes.d.ts.map +1 -1
  15. package/dist/base-recipes.js +17 -0
  16. package/dist/base-recipes.js.map +1 -1
  17. package/dist/behaviors.d.ts +346 -3
  18. package/dist/behaviors.d.ts.map +1 -1
  19. package/dist/behaviors.js +35 -1
  20. package/dist/behaviors.js.map +1 -1
  21. package/dist/bottom-info.d.ts +58 -0
  22. package/dist/bottom-info.d.ts.map +1 -0
  23. package/dist/bottom-info.js +48 -0
  24. package/dist/bottom-info.js.map +1 -0
  25. package/dist/carousel.d.ts +3 -3
  26. package/dist/carousel.js +1 -1
  27. package/dist/carousel.js.map +1 -1
  28. package/dist/catalog.d.ts +1250 -239
  29. package/dist/catalog.d.ts.map +1 -1
  30. package/dist/catalog.js +65 -25
  31. package/dist/catalog.js.map +1 -1
  32. package/dist/collapsible.d.ts +56 -0
  33. package/dist/collapsible.d.ts.map +1 -0
  34. package/dist/collapsible.js +37 -0
  35. package/dist/collapsible.js.map +1 -0
  36. package/dist/component-definitions.d.ts +16 -0
  37. package/dist/component-definitions.d.ts.map +1 -1
  38. package/dist/component-definitions.js +16 -0
  39. package/dist/component-definitions.js.map +1 -1
  40. package/dist/component-recipes.d.ts +20 -3
  41. package/dist/component-recipes.d.ts.map +1 -1
  42. package/dist/component-recipes.js +24 -1
  43. package/dist/component-recipes.js.map +1 -1
  44. package/dist/context-menu.d.ts +53 -0
  45. package/dist/context-menu.d.ts.map +1 -0
  46. package/dist/context-menu.js +44 -0
  47. package/dist/context-menu.js.map +1 -0
  48. package/dist/counter-badge-recipe.d.ts +5 -0
  49. package/dist/counter-badge-recipe.d.ts.map +1 -1
  50. package/dist/counter-badge-recipe.js +5 -0
  51. package/dist/counter-badge-recipe.js.map +1 -1
  52. package/dist/dataviz.d.ts +76 -0
  53. package/dist/dataviz.d.ts.map +1 -0
  54. package/dist/dataviz.js +58 -0
  55. package/dist/dataviz.js.map +1 -0
  56. package/dist/date-range.d.ts +56 -0
  57. package/dist/date-range.d.ts.map +1 -0
  58. package/dist/date-range.js +79 -0
  59. package/dist/date-range.js.map +1 -0
  60. package/dist/design-system-provider.d.ts +24 -0
  61. package/dist/design-system-provider.d.ts.map +1 -1
  62. package/dist/design-system-provider.js +21 -0
  63. package/dist/design-system-provider.js.map +1 -1
  64. package/dist/floating-action-button.d.ts +1 -1
  65. package/dist/floating-action-button.d.ts.map +1 -1
  66. package/dist/floating-action-button.js +3 -1
  67. package/dist/floating-action-button.js.map +1 -1
  68. package/dist/formatters.d.ts +37 -0
  69. package/dist/formatters.d.ts.map +1 -0
  70. package/dist/formatters.js +69 -0
  71. package/dist/formatters.js.map +1 -0
  72. package/dist/heading.d.ts +82 -0
  73. package/dist/heading.d.ts.map +1 -0
  74. package/dist/heading.js +48 -0
  75. package/dist/heading.js.map +1 -0
  76. package/dist/index.d.ts +19 -0
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +19 -0
  79. package/dist/index.js.map +1 -1
  80. package/dist/menubar.d.ts +133 -0
  81. package/dist/menubar.d.ts.map +1 -0
  82. package/dist/menubar.js +80 -0
  83. package/dist/menubar.js.map +1 -0
  84. package/dist/native-platform.d.ts +88 -0
  85. package/dist/native-platform.d.ts.map +1 -0
  86. package/dist/native-platform.js +70 -0
  87. package/dist/native-platform.js.map +1 -0
  88. package/dist/popover.d.ts +19 -4
  89. package/dist/popover.d.ts.map +1 -1
  90. package/dist/popover.js +3 -0
  91. package/dist/popover.js.map +1 -1
  92. package/dist/progress-recipe.d.ts +14 -0
  93. package/dist/progress-recipe.d.ts.map +1 -1
  94. package/dist/progress-recipe.js +14 -1
  95. package/dist/progress-recipe.js.map +1 -1
  96. package/dist/provider-button.d.ts +140 -0
  97. package/dist/provider-button.d.ts.map +1 -0
  98. package/dist/provider-button.js +83 -0
  99. package/dist/provider-button.js.map +1 -0
  100. package/dist/recipes.d.ts +15 -1
  101. package/dist/recipes.d.ts.map +1 -1
  102. package/dist/recipes.js +14 -0
  103. package/dist/recipes.js.map +1 -1
  104. package/dist/sheet.d.ts +16 -0
  105. package/dist/sheet.d.ts.map +1 -1
  106. package/dist/sheet.js +33 -0
  107. package/dist/sheet.js.map +1 -1
  108. package/dist/sidebar.d.ts +168 -0
  109. package/dist/sidebar.d.ts.map +1 -0
  110. package/dist/sidebar.js +98 -0
  111. package/dist/sidebar.js.map +1 -0
  112. package/dist/skip-nav.d.ts +67 -0
  113. package/dist/skip-nav.d.ts.map +1 -0
  114. package/dist/skip-nav.js +47 -0
  115. package/dist/skip-nav.js.map +1 -0
  116. package/dist/tags-input.d.ts +157 -0
  117. package/dist/tags-input.d.ts.map +1 -0
  118. package/dist/tags-input.js +108 -0
  119. package/dist/tags-input.js.map +1 -0
  120. package/dist/text-formats.d.ts +85 -0
  121. package/dist/text-formats.d.ts.map +1 -0
  122. package/dist/text-formats.js +43 -0
  123. package/dist/text-formats.js.map +1 -0
  124. package/dist/toggle-group.d.ts +111 -0
  125. package/dist/toggle-group.d.ts.map +1 -0
  126. package/dist/toggle-group.js +78 -0
  127. package/dist/toggle-group.js.map +1 -0
  128. package/dist/top.d.ts +113 -0
  129. package/dist/top.d.ts.map +1 -0
  130. package/dist/top.js +72 -0
  131. package/dist/top.js.map +1 -0
  132. package/dist/version.d.ts +1 -1
  133. package/dist/version.js +1 -1
  134. package/dist/version.js.map +1 -1
  135. package/docs/agreement.md +33 -0
  136. package/docs/anchor.md +59 -59
  137. package/docs/ant-design-coverage.md +5 -3
  138. package/docs/asset.md +26 -0
  139. package/docs/bottom-info.md +15 -0
  140. package/docs/breadcrumb.md +15 -15
  141. package/docs/button-label.md +20 -0
  142. package/docs/calendar.md +82 -154
  143. package/docs/carousel.md +27 -2
  144. package/docs/cascader.md +17 -0
  145. package/docs/chart.md +33 -0
  146. package/docs/clipboard.md +13 -0
  147. package/docs/collapsible.md +19 -0
  148. package/docs/command-palette.md +19 -0
  149. package/docs/confirm-popover.md +11 -1
  150. package/docs/context-menu.md +23 -0
  151. package/docs/data-table.md +22 -0
  152. package/docs/date-range.md +31 -0
  153. package/docs/density.md +23 -0
  154. package/docs/expansion-roadmap.md +16 -3
  155. package/docs/floating-action-button.md +35 -3
  156. package/docs/formatters.md +18 -0
  157. package/docs/generated/component-maturity.md +39 -23
  158. package/docs/generated/renderer-evidence.json +2921 -642
  159. package/docs/generated/renderer-evidence.md +53 -6
  160. package/docs/generated/showcase-manifest.json +1087 -64
  161. package/docs/heading.md +25 -0
  162. package/docs/identity.md +5 -0
  163. package/docs/layout.md +1 -1
  164. package/docs/library-gap-analysis.md +29 -9
  165. package/docs/list-row.md +14 -0
  166. package/docs/mentions.md +18 -0
  167. package/docs/menubar.md +22 -0
  168. package/docs/native-platform.md +36 -0
  169. package/docs/overlay-stack.md +24 -0
  170. package/docs/pagination.md +16 -8
  171. package/docs/popover.md +42 -13
  172. package/docs/product-audit-2026-09-15.md +88 -0
  173. package/docs/progress.md +16 -0
  174. package/docs/provider-button.md +33 -0
  175. package/docs/rating.md +19 -0
  176. package/docs/react-native-completion.md +347 -0
  177. package/docs/screen-chrome.md +52 -0
  178. package/docs/side-panel.md +23 -2
  179. package/docs/sidebar.md +30 -0
  180. package/docs/skip-nav.md +20 -0
  181. package/docs/splitter.md +22 -2
  182. package/docs/stable-promotion.md +51 -0
  183. package/docs/tags-input.md +25 -0
  184. package/docs/text-formats.md +15 -0
  185. package/docs/theming.md +84 -0
  186. package/docs/time-picker.md +22 -1
  187. package/docs/toast.md +18 -0
  188. package/docs/toggle-group.md +21 -0
  189. package/docs/top.md +32 -0
  190. package/docs/tour.md +23 -2
  191. package/docs/transfer-list.md +20 -0
  192. package/docs/tree-select.md +18 -0
  193. package/docs/tree.md +21 -0
  194. package/package.json +120 -6
package/dist/top.js ADDED
@@ -0,0 +1,72 @@
1
+ import { heading, spacing, typography } from "./foundations.js";
2
+ import { semanticColors } from "./semantic-colors.js";
3
+ export const topDefaults = {
4
+ size: "large",
5
+ headingLevel: 1,
6
+ };
7
+ function assertNonEmpty(value, field) {
8
+ if (typeof value !== "string" || value.trim().length === 0) {
9
+ throw new TypeError(`Top ${field} must not be empty`);
10
+ }
11
+ }
12
+ export function validateTopDescriptor(descriptor) {
13
+ if (descriptor === null || typeof descriptor !== "object") {
14
+ throw new TypeError("Top descriptor must be an object");
15
+ }
16
+ assertNonEmpty(descriptor.title, "title");
17
+ if (descriptor.eyebrow !== undefined)
18
+ assertNonEmpty(descriptor.eyebrow, "eyebrow");
19
+ if (descriptor.description !== undefined) {
20
+ assertNonEmpty(descriptor.description, "description");
21
+ }
22
+ if (descriptor.size !== undefined && descriptor.size !== "medium" && descriptor.size !== "large") {
23
+ throw new TypeError(`Unsupported Top size: ${String(descriptor.size)}`);
24
+ }
25
+ if (descriptor.headingLevel !== undefined &&
26
+ ![1, 2, 3].includes(descriptor.headingLevel)) {
27
+ throw new TypeError(`Unsupported Top headingLevel: ${String(descriptor.headingLevel)}`);
28
+ }
29
+ }
30
+ /**
31
+ * `large`는 화면의 첫 제목, `medium`은 시트·모달 안의 첫 제목이다. 두 단계뿐인 이유는
32
+ * 세 번째가 필요해지는 자리가 곧 `Section`이기 때문이다 — 크기를 더 늘리는 대신 다른
33
+ * 컴포넌트를 쓰라는 신호로 둔다.
34
+ */
35
+ export const topRecipe = {
36
+ slots: ["root", "eyebrow", "title", "description", "trailing"],
37
+ defaults: { size: topDefaults.size },
38
+ sizes: {
39
+ medium: { title: typography.titleLarge, paddingTop: spacing.md, paddingBottom: spacing.sm },
40
+ large: { title: heading.level2, paddingTop: spacing.xl, paddingBottom: spacing.md },
41
+ },
42
+ eyebrow: {
43
+ color: semanticColors.content.brand,
44
+ textVariant: "label",
45
+ marginBottom: spacing.xxs,
46
+ },
47
+ title: { color: semanticColors.content.primary },
48
+ description: {
49
+ color: semanticColors.content.body,
50
+ textVariant: "body",
51
+ marginTop: spacing.xs,
52
+ },
53
+ /** 제목 줄 오른쪽 보조 행동. 본문이므로 TopBar처럼 아이콘 전용을 강제하지 않는다. */
54
+ trailing: { gap: spacing.xs },
55
+ gap: spacing.xs,
56
+ };
57
+ export const topBehavior = {
58
+ /** 상태 축이 없다 — 화면이 제목을 바꾸면 그냥 다른 문자열을 넘긴다. */
59
+ controlled: [],
60
+ inputs: ["title", "eyebrow", "description", "size", "headingLevel"],
61
+ stateAxes: {},
62
+ web: { roles: ["heading"], keyboard: [], focus: "none" },
63
+ native: { roles: ["header"], states: [], actions: [] },
64
+ scenarios: [
65
+ "the-title-is-a-real-heading-element-at-the-declared-level-not-styled-text",
66
+ "top-is-body-content-that-scrolls-away-while-topbar-is-fixed-chrome",
67
+ "description-wraps-instead-of-truncating-so-large-text-never-hides-the-question",
68
+ "a-trailing-action-shares-the-title-row-and-drops-below-it-when-space-runs-out",
69
+ "eyebrow-never-carries-information-the-title-does-not-repeat-in-some-form",
70
+ ],
71
+ };
72
+ //# sourceMappingURL=top.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"top.js","sourceRoot":"","sources":["../src/top.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BehaviorContract } from \"./behaviors.js\";\nimport type { ColorReference } from \"./color-references.js\";\nimport { heading, spacing, typography } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\n/**\n * 화면 본문의 **첫 블록**이다. `TopBar`(상단 내비게이션 바)와 다른 자리이고, 그 차이가\n * 이 계약의 존재 이유다.\n *\n * - `TopBar`는 화면에 고정된 크롬이다 — 뒤로가기, 화면 이름, 액션. 스크롤과 무관하게\n * 붙어 있고 safe area를 다룬다.\n * - `Top`은 본문이다 — 스크롤과 함께 올라가고, 사용자가 \"이 화면이 무엇을 묻는지\"\n * 읽는 문장이 여기 있다. 토스 TDS가 \"Top과 ListRow로 화면 대부분을 만든다\"고\n * 말하는 그 Top이고, 지금까지 BurnTok `AppScreenHeader`·Taground `screen-shell`·\n * Diairy `DetailPanel`이 각자 다시 만들던 블록이다.\n *\n * `Section`과도 다르다. Section은 본문 **중간**의 묶음 제목이라 heading level이 화면\n * 구조에 종속되지만, Top은 화면당 하나뿐인 첫 제목이라 기본이 `h1`이다. 그래서 두\n * 계약을 합치지 않았다 — 합치면 \"이 Section이 화면의 h1인가\"를 매번 물어야 한다.\n */\nexport type TopSize = \"medium\" | \"large\";\n\nexport type TopDescriptor = Readonly<{\n title: string;\n /** 제목 위 한 줄. 카테고리·단계처럼 제목을 한정하는 짧은 말. */\n eyebrow?: string;\n /** 제목 아래 보조 문장. 길면 줄바꿈되며 잘리지 않는다. */\n description?: string;\n size?: TopSize;\n /**\n * 문서 구조상의 제목 단계. 화면당 하나인 Top은 `1`이 기본이지만, 한 라우트가\n * 여러 화면을 담는 경우(시트 안의 화면 등) 제품이 낮출 수 있다.\n */\n headingLevel?: 1 | 2 | 3;\n}>;\n\nexport const topDefaults = {\n size: \"large\",\n headingLevel: 1,\n} as const satisfies Readonly<{ size: TopSize; headingLevel: 1 | 2 | 3 }>;\n\nfunction assertNonEmpty(value: string, field: string): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new TypeError(`Top ${field} must not be empty`);\n }\n}\n\nexport function validateTopDescriptor(descriptor: TopDescriptor): void {\n if (descriptor === null || typeof descriptor !== \"object\") {\n throw new TypeError(\"Top descriptor must be an object\");\n }\n assertNonEmpty(descriptor.title, \"title\");\n if (descriptor.eyebrow !== undefined) assertNonEmpty(descriptor.eyebrow, \"eyebrow\");\n if (descriptor.description !== undefined) {\n assertNonEmpty(descriptor.description, \"description\");\n }\n if (descriptor.size !== undefined && descriptor.size !== \"medium\" && descriptor.size !== \"large\") {\n throw new TypeError(`Unsupported Top size: ${String(descriptor.size)}`);\n }\n if (\n descriptor.headingLevel !== undefined &&\n ![1, 2, 3].includes(descriptor.headingLevel)\n ) {\n throw new TypeError(`Unsupported Top headingLevel: ${String(descriptor.headingLevel)}`);\n }\n}\n\n/**\n * `large`는 화면의 첫 제목, `medium`은 시트·모달 안의 첫 제목이다. 두 단계뿐인 이유는\n * 세 번째가 필요해지는 자리가 곧 `Section`이기 때문이다 — 크기를 더 늘리는 대신 다른\n * 컴포넌트를 쓰라는 신호로 둔다.\n */\nexport const topRecipe = {\n slots: [\"root\", \"eyebrow\", \"title\", \"description\", \"trailing\"] as const,\n defaults: { size: topDefaults.size },\n sizes: {\n medium: { title: typography.titleLarge, paddingTop: spacing.md, paddingBottom: spacing.sm },\n large: { title: heading.level2, paddingTop: spacing.xl, paddingBottom: spacing.md },\n },\n eyebrow: {\n color: semanticColors.content.brand,\n textVariant: \"label\" as const,\n marginBottom: spacing.xxs,\n },\n title: { color: semanticColors.content.primary },\n description: {\n color: semanticColors.content.body,\n textVariant: \"body\" as const,\n marginTop: spacing.xs,\n },\n /** 제목 줄 오른쪽 보조 행동. 본문이므로 TopBar처럼 아이콘 전용을 강제하지 않는다. */\n trailing: { gap: spacing.xs },\n gap: spacing.xs,\n} as const satisfies {\n slots: readonly [\"root\", \"eyebrow\", \"title\", \"description\", \"trailing\"];\n defaults: { size: TopSize };\n sizes: Readonly<{\n medium: { title: typeof typography.titleLarge; paddingTop: number; paddingBottom: number };\n large: { title: typeof heading.level2; paddingTop: number; paddingBottom: number };\n }>;\n eyebrow: { color: ColorReference; textVariant: \"label\"; marginBottom: number };\n title: { color: ColorReference };\n description: { color: ColorReference; textVariant: \"body\"; marginTop: number };\n trailing: { gap: number };\n gap: number;\n};\n\nexport const topBehavior = {\n /** 상태 축이 없다 — 화면이 제목을 바꾸면 그냥 다른 문자열을 넘긴다. */\n controlled: [],\n inputs: [\"title\", \"eyebrow\", \"description\", \"size\", \"headingLevel\"],\n stateAxes: {},\n web: { roles: [\"heading\"], keyboard: [], focus: \"none\" },\n native: { roles: [\"header\"], states: [], actions: [] },\n scenarios: [\n \"the-title-is-a-real-heading-element-at-the-declared-level-not-styled-text\",\n \"top-is-body-content-that-scrolls-away-while-topbar-is-fixed-chrome\",\n \"description-wraps-instead-of-truncating-so-large-text-never-hides-the-question\",\n \"a-trailing-action-shares-the-title-row-and-drops-below-it-when-space-runs-out\",\n \"eyebrow-never-carries-information-the-title-does-not-repeat-in-some-form\",\n ],\n} as const satisfies BehaviorContract;\n"]}
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  /** Package release shown by documentation surfaces. Kept in sync by a test. */
2
- export declare const designSystemVersion: "1.1.1";
2
+ export declare const designSystemVersion: "1.2.0";
3
3
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  /** Package release shown by documentation surfaces. Kept in sync by a test. */
2
- export const designSystemVersion = "1.1.1";
2
+ export const designSystemVersion = "1.2.0";
3
3
  //# sourceMappingURL=version.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAgB,CAAC","sourcesContent":["/** Package release shown by documentation surfaces. Kept in sync by a test. */\nexport const designSystemVersion = \"1.1.1\" as const;\n"]}
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAgB,CAAC","sourcesContent":["/** Package release shown by documentation surfaces. Kept in sync by a test. */\nexport const designSystemVersion = \"1.2.0\" as const;\n"]}
@@ -0,0 +1,33 @@
1
+ # Agreement contract
2
+
3
+ **문제.** 가입·결제 앞에 서는 약관 동의 묶음이다. 전체 동의 한 줄과 개별 항목들,
4
+ 필수/선택 구분, 각 항목의 전문 보기가 한 덩어리로 움직인다.
5
+
6
+ **왜 CheckboxGroup으로 충분하지 않은가.** 세 가지가 체크박스 목록이 아니라 절차이기
7
+ 때문이다.
8
+
9
+ 1. **필수/선택이 제출 가능 여부를 정한다.** CheckboxGroup은 "몇 개 골랐는가"만 알고
10
+ "이 화면이 진행 가능한가"는 모른다. `resolveAgreementState`의 `satisfied`와
11
+ `missingRequiredIds`가 그 판정이고, 제품의 제출 버튼이 그것만 읽는다.
12
+ 2. **전체 동의는 파생이다.** 저장되는 값은 개별 항목뿐이고 전체 행은 tri-state로
13
+ 그려진다 — DataTable 머리글(`resolveDataTableSelectAllState`)·TreeSelect 부모
14
+ (`resolveTreeCheckedStates`)와 같은 관계다. 전체 동의를 별도 값으로 저장하면
15
+ "전체 동의했지만 개별은 두 개만 체크됨" 같은 모순이 표현 가능해진다.
16
+ 3. **읽을 수 있어야 동의다.** 전문 경로가 없는 동의 항목은 만들 수 없게 두지는 않지만,
17
+ `detail`을 계약된 행동으로 둬서 renderer가 장식 화살표로 처리하지 못하게 한다.
18
+ 그 컨트롤은 체크박스와 **다른 tab stop**이고 누르면 절대 체크되지 않는다.
19
+
20
+ **막아 둔 조합.** 필수이면서 비활성인 항목은 descriptor 단계에서 거절한다. 허용하면
21
+ 사용자가 영원히 진행할 수 없는 화면이 조용히 만들어지고, "버튼이 왜 안 눌리는가"를
22
+ 아무도 설명할 수 없다.
23
+
24
+ **비활성 항목의 회계.** 전체 동의의 분모에서 뺀다 — 사용자가 바꿀 수 없는 것을 "덜
25
+ 동의했다"고 셀 수 없다. 필수는 비활성일 수 없으므로 `satisfied` 판정에는 이 예외가 없다.
26
+
27
+ **제품이 소유하는 것.** 문구, 링크 주소, 법적 유효성, 동의 기록의 저장·증빙. HJM은
28
+ 판정과 배치만 갖는다. 약관 내용을 어디에 보여줄지(링크·시트·새 화면)도 제품 결정이라
29
+ `detail.href`는 선택이고, 없으면 renderer가 버튼으로 낸다.
30
+
31
+ **플랫폼 번역.** Web은 `group` + `checkbox` + 별도 링크/버튼, Native는 checkbox
32
+ accessibility state와 `openDetail` action이다. 두 표면 모두 전체 동의 행이 목록의 제목
33
+ 역할을 하도록 `surface.sunken` 위에 놓는다.
package/docs/anchor.md CHANGED
@@ -1,59 +1,59 @@
1
- # Anchor — 계약을 만들지 않는다
2
-
3
- ## 문제로 제기된 것
4
-
5
- Ant Design `Anchor`는 긴 문서 안의 목차 링크를 스크롤 위치와 동기화합니다. 검토
6
- 관찰은, 진짜 계약은 **현재 위치 표시**(`aria-current`), **스크롤 동기화**,
7
- **reduced motion에서 부드러운 스크롤을 끄는 것**이라는 것과, 두 제품에 긴 문서 화면이
8
- 있는지 확인하라는 것이었습니다.
9
-
10
- ## 판정: 만들지 않는다
11
-
12
- ### 1. 두 제품 중 어디에도 목차가 필요한 긴 단일 문서 화면이 없다
13
-
14
- - **Yajalal RN**: `약관`/`개인정보처리방침`/`이용약관` 류 화면, 또는 섹션이 여럿인 긴
15
- 스크롤 문서 화면을 전수 검색했지만 찾지 못했습니다. `modules/app-rn/src/lib/copy/terms.ts`는
16
- 이름과 달리 법적 약관이 아니라 "같은 개념에는 하나의 단어"를 강제하는 **카피 용어
17
- 단일 출처**(구단·경기차 표기 등)일 뿐, 목차 내비게이션이 필요한 문서가 아닙니다.
18
- 화면 구조도 DESIGN_SYSTEM.md가 "48pt 상단바 + overline·대형 제목·설명 문장으로 이루어진
19
- 페이지 헤더는 쓰지 않는다"로 규정해, 애초에 스크롤 목차가 붙는 긴 article 레이아웃을
20
- 피하는 방향입니다.
21
- - **BurnTok**: `apps/web/src/app`의 최상위 라우트(`feed`, `messages`, `notifications`,
22
- `u/[id]`, `c/[id]`, `create`, `ideas`, `run` 등)를 전수 확인했지만 약관·도움말·블로그
23
- 같은 긴 문서 페이지 자체가 없습니다. 앱의 성격(피드형 소셜/영상)상 스크롤 목차가
24
- 붙을 만한 단일 긴 문서 화면이 없습니다.
25
- - `anchor`로 두 저장소를 검색했을 때 나온 유일한 실제 코드 매치는 BurnTok
26
- `AppTooltip.tsx`/`anchored-overlay.ts`의 "anchor element"였는데, 이는 툴팁이 붙는
27
- 기준 요소를 뜻하는 **오버레이 위치 계산 용어**로, antd `Anchor`(문서 내 목차)와는
28
- 이름만 같고 완전히 다른 개념입니다.
29
-
30
- ### 2. 계약할 상태 축은 유효하지만, 지금 채울 화면이 없다
31
-
32
- 관찰이 짚은 세 계약(`aria-current` 현재 위치, 스크롤 동기화, reduced motion에서 smooth
33
- scroll 끄기)은 실제로 새로운 플랫폼 중립 개념입니다 — `BottomNavigation`의
34
- `aria-current="page"`(`docs/architecture.md` BottomNavigation 절)와 다른 종류의 "현재
35
- 위치" 표시이고, `Toast`/`Sheet`의 Reduce Motion 처리와도 다른 대상(스크롤 애니메이션)에
36
- 적용됩니다. `Notification`/`Dropdown`/`VirtualList`처럼 "문제가 이미 다른 컴포넌트에
37
- 흡수됐다"는 판정은 아닙니다 — 흡수할 곳이 없고, 처음부터 이 문제를 푸는 컴포넌트가
38
- 없다는 뜻입니다.
39
-
40
- 그럼에도 만들지 않는 이유는 순전히 **측정된 수요 부재**입니다. 목차와 동기화할 긴
41
- 문서 자체가 없는 상태에서 스크롤 동기화 로직을 먼저 설계하면, 실제 문서 구조(섹션
42
- 개수, 중첩 깊이, 모바일에서 세로 목차를 어떻게 접을지)를 모른 채 API를 고정하게 됩니다.
43
- `docs/authoring-brief.md`와 로드맵이 반복해서 요구하는 "실제 제품 vertical slice 없이
44
- 승격하지 않는다"는 gate 이전에, **애초에 계약을 검증할 화면이 없는** 상태입니다.
45
-
46
- ## 만들지 않은 것
47
-
48
- `src/anchor.ts`, `test/anchor.test.ts`는 없습니다. `componentCatalog`의
49
- `{ name: "Anchor", category: "navigation", platform: "web", status: "planned" }` 행과
50
- crosswalk의 `Anchor → Anchor` direct 관계(`src/component-references.ts:64`)는 건드리지
51
- 않습니다.
52
-
53
- ## 뒤집힐 조건
54
-
55
- 1. BurnTok 또는 Yajalal(Web 우선, `platform: "web"` 그대로 유지 가능)에 여러 섹션을 가진
56
- 긴 단일 문서 화면(약관, 도움말, 가이드 등)이 실제로 생긴다.
57
- 2. 그 화면에서 `aria-current` 기반 현재 섹션 표시, 스크롤 동기화, reduced motion의 즉시
58
- 이동(auto) 전환이 실제로 요구되는 vertical slice가 측정된다 — 그때 이 세 축을 공개
59
- 상태 축으로 계약한다.
1
+ # Anchor — 같은 문서 안의 목차
2
+
3
+ 2026-09-16 React/RN 확장 요청에 따라 Web 라이브러리 beta를 제공합니다. 이전에는 실제 제품의
4
+ 긴 문서 수요가 없어 planned였습니다. 이번에는 작동하는 문서 예제와 브라우저 검증까지
5
+ 제공하며 제품 채택·보조기기 검증·stable 승격은 별도로 남깁니다.
6
+
7
+ [Ant Design Anchor](https://ant.design/components/anchor/)의 문서 내 링크·현재 위치·스크롤
8
+ 영역 구분을 비교했습니다. HJM은 content-brand 글자와 얇은 논리 방향 표시선으로 위치를
9
+ 보여주며 별도 카드나 주요 행동 버튼으로 강조하지 않습니다. API 호환을 약속하지 않습니다.
10
+
11
+ ## 사용법
12
+
13
+ ```tsx
14
+ import { Anchor } from "@hjmds/react/anchor";
15
+
16
+ <Anchor label="가이드 목차" items={[
17
+ { id: "start", label: "시작하기" },
18
+ { id: "next", label: "다음 단계" },
19
+ ]} offset={64} />
20
+ // 같은 문서에 고유 ID를 가진 <section id="start">, <section id="next">를 둡니다.
21
+ ```
22
+
23
+ - `items`는 비어 있지 않은 `{ id, label }` 배열입니다. ID는 공백 없이 문서 안에서 고유해야
24
+ 합니다. 중복 ID·빈 label을 거부합니다. fragment는 ID를 URL encode해 생성합니다.
25
+ - `container` 생략 시 문서 스크롤을 관찰합니다. `HTMLElement`를 넘기면 해당 영역 내부만
26
+ 관찰합니다. ref를 기다리는 `null`에서는 관찰하지 않습니다.
27
+ - `offset`은 상단 고정 헤더 아래에 남길 CSS px입니다. 기본 0이고 유한한 음이 아닌 수만
28
+ 허용합니다. 별도 스크롤 영역의 border와 scrollTop을 포함해 계산합니다.
29
+ - `orientation`은 `vertical` 기본값 또는 `horizontal`입니다. 좁은 폭에서 링크가 줄바꿈하며
30
+ 긴 label을 자르지 않습니다. 목차의 sticky 배치와 문서 레이아웃은 호스트가 소유합니다.
31
+ - `historyMode`는 `push` 기본값, `replace`, `none`입니다. 앞의 둘은 기존 history.state를
32
+ 보존하며 fragment만 갱신합니다. `none`은 URL을 쓰지 않는 내장 미리보기에 적합합니다.
33
+ - `onNavigate(id, event)`에서 `preventDefault()`로 앱 라우터 등에 동작을 위임할 수 있습니다.
34
+ 새 탭을 여는 modifier 클릭은 브라우저에 맡깁니다. `ref`는 nav 요소를 가리킵니다.
35
+
36
+ ## 스크롤·초점 계약
37
+
38
+ `nav > ul > li > a`를 사용하고 현재 링크 하나에 `aria-current="location"`을 둡니다.
39
+ 대상이 DOM에 없는 링크는 현재 위치 후보에서 제외하며 기본 href 동작을 유지합니다.
40
+ 사용자가 링크를 누르면 해당 section/제목에 초점을 옮깁니다. 일반 section은 임시 `tabindex=-1`을
41
+ 받고 blur/unmount 때 복원합니다. Tab/Enter는 브라우저 링크 동작입니다.
42
+
43
+ 현재 위치는 DOM 순서의 가정 대신 실제 위치로 계산합니다. 스크롤 끝에서는 짧은 마지막
44
+ 섹션도 선택합니다. 스크롤·창 크기·대상/부모 크기·본문 삽입/삭제를 관찰하고 unmount 때
45
+ listener·observer·예약 frame을 해제합니다. CSS transform만으로 위치를 움직이는 별도 애니메이션은
46
+ 계약에 포함하지 않습니다. window의 hashchange/popstate와 초기 fragment는 등록된 대상으로
47
+ 즉시 이동하여 별도 스크롤 영역도 복구합니다.
48
+
49
+ 일반 클릭은 smooth scroll입니다. HJM 환경 또는 OS가 reduced motion이면 `instant`로 이동합니다.
50
+ `auto`는 호스트의 scroll-behavior에 따라 다시 애니메이션될 수 있어 사용하지 않습니다.
51
+
52
+ ## 범위와 검증
53
+
54
+ Web 전용입니다. Native 라우팅·목록 위치 이동은 해당 플랫폼과 제품이 소유하며 빈 RN wrapper를
55
+ 추가하지 않습니다. 중첩 트리 목차·자동 제목 수집·오버레이 anchor positioning은 포함하지 않습니다.
56
+
57
+ `Patterns/Anchor`는 세 부분으로 나눈 읽기 가이드와 별도 스크롤 영역을 제공합니다.
58
+ 브라우저 테스트는 위치 동기화·짧은 마지막 부분·offset·reduced motion·임시 focus 복원·
59
+ fragment 복구·나중에 삽입된 대상을 검사합니다. 실제 제품 문서 채택·보조기기 검증은 남아 있습니다.
@@ -53,7 +53,7 @@ message와 notification을 가르는 축(정보 층, 지속 시간, 동시 개
53
53
  | 종류 | 뜻 | 예 | 재검토 신호 |
54
54
  |---|---|---|---|
55
55
  | **흡수됨** | 그 문제를 이미 다른 컴포넌트가 완결한다 | `Notification`→Toast, `Dropdown`→Menu, `ContextPanel`→SidePanel/Sheet, `Flex`→Stack, `TimePicker`→Select 조합, `Rating`→Slider/Statistic | 흡수한 쪽이 못 푸는 요구가 나올 때 |
56
- | **검증할 화면이 없음** | 계약 자체는 유효하나 이를 확인할 제품 화면이 없다 | `Anchor`, `Calendar` | 그 화면이 실제로 생길 때 |
56
+ | **검증할 화면이 없음** | 계약 자체는 유효하나 이를 확인할 제품 화면이 없다 | `Anchor` | 그 화면이 실제로 생길 때 |
57
57
  | **거절됨** | 계약도 유효하고 화면이 생겨도 만들지 않는다 | `BorderBeam`(장식), `AppProvider`(런타임뿐), `Utility`(실체 없음) | 정체성이나 아키텍처 경계가 바뀔 때만 |
58
58
  | **흡수 대기** | 흡수 판정은 끝났으나 **선결 축이 아직 없다** | `Cascader`(TreeSelect에 `valueMode`/`commitAt`가 추가돼야 성립) | 그 축이 실제로 추가될 때 |
59
59
 
@@ -88,8 +88,10 @@ evidence registry로 판단합니다.
88
88
  - partial maturity: decomposed target 중 일부만 stable 또는 beta
89
89
  - planned only: 모든 target이 planned
90
90
 
91
- 2026-08-27 snapshot의 status 기반 분포는 **fully mature 45 / partial maturity 1 /
92
- planned only 27**입니다. 따라서 73/73 tracking은 73개 구현 완료를 의미하지 않습니다.
91
+ 2026-09-18 SidePanel·Splitter·Tour·Tree·TransferList·Mentions·CommandPalette·DataTable renderer까지 추가한 뒤 status 기반 분포는
92
+ **fully mature 59 / partial maturity 0 /
93
+ planned only 14**입니다. decomposed Drawer의 두 갈래(Sheet·SidePanel)가 모두 구현되면서
94
+ partial maturity가 비었습니다 — 0은 "부분 구현이 없다"는 뜻이고 planned only 20은 그대로입니다. 따라서 73/73 tracking은 73개 구현 완료를 의미하지 않습니다.
93
95
  홈과 Component Explorer는 이 수치를 분리해 표시합니다. 이 숫자는 source inventory 수가
94
96
  아니라 HJM target의 maturity에서 계산하므로 catalog status가 바뀌면 함께 갱신합니다.
95
97
 
package/docs/asset.md ADDED
@@ -0,0 +1,26 @@
1
+ # Asset contract
2
+
3
+ **문제.** 아이콘·이미지·Lottie·비디오를 **같은 액자**에 넣는 자리.
4
+
5
+ **Icon·Image·Avatar가 있는데 왜 또 두는가.** 셋은 각자 다른 모양 규칙을 갖는다 — 아이콘은
6
+ 정사각, Avatar는 원, Image는 비율. 이들이 같은 줄에 섞여 나오면 크기와 모서리가 어긋난다.
7
+ 에어리의 `LottieArt`·`AnimalArt`가 그 어긋남을 제품 안에서 다시 맞추고 있었다. Asset은
8
+ 액자 규칙(크기 4단·모양 3종·겹침·보조 표식)만 갖는다.
9
+
10
+ **무엇을 그릴지는 슬롯으로 받는다.** Lottie 재생기도 비디오 재생기도 이 패키지가 의존하지
11
+ 않는다. 그래서 이 계약에 재생 상태(play/pause/seek)가 없다 — 재생기는 제품 것이고, 여기서
12
+ 정하는 것은 액자뿐이다. 이 경계를 흐리면 `@hjmds/react`가 애니메이션 런타임을 끌고 온다.
13
+
14
+ **뜻이 있는 자산에 이름이 없으면 거절한다.** 움직이는 그림일수록 "무엇을 뜻하는지"가
15
+ 화면에 없으면 보조기기에서 사라진다. 반대로 `decorative`와 `accessibilityLabel`을 **함께**
16
+ 주는 것도 거절한다 — 조용히 한쪽을 무시하면 이름을 지운 줄 모른다.
17
+
18
+ **`reducedMotion`에서는 숨기지 않고 정지한다.** 그 자리에 그림이 있다는 사실 자체가 화면의
19
+ 뜻인 경우가 많다(에어리의 동물). 정지 화면을 제품이 주지 못하면 재생만 멈춘다.
20
+ `shouldAnimateAsset(kind, reducedMotion)`이 이 판단을 갖는다.
21
+
22
+ **보조 표식은 액자 바깥 모서리에 붙는다.** 재생 아이콘이 매체 위에 올라가면 가장 중요한
23
+ 프레임 가운데를 가린다.
24
+
25
+ **겹침 비율은 Avatar와 같은 값이다.** 한 줄에 섞여 나오는 것이 이 컴포넌트가 생긴 이유라
26
+ 두 값이 다르면 그 줄이 다시 어긋난다.
@@ -0,0 +1,15 @@
1
+ # BottomInfo contract
2
+
3
+ **문제.** 주 행동 **아래** 붙는 작은 안내 — "가입하면 약관에 동의하는 것으로 봅니다",
4
+ "수수료는 결제 시점에 확정됩니다". 토스 TDS의 BottomInfo와 같은 자리다.
5
+
6
+ **Notice와 다르다.** Notice는 **지금 생긴 상태**를 알린다(오류·성공·경고). BottomInfo는
7
+ 화면에 늘 있는 **조건**이다. 그래서 tone에 danger/success가 없고, 아이콘이 없고,
8
+ `role="status"`도 없다 — 상태 변화로 발표하면 사용자가 매번 "무슨 문제가 생겼나" 하고
9
+ 읽게 된다. 두 단계(`muted`/`emphasis`)만 두는 이유도 같다: 강조는 있어도 경보는 없다.
10
+
11
+ **한 줄과 여러 줄이 다르게 읽힌다.** 한 줄은 문장, 여러 줄은 목록이다. 한 줄에 점을
12
+ 찍으면 오히려 시끄러워서 `listMarkerFrom`으로 경계를 계약에 적었다.
13
+
14
+ **문장 안의 링크는 제품 것이다.** 약관 링크의 주소·라우팅은 제품이 소유하므로
15
+ `renderItem`으로 그 줄만 대체한다.
@@ -28,8 +28,7 @@ Breadcrumb는 새 href 개념을 만들지 않습니다. 조상 항목의 `desti
28
28
  `LinkDestination`(`internal | external`) 타입 그대로이고, `validateBreadcrumbDescriptor`는
29
29
  각 조상 항목마다 `validateLinkDestination`을 그대로 호출합니다. 그래서 internal href가
30
30
  `/`, `?`, `#`로 시작해야 한다거나 external href가 허용된 protocol만 써야 한다는 규칙은
31
- Link 문서(`docs/link.md`)가 유일한 출처입니다. Breadcrumb 조상 항목은 Web에서는 실제
32
- anchor, Native가 이 컴포넌트를 쓴다면 Expo Router Link로 렌더링될 항목이라는 뜻이며,
31
+ Link 문서(`docs/link.md`)가 유일한 출처입니다. Breadcrumb 조상 항목은 Web의 실제 anchor로 렌더링하며,
33
32
  `Link`의 `disabled`/`onClick`/`onPress` 금지 규칙도 그대로 상속합니다.
34
33
 
35
34
  ## HJM 기본값
@@ -38,14 +37,13 @@ anchor, Native가 이 컴포넌트를 쓴다면 Expo Router Link로 렌더링될
38
37
  - 구분자(`/`, `›`)는 정보가 아니라 장식입니다. `breadcrumbRecipe.separator.decorative`는
39
38
  항상 `true`이고 renderer는 이를 접근성 트리에서 숨깁니다(Web `aria-hidden`, 스크린
40
39
  리더는 순서만 듣습니다).
41
- - 구분자 아이콘은 `chevronEnd`처럼 Icon registry의 논리 방향 이름을 씁니다. RTL 미러링은
42
- Icon 계약이 이미 소유하므로 Breadcrumb가 따로 방향을 계산하지 않습니다.
40
+ - 기본 구분자 `›`만 RTL에서 미러링합니다. `separator`로 전달한 Icon이나 문자는 소비자가
41
+ 방향을 소유하므로 다시 뒤집지 않습니다. `separator={null}`은 구분자를 숨깁니다.
43
42
  - **축약(`...`)을 넣지 않습니다.** 항목이 많을 때 가운데를 접는 것은 실제 화면에서
44
43
  측정된 수요가 아직 없습니다. 필요해지면 별도 `collapsed` 축으로 명시적으로 추가하고,
45
44
  지금은 renderer가 전체 trail을 그대로 그립니다.
46
45
  - 크기는 Link의 inline 취급을 따릅니다 — 44-unit 최소 target을 강제하지 않고 밑줄과
47
- focus indicator만 유지합니다. Breadcrumb 항목은 문장이 아니라 한 줄 경로이므로 독립된
48
- standalone Link처럼 하나씩 별도 target으로 쓰기보다, 촘촘한 한 줄 trail로 배치됩니다.
46
+ focus indicator만 유지합니다. 긴 경로는 줄바꿈하고 전체 항목을 유지합니다. 링크의 밑줄과 focus indicator는 유지합니다.
49
47
 
50
48
  ## 플랫폼 번역 — 왜 Web 전용인가
51
49
 
@@ -70,13 +68,15 @@ Web에서는:
70
68
  가져오므로 Breadcrumb 자체가 새 키보드 상호작용을 정의하지 않습니다.
71
69
  - 현재 항목은 tab stop이 아니고 `aria-current="page"`만 갖습니다.
72
70
 
73
- ## 검증 화면
71
+ ## 공개 경로와 검증
74
72
 
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
- 검증을 거친 뒤 리드가 결정한다.
73
+ `import { Breadcrumb } from "@hjmds/react/breadcrumb"`로 가져옵니다. root와 기존 navigation
74
+ 경로도 유지합니다. 필수 props는 `label`, `items`이고 `ref`는 nav 요소를 가리킵니다.
75
+
76
+ 2026-09-16 사용자의 React/RN 라이브러리 확장 요청으로, 제품 채택 대기와 라이브러리 beta
77
+ 제공을 분리했습니다. `Patterns/WebNavigation`은 조상 링크로 보관함을 열고 다시 기록 목록으로
78
+ 돌아오는 작동 예제입니다. 제품 채택이나 stable 증거로 계산하지 않습니다.
79
+
80
+ [WAI Breadcrumb](https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/)의 landmark·조상 링크·
81
+ 현재 위치 의미를 확인했습니다. 브라우저 테스트는 실제 anchor, 현재 plain text, 장식 구분자,
82
+ 320px/2배 글자·RTL 줄바꿈을 다룹니다. 실제 제품 라우팅·보조기기 검증은 남아 있습니다.
@@ -0,0 +1,20 @@
1
+ # 버튼 라벨 줄바꿈 정책
2
+
3
+ **문제.** 긴 라벨의 처리 규칙이 recipe에 없어서 Web은 무한히 줄바꿈하고 Native는 한 줄로
4
+ 잘렸다. **같은 번역문이 두 표면에서 다르게 보였다.**
5
+
6
+ **규칙.** 자르지 않고 **두 줄까지 접는다**(`buttonRecipe.label.maxLines = 2`).
7
+
8
+ - **자르지 않는 이유**: 잘린 행동 라벨("삭제하…")은 무엇을 하는 버튼인지 잃는다. 링크
9
+ 텍스트나 본문과 달리 버튼 라벨은 그 자체가 행동의 정의다.
10
+ - **무한히 늘리지 않는 이유**: 하단 CTA 행처럼 버튼이 남의 레이아웃을 밀어내는 자리가
11
+ 있다. 두 줄을 넘는 라벨은 레이아웃이 아니라 **카피의 문제**이고, 그 신호를 화면이
12
+ 주는 편이 낫다.
13
+
14
+ **큰 글자 설정에서는 상한을 푼다**(`liftCapOnLargeText`). 사용자가 키운 글자에서 두 줄로
15
+ 자르면 내용이 사라지고 그건 WCAG 1.4.4다. 제품 카피가 아니라 사용자 설정이 원인이므로
16
+ 같은 규칙을 적용할 수 없다. `resolveButtonLabelLines(largeText)`가 이 판정을 갖고,
17
+ Web은 `-webkit-line-clamp`, Native는 `numberOfLines`로 같은 답을 쓴다.
18
+
19
+ **숫자는 recipe에만 있다.** 스타일시트는 `--hjm-button-label-lines`를 읽고 렌더러가 그
20
+ 변수를 recipe에서 채운다 — 두 벌이 되면 언젠가 어긋난다.