@hjmds/design-contracts 1.1.1 → 1.3.4

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 (199) 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/auth-screen.d.ts +119 -0
  14. package/dist/auth-screen.d.ts.map +1 -0
  15. package/dist/auth-screen.js +87 -0
  16. package/dist/auth-screen.js.map +1 -0
  17. package/dist/base-recipes.d.ts +19 -0
  18. package/dist/base-recipes.d.ts.map +1 -1
  19. package/dist/base-recipes.js +17 -0
  20. package/dist/base-recipes.js.map +1 -1
  21. package/dist/behaviors.d.ts +363 -3
  22. package/dist/behaviors.d.ts.map +1 -1
  23. package/dist/behaviors.js +37 -1
  24. package/dist/behaviors.js.map +1 -1
  25. package/dist/bottom-info.d.ts +58 -0
  26. package/dist/bottom-info.d.ts.map +1 -0
  27. package/dist/bottom-info.js +48 -0
  28. package/dist/bottom-info.js.map +1 -0
  29. package/dist/carousel.d.ts +3 -3
  30. package/dist/carousel.js +1 -1
  31. package/dist/carousel.js.map +1 -1
  32. package/dist/catalog.d.ts +1315 -245
  33. package/dist/catalog.d.ts.map +1 -1
  34. package/dist/catalog.js +67 -25
  35. package/dist/catalog.js.map +1 -1
  36. package/dist/collapsible.d.ts +56 -0
  37. package/dist/collapsible.d.ts.map +1 -0
  38. package/dist/collapsible.js +37 -0
  39. package/dist/collapsible.js.map +1 -0
  40. package/dist/component-definitions.d.ts +17 -0
  41. package/dist/component-definitions.d.ts.map +1 -1
  42. package/dist/component-definitions.js +17 -0
  43. package/dist/component-definitions.js.map +1 -1
  44. package/dist/component-recipes.d.ts +20 -3
  45. package/dist/component-recipes.d.ts.map +1 -1
  46. package/dist/component-recipes.js +24 -1
  47. package/dist/component-recipes.js.map +1 -1
  48. package/dist/context-menu.d.ts +53 -0
  49. package/dist/context-menu.d.ts.map +1 -0
  50. package/dist/context-menu.js +44 -0
  51. package/dist/context-menu.js.map +1 -0
  52. package/dist/counter-badge-recipe.d.ts +5 -0
  53. package/dist/counter-badge-recipe.d.ts.map +1 -1
  54. package/dist/counter-badge-recipe.js +5 -0
  55. package/dist/counter-badge-recipe.js.map +1 -1
  56. package/dist/dataviz.d.ts +76 -0
  57. package/dist/dataviz.d.ts.map +1 -0
  58. package/dist/dataviz.js +58 -0
  59. package/dist/dataviz.js.map +1 -0
  60. package/dist/date-range.d.ts +56 -0
  61. package/dist/date-range.d.ts.map +1 -0
  62. package/dist/date-range.js +79 -0
  63. package/dist/date-range.js.map +1 -0
  64. package/dist/design-system-provider.d.ts +24 -0
  65. package/dist/design-system-provider.d.ts.map +1 -1
  66. package/dist/design-system-provider.js +21 -0
  67. package/dist/design-system-provider.js.map +1 -1
  68. package/dist/floating-action-button.d.ts +1 -1
  69. package/dist/floating-action-button.d.ts.map +1 -1
  70. package/dist/floating-action-button.js +3 -1
  71. package/dist/floating-action-button.js.map +1 -1
  72. package/dist/formatters.d.ts +37 -0
  73. package/dist/formatters.d.ts.map +1 -0
  74. package/dist/formatters.js +69 -0
  75. package/dist/formatters.js.map +1 -0
  76. package/dist/heading.d.ts +82 -0
  77. package/dist/heading.d.ts.map +1 -0
  78. package/dist/heading.js +48 -0
  79. package/dist/heading.js.map +1 -0
  80. package/dist/index.d.ts +20 -0
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +20 -0
  83. package/dist/index.js.map +1 -1
  84. package/dist/menubar.d.ts +133 -0
  85. package/dist/menubar.d.ts.map +1 -0
  86. package/dist/menubar.js +80 -0
  87. package/dist/menubar.js.map +1 -0
  88. package/dist/native-platform.d.ts +88 -0
  89. package/dist/native-platform.d.ts.map +1 -0
  90. package/dist/native-platform.js +70 -0
  91. package/dist/native-platform.js.map +1 -0
  92. package/dist/popover.d.ts +19 -4
  93. package/dist/popover.d.ts.map +1 -1
  94. package/dist/popover.js +3 -0
  95. package/dist/popover.js.map +1 -1
  96. package/dist/progress-recipe.d.ts +14 -0
  97. package/dist/progress-recipe.d.ts.map +1 -1
  98. package/dist/progress-recipe.js +14 -1
  99. package/dist/progress-recipe.js.map +1 -1
  100. package/dist/provider-button.d.ts +140 -0
  101. package/dist/provider-button.d.ts.map +1 -0
  102. package/dist/provider-button.js +83 -0
  103. package/dist/provider-button.js.map +1 -0
  104. package/dist/recipes.d.ts +16 -1
  105. package/dist/recipes.d.ts.map +1 -1
  106. package/dist/recipes.js +15 -0
  107. package/dist/recipes.js.map +1 -1
  108. package/dist/sheet.d.ts +16 -0
  109. package/dist/sheet.d.ts.map +1 -1
  110. package/dist/sheet.js +33 -0
  111. package/dist/sheet.js.map +1 -1
  112. package/dist/sidebar.d.ts +168 -0
  113. package/dist/sidebar.d.ts.map +1 -0
  114. package/dist/sidebar.js +98 -0
  115. package/dist/sidebar.js.map +1 -0
  116. package/dist/skip-nav.d.ts +67 -0
  117. package/dist/skip-nav.d.ts.map +1 -0
  118. package/dist/skip-nav.js +47 -0
  119. package/dist/skip-nav.js.map +1 -0
  120. package/dist/tags-input.d.ts +157 -0
  121. package/dist/tags-input.d.ts.map +1 -0
  122. package/dist/tags-input.js +108 -0
  123. package/dist/tags-input.js.map +1 -0
  124. package/dist/text-formats.d.ts +85 -0
  125. package/dist/text-formats.d.ts.map +1 -0
  126. package/dist/text-formats.js +43 -0
  127. package/dist/text-formats.js.map +1 -0
  128. package/dist/toggle-group.d.ts +111 -0
  129. package/dist/toggle-group.d.ts.map +1 -0
  130. package/dist/toggle-group.js +78 -0
  131. package/dist/toggle-group.js.map +1 -0
  132. package/dist/top.d.ts +113 -0
  133. package/dist/top.d.ts.map +1 -0
  134. package/dist/top.js +72 -0
  135. package/dist/top.js.map +1 -0
  136. package/dist/version.d.ts +1 -1
  137. package/dist/version.js +1 -1
  138. package/dist/version.js.map +1 -1
  139. package/docs/agreement.md +33 -0
  140. package/docs/anchor.md +59 -59
  141. package/docs/ant-design-coverage.md +5 -3
  142. package/docs/asset.md +26 -0
  143. package/docs/auth-screen.md +65 -0
  144. package/docs/bottom-info.md +15 -0
  145. package/docs/breadcrumb.md +15 -15
  146. package/docs/button-label.md +20 -0
  147. package/docs/calendar.md +82 -154
  148. package/docs/carousel.md +27 -2
  149. package/docs/cascader.md +17 -0
  150. package/docs/chart.md +33 -0
  151. package/docs/clipboard.md +13 -0
  152. package/docs/collapsible.md +19 -0
  153. package/docs/command-palette.md +19 -0
  154. package/docs/confirm-popover.md +11 -1
  155. package/docs/context-menu.md +23 -0
  156. package/docs/data-table.md +22 -0
  157. package/docs/date-range.md +31 -0
  158. package/docs/density.md +52 -0
  159. package/docs/expansion-roadmap.md +16 -3
  160. package/docs/floating-action-button.md +35 -3
  161. package/docs/formatters.md +18 -0
  162. package/docs/generated/component-maturity.md +40 -23
  163. package/docs/generated/renderer-evidence.json +3029 -652
  164. package/docs/generated/renderer-evidence.md +55 -6
  165. package/docs/generated/showcase-manifest.json +1156 -81
  166. package/docs/heading.md +25 -0
  167. package/docs/identity.md +5 -0
  168. package/docs/layout.md +1 -1
  169. package/docs/library-gap-analysis.md +29 -9
  170. package/docs/list-row.md +14 -0
  171. package/docs/mentions.md +18 -0
  172. package/docs/menubar.md +22 -0
  173. package/docs/native-platform.md +36 -0
  174. package/docs/overlay-stack.md +24 -0
  175. package/docs/pagination.md +16 -8
  176. package/docs/popover.md +42 -13
  177. package/docs/product-audit-2026-09-15.md +88 -0
  178. package/docs/progress.md +16 -0
  179. package/docs/provider-button.md +33 -0
  180. package/docs/rating.md +19 -0
  181. package/docs/react-native-completion.md +347 -0
  182. package/docs/screen-chrome.md +52 -0
  183. package/docs/side-panel.md +23 -2
  184. package/docs/sidebar.md +30 -0
  185. package/docs/skip-nav.md +20 -0
  186. package/docs/splitter.md +22 -2
  187. package/docs/stable-promotion.md +51 -0
  188. package/docs/tags-input.md +25 -0
  189. package/docs/text-formats.md +15 -0
  190. package/docs/theming.md +84 -0
  191. package/docs/time-picker.md +22 -1
  192. package/docs/toast.md +18 -0
  193. package/docs/toggle-group.md +21 -0
  194. package/docs/top.md +32 -0
  195. package/docs/tour.md +23 -2
  196. package/docs/transfer-list.md +20 -0
  197. package/docs/tree-select.md +18 -0
  198. package/docs/tree.md +21 -0
  199. package/package.json +126 -6
@@ -0,0 +1,108 @@
1
+ import { fieldFrameContract, focusIndicatorContract } from "./component-contracts.js";
2
+ import { control, spacing } from "./foundations.js";
3
+ import { semanticColors } from "./semantic-colors.js";
4
+ /**
5
+ * 활성 후보를 방향키로 옮긴 결과. 목록 끝에서 순환한다 — 후보가 보통 짧아서 끝에
6
+ * 막히면 반대 끝까지 다시 내려가야 한다.
7
+ */
8
+ export function resolveTagsInputActiveSuggestion(suggestions, currentId, direction) {
9
+ const enabled = suggestions.filter((item) => item.disabled !== true);
10
+ if (enabled.length === 0)
11
+ return null;
12
+ const index = enabled.findIndex((item) => item.id === currentId);
13
+ if (index === -1)
14
+ return direction === "next" ? enabled[0].id : enabled.at(-1).id;
15
+ const next = direction === "next" ? index + 1 : index - 1;
16
+ return enabled[(next + enabled.length) % enabled.length].id;
17
+ }
18
+ export const tagsInputBehaviorDefaults = {
19
+ /**
20
+ * Enter만 기본이다. 쉼표·공백은 언어에 따라 값의 일부다(한국어 태그에 공백이 흔하다).
21
+ * 제품이 자기 도메인을 알 때 켠다.
22
+ */
23
+ commitKeys: ["Enter"],
24
+ /** 빈 입력에서 Backspace는 마지막 태그를 **선택**만 한다. 한 번 더 눌러야 지운다. */
25
+ backspaceRemovesLastTag: false,
26
+ };
27
+ /**
28
+ * 태그 하나를 확정할 수 있는지 판정한다. 값을 바꾸지 않고 결과만 돌려준다 —
29
+ * 실제 상태 갱신은 renderer가, 사용자에게 보여 줄 문장은 제품이 만든다.
30
+ */
31
+ export function resolveTagsInputCommit(current, rawValue, policy = {}) {
32
+ const value = rawValue.trim();
33
+ if (value.length === 0)
34
+ return { accepted: false, value, reason: "empty" };
35
+ if (policy.allowDuplicates !== true && current.includes(value)) {
36
+ return { accepted: false, value, reason: "duplicate" };
37
+ }
38
+ if (policy.maxTags !== undefined && current.length >= policy.maxTags) {
39
+ return { accepted: false, value, reason: "limit" };
40
+ }
41
+ if (policy.isValid !== undefined && !policy.isValid(value)) {
42
+ return { accepted: false, value, reason: "invalid" };
43
+ }
44
+ return { accepted: true, value };
45
+ }
46
+ /** 태그 하나를 뺀 목록. 같은 값이 여러 개면 해당 위치 하나만 지운다. */
47
+ export function removeTagAt(current, index) {
48
+ if (!Number.isInteger(index) || index < 0 || index >= current.length) {
49
+ throw new RangeError(`TagsInput index out of range: ${String(index)}`);
50
+ }
51
+ return [...current.slice(0, index), ...current.slice(index + 1)];
52
+ }
53
+ export const tagsInputRecipe = {
54
+ slots: ["root", "frame", "tag", "remove", "input"],
55
+ defaults: { commitKeys: tagsInputBehaviorDefaults.commitKeys },
56
+ frame: {
57
+ minHeight: fieldFrameContract.minHeight,
58
+ paddingHorizontal: fieldFrameContract.paddingHorizontal,
59
+ paddingVertical: spacing.xxs,
60
+ gap: spacing.xs,
61
+ border: fieldFrameContract.border,
62
+ background: fieldFrameContract.background,
63
+ },
64
+ tag: {
65
+ minHeight: 28,
66
+ paddingHorizontal: spacing.xs,
67
+ gap: spacing.xxs,
68
+ background: semanticColors.surface.sunken,
69
+ color: semanticColors.content.body,
70
+ },
71
+ /** 삭제 버튼은 태그보다 작아도 되지만 포인터 타깃은 공용 최소값을 지킨다. */
72
+ remove: { minTouchTarget: control.minTouchTarget, color: semanticColors.content.secondary },
73
+ selectedTagOutline: semanticColors.border.focus,
74
+ states: { focus: focusIndicatorContract },
75
+ };
76
+ export const tagsInputBehavior = {
77
+ controlled: ["tags", "defaultTags", "onTagsChange"],
78
+ inputs: ["policy", "commitKeys", "label", "removeLabel"],
79
+ /**
80
+ * `onDraftChange`가 없으면 제품이 후보를 거를 수 없다 — 무엇을 입력 중인지 모르기
81
+ * 때문이다. 후보 목록을 계약에 넣는 순간 이 이벤트도 함께 필요하다.
82
+ */
83
+ events: ["onReject", "onDraftChange"],
84
+ defaults: { backspaceRemovesLastTag: tagsInputBehaviorDefaults.backspaceRemovesLastTag },
85
+ stateAxes: {
86
+ availability: ["enabled", "disabled"],
87
+ value: ["empty", "filled"],
88
+ validation: ["valid", "invalid"],
89
+ },
90
+ web: {
91
+ roles: ["combobox", "listbox", "option", "button", "list", "listitem"],
92
+ keyboard: ["Enter", "Backspace", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape", "Tab"],
93
+ focus: "native",
94
+ },
95
+ native: { roles: ["button"], states: ["disabled"], actions: ["commit", "remove"] },
96
+ scenarios: [
97
+ "enter-commits-the-trimmed-value-and-clears-the-input",
98
+ "an-empty-or-whitespace-only-value-is-never-committed",
99
+ "duplicates-are-rejected-by-default-and-the-reason-is-reported-not-swallowed",
100
+ "backspace-in-an-empty-input-selects-the-last-tag-before-a-second-press-removes-it",
101
+ "every-tag-carries-its-own-remove-control-with-a-localized-name",
102
+ "the-policy-judges-and-the-product-writes-the-sentence-the-user-reads",
103
+ "a-candidate-list-lives-in-this-field-because-the-value-shape-is-this-fields-not-comboboxs",
104
+ "enter-commits-the-active-candidate-when-one-is-highlighted-and-the-typed-text-otherwise",
105
+ "candidate-filtering-belongs-to-the-product-the-contract-only-commits-the-choice",
106
+ ],
107
+ };
108
+ //# sourceMappingURL=tags-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tags-input.js","sourceRoot":"","sources":["../src/tags-input.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BehaviorContract } from \"./behaviors.js\";\nimport type { ColorReference } from \"./color-references.js\";\nimport { fieldFrameContract, focusIndicatorContract } from \"./component-contracts.js\";\nimport { control, spacing } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\n/**\n * 자유 입력으로 여러 값을 모으는 필드 — 해시태그, 초대할 사람, 검색 필터.\n *\n * Combobox와 다르다. Combobox는 **주어진 목록에서 고른다**. TagsInput은 목록이 없거나\n * 있어도 목록 밖의 값을 만들 수 있다. 그래서 Combobox에 `multiple` 축을 붙이는 대신\n * 별도로 둔다 — 합치면 \"목록에 없는 값을 고를 수 있는 Select\"라는 모순이 생긴다.\n * (제품이 후보 목록을 함께 보여 주고 싶으면 이 필드와 Combobox의 listbox를 조합한다.)\n *\n * 이 계약이 소유하는 것은 **언제 태그가 확정되는가**와 **빈 입력에서 지우면 무엇이\n * 지워지는가** 둘이다. 중복 허용 여부·최대 개수·글자 규칙은 제품 정책이라 판정 결과를\n * 받기만 한다.\n */\nexport type TagsInputCommitKey = \"Enter\" | \"Comma\" | \"Space\" | \"Blur\";\n\n/**\n * 후보 목록. Combobox의 listbox 계약을 **이 필드 안에서** 쓰는 형태다.\n *\n * \"Combobox에 multiple을 붙이자\"가 아니라 여기로 온 이유: Combobox의 값은 목록 안의\n * 키 하나이고 이 필드의 값은 문자열 배열이다. 값 모양이 다른 둘을 한 컴포넌트에 넣으면\n * 모든 소비자가 매번 좁히기를 해야 한다. 자유 입력이 이미 여기 있으므로 후보를 여기에\n * 더하는 쪽이 값 하나로 끝난다.\n *\n * 필터링은 제품이 한다 — 이 계약은 \"무엇을 보여줄지\"가 아니라 \"고른 것을 어떻게\n * 확정할지\"만 안다.\n */\nexport type TagsInputSuggestion = Readonly<{\n id: string;\n label: string;\n /** 확정될 문자열. 생략하면 `label`. */\n value?: string;\n disabled?: boolean;\n}>;\n\n/**\n * 활성 후보를 방향키로 옮긴 결과. 목록 끝에서 순환한다 — 후보가 보통 짧아서 끝에\n * 막히면 반대 끝까지 다시 내려가야 한다.\n */\nexport function resolveTagsInputActiveSuggestion(\n suggestions: readonly TagsInputSuggestion[],\n currentId: string | null,\n direction: \"next\" | \"previous\",\n): string | null {\n const enabled = suggestions.filter((item) => item.disabled !== true);\n if (enabled.length === 0) return null;\n const index = enabled.findIndex((item) => item.id === currentId);\n if (index === -1) return direction === \"next\" ? enabled[0]!.id : enabled.at(-1)!.id;\n const next = direction === \"next\" ? index + 1 : index - 1;\n return enabled[(next + enabled.length) % enabled.length]!.id;\n}\n\nexport const tagsInputBehaviorDefaults = {\n /**\n * Enter만 기본이다. 쉼표·공백은 언어에 따라 값의 일부다(한국어 태그에 공백이 흔하다).\n * 제품이 자기 도메인을 알 때 켠다.\n */\n commitKeys: [\"Enter\"] as readonly TagsInputCommitKey[],\n /** 빈 입력에서 Backspace는 마지막 태그를 **선택**만 한다. 한 번 더 눌러야 지운다. */\n backspaceRemovesLastTag: false,\n} as const;\n\nexport type TagsInputRejectionReason = \"empty\" | \"duplicate\" | \"limit\" | \"invalid\";\n\nexport type TagsInputCommitResult = Readonly<{\n accepted: boolean;\n /** 정리된 값(앞뒤 공백 제거). 거절돼도 제품이 문구에 쓸 수 있게 돌려준다. */\n value: string;\n reason?: TagsInputRejectionReason;\n}>;\n\nexport type TagsInputPolicy = Readonly<{\n /** 같은 값을 다시 넣을 수 있는가. 기본은 막는다. */\n allowDuplicates?: boolean;\n maxTags?: number;\n /** 제품 도메인 규칙. 실패 사유는 `invalid` 하나로 합쳐 돌려준다. */\n isValid?: (value: string) => boolean;\n}>;\n\n/**\n * 태그 하나를 확정할 수 있는지 판정한다. 값을 바꾸지 않고 결과만 돌려준다 —\n * 실제 상태 갱신은 renderer가, 사용자에게 보여 줄 문장은 제품이 만든다.\n */\nexport function resolveTagsInputCommit(\n current: readonly string[],\n rawValue: string,\n policy: TagsInputPolicy = {},\n): TagsInputCommitResult {\n const value = rawValue.trim();\n if (value.length === 0) return { accepted: false, value, reason: \"empty\" };\n if (policy.allowDuplicates !== true && current.includes(value)) {\n return { accepted: false, value, reason: \"duplicate\" };\n }\n if (policy.maxTags !== undefined && current.length >= policy.maxTags) {\n return { accepted: false, value, reason: \"limit\" };\n }\n if (policy.isValid !== undefined && !policy.isValid(value)) {\n return { accepted: false, value, reason: \"invalid\" };\n }\n return { accepted: true, value };\n}\n\n/** 태그 하나를 뺀 목록. 같은 값이 여러 개면 해당 위치 하나만 지운다. */\nexport function removeTagAt(current: readonly string[], index: number): readonly string[] {\n if (!Number.isInteger(index) || index < 0 || index >= current.length) {\n throw new RangeError(`TagsInput index out of range: ${String(index)}`);\n }\n return [...current.slice(0, index), ...current.slice(index + 1)];\n}\n\nexport const tagsInputRecipe = {\n slots: [\"root\", \"frame\", \"tag\", \"remove\", \"input\"] as const,\n defaults: { commitKeys: tagsInputBehaviorDefaults.commitKeys },\n frame: {\n minHeight: fieldFrameContract.minHeight,\n paddingHorizontal: fieldFrameContract.paddingHorizontal,\n paddingVertical: spacing.xxs,\n gap: spacing.xs,\n border: fieldFrameContract.border,\n background: fieldFrameContract.background,\n },\n tag: {\n minHeight: 28,\n paddingHorizontal: spacing.xs,\n gap: spacing.xxs,\n background: semanticColors.surface.sunken,\n color: semanticColors.content.body,\n },\n /** 삭제 버튼은 태그보다 작아도 되지만 포인터 타깃은 공용 최소값을 지킨다. */\n remove: { minTouchTarget: control.minTouchTarget, color: semanticColors.content.secondary },\n selectedTagOutline: semanticColors.border.focus,\n states: { focus: focusIndicatorContract },\n} as const satisfies {\n slots: readonly [\"root\", \"frame\", \"tag\", \"remove\", \"input\"];\n defaults: { commitKeys: readonly TagsInputCommitKey[] };\n frame: {\n minHeight: number;\n paddingHorizontal: number;\n paddingVertical: number;\n gap: number;\n border: ColorReference;\n background: ColorReference;\n };\n tag: { minHeight: number; paddingHorizontal: number; gap: number; background: ColorReference; color: ColorReference };\n remove: { minTouchTarget: number; color: ColorReference };\n selectedTagOutline: ColorReference;\n states: { focus: typeof focusIndicatorContract };\n};\n\nexport const tagsInputBehavior = {\n controlled: [\"tags\", \"defaultTags\", \"onTagsChange\"],\n inputs: [\"policy\", \"commitKeys\", \"label\", \"removeLabel\"],\n /**\n * `onDraftChange`가 없으면 제품이 후보를 거를 수 없다 — 무엇을 입력 중인지 모르기\n * 때문이다. 후보 목록을 계약에 넣는 순간 이 이벤트도 함께 필요하다.\n */\n events: [\"onReject\", \"onDraftChange\"],\n defaults: { backspaceRemovesLastTag: tagsInputBehaviorDefaults.backspaceRemovesLastTag },\n stateAxes: {\n availability: [\"enabled\", \"disabled\"],\n value: [\"empty\", \"filled\"],\n validation: [\"valid\", \"invalid\"],\n },\n web: {\n roles: [\"combobox\", \"listbox\", \"option\", \"button\", \"list\", \"listitem\"],\n keyboard: [\"Enter\", \"Backspace\", \"ArrowUp\", \"ArrowDown\", \"ArrowLeft\", \"ArrowRight\", \"Escape\", \"Tab\"],\n focus: \"native\",\n },\n native: { roles: [\"button\"], states: [\"disabled\"], actions: [\"commit\", \"remove\"] },\n scenarios: [\n \"enter-commits-the-trimmed-value-and-clears-the-input\",\n \"an-empty-or-whitespace-only-value-is-never-committed\",\n \"duplicates-are-rejected-by-default-and-the-reason-is-reported-not-swallowed\",\n \"backspace-in-an-empty-input-selects-the-last-tag-before-a-second-press-removes-it\",\n \"every-tag-carries-its-own-remove-control-with-a-localized-name\",\n \"the-policy-judges-and-the-product-writes-the-sentence-the-user-reads\",\n \"a-candidate-list-lives-in-this-field-because-the-value-shape-is-this-fields-not-comboboxs\",\n \"enter-commits-the-active-candidate-when-one-is-highlighted-and-the-typed-text-otherwise\",\n \"candidate-filtering-belongs-to-the-product-the-contract-only-commits-the-choice\",\n ],\n} as const satisfies BehaviorContract;\n"]}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `Text` 하나로는 만들 수 없는 **의미 있는 글자 조각들**이다 — 단축키, 코드, 인용.
3
+ * 도움말·개발자 문서·약관 본문이 반복해서 필요로 하고, 지금까지는 제품이 `<kbd>`에
4
+ * 직접 CSS를 붙여 만들었다.
5
+ *
6
+ * 왜 `Text`의 variant가 아닌가: 이들은 **크기가 아니라 요소**다. `<kbd>`, `<code>`,
7
+ * `<blockquote>`는 각자 의미를 가진 HTML 요소이고 보조기기가 다르게 읽는다. variant로
8
+ * 두면 `<span>`에 코드처럼 보이는 스타일만 입히게 되고, 그 차이가 사라진다.
9
+ */
10
+ export type TextFormatKind = "kbd" | "code" | "quote";
11
+ export declare const textFormatRecipe: {
12
+ readonly slots: readonly ["root"];
13
+ readonly kbd: {
14
+ readonly fontFamily: readonly ["ui-monospace", "SFMono-Regular", "Consolas", "Liberation Mono", "monospace"];
15
+ readonly textVariant: "label";
16
+ readonly paddingHorizontal: 4;
17
+ readonly radius: 8;
18
+ readonly background: Readonly<{
19
+ source: "theme";
20
+ key: "surfaceAlt";
21
+ alpha?: number;
22
+ }>;
23
+ readonly color: Readonly<{
24
+ source: "theme";
25
+ key: "textBody";
26
+ alpha?: number;
27
+ }>;
28
+ readonly border: Readonly<{
29
+ source: "theme";
30
+ key: "border";
31
+ alpha?: number;
32
+ }>;
33
+ };
34
+ readonly code: {
35
+ readonly fontFamily: readonly ["ui-monospace", "SFMono-Regular", "Consolas", "Liberation Mono", "monospace"];
36
+ readonly textVariant: "body";
37
+ readonly paddingHorizontal: 4;
38
+ readonly radius: 8;
39
+ readonly background: Readonly<{
40
+ source: "theme";
41
+ key: "surfaceAlt";
42
+ alpha?: number;
43
+ }>;
44
+ readonly color: Readonly<{
45
+ source: "theme";
46
+ key: "text";
47
+ alpha?: number;
48
+ }>;
49
+ };
50
+ readonly quote: {
51
+ readonly textVariant: "body";
52
+ readonly paddingInlineStart: 16;
53
+ readonly borderWidth: 2;
54
+ readonly border: Readonly<{
55
+ source: "theme";
56
+ key: "border";
57
+ alpha?: number;
58
+ }>;
59
+ readonly color: Readonly<{
60
+ source: "theme";
61
+ key: "textBody";
62
+ alpha?: number;
63
+ }>;
64
+ };
65
+ };
66
+ export declare const textFormatBehavior: {
67
+ readonly controlled: readonly [];
68
+ readonly inputs: readonly ["kind"];
69
+ readonly configuration: {
70
+ readonly kind: readonly ["kbd", "code", "quote"];
71
+ };
72
+ readonly stateAxes: {};
73
+ readonly web: {
74
+ readonly roles: readonly [];
75
+ readonly keyboard: readonly [];
76
+ readonly focus: "none";
77
+ };
78
+ readonly native: {
79
+ readonly roles: readonly [];
80
+ readonly states: readonly [];
81
+ readonly actions: readonly [];
82
+ };
83
+ readonly scenarios: readonly ["each-kind-emits-its-own-html-element-so-assistive-technology-reads-it-as-what-it-is", "these-are-elements-not-text-sizes-which-is-why-they-are-not-a-text-variant", "a-key-name-is-product-copy-because-the-same-key-is-called-different-things-per-platform"];
84
+ };
85
+ //# sourceMappingURL=text-formats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-formats.d.ts","sourceRoot":"","sources":["../src/text-formats.ts"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAEtD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoD5B,CAAC;AAEF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;CAYM,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { fontFamily, radius, spacing, typography } from "./foundations.js";
2
+ import { semanticColors } from "./semantic-colors.js";
3
+ export const textFormatRecipe = {
4
+ slots: ["root"],
5
+ kbd: {
6
+ fontFamily: fontFamily.code,
7
+ textVariant: "label",
8
+ paddingHorizontal: spacing.xxs,
9
+ radius: radius.sm,
10
+ background: semanticColors.surface.sunken,
11
+ color: semanticColors.content.body,
12
+ border: semanticColors.border.default,
13
+ },
14
+ code: {
15
+ fontFamily: fontFamily.code,
16
+ textVariant: "body",
17
+ paddingHorizontal: spacing.xxs,
18
+ radius: radius.sm,
19
+ background: semanticColors.surface.sunken,
20
+ color: semanticColors.content.primary,
21
+ },
22
+ quote: {
23
+ textVariant: "body",
24
+ paddingInlineStart: spacing.md,
25
+ borderWidth: 2,
26
+ border: semanticColors.border.default,
27
+ color: semanticColors.content.body,
28
+ },
29
+ };
30
+ export const textFormatBehavior = {
31
+ controlled: [],
32
+ inputs: ["kind"],
33
+ configuration: { kind: ["kbd", "code", "quote"] },
34
+ stateAxes: {},
35
+ web: { roles: [], keyboard: [], focus: "none" },
36
+ native: { roles: [], states: [], actions: [] },
37
+ scenarios: [
38
+ "each-kind-emits-its-own-html-element-so-assistive-technology-reads-it-as-what-it-is",
39
+ "these-are-elements-not-text-sizes-which-is-why-they-are-not-a-text-variant",
40
+ "a-key-name-is-product-copy-because-the-same-key-is-called-different-things-per-platform",
41
+ ],
42
+ };
43
+ //# sourceMappingURL=text-formats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-formats.js","sourceRoot":"","sources":["../src/text-formats.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC3E,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAatD,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,KAAK,EAAE,CAAC,MAAM,CAAU;IACxB,GAAG,EAAE;QACH,UAAU,EAAE,UAAU,CAAC,IAAI;QAC3B,WAAW,EAAE,OAAgB;QAC7B,iBAAiB,EAAE,OAAO,CAAC,GAAG;QAC9B,MAAM,EAAE,MAAM,CAAC,EAAE;QACjB,UAAU,EAAE,cAAc,CAAC,OAAO,CAAC,MAAM;QACzC,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,IAAI;QAClC,MAAM,EAAE,cAAc,CAAC,MAAM,CAAC,OAAO;KACtC;IACD,IAAI,EAAE;QACJ,UAAU,EAAE,UAAU,CAAC,IAAI;QAC3B,WAAW,EAAE,MAAe;QAC5B,iBAAiB,EAAE,OAAO,CAAC,GAAG;QAC9B,MAAM,EAAE,MAAM,CAAC,EAAE;QACjB,UAAU,EAAE,cAAc,CAAC,OAAO,CAAC,MAAM;QACzC,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,OAAO;KACtC;IACD,KAAK,EAAE;QACL,WAAW,EAAE,MAAe;QAC5B,kBAAkB,EAAE,OAAO,CAAC,EAAE;QAC9B,WAAW,EAAE,CAAC;QACd,MAAM,EAAE,cAAc,CAAC,MAAM,CAAC,OAAO;QACrC,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,IAAI;KACnC;CA2BF,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,UAAU,EAAE,EAAE;IACd,MAAM,EAAE,CAAC,MAAM,CAAC;IAChB,aAAa,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IACjD,SAAS,EAAE,EAAE;IACb,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE;IAC/C,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;IAC9C,SAAS,EAAE;QACT,qFAAqF;QACrF,4EAA4E;QAC5E,yFAAyF;KAC1F;CACkC,CAAC","sourcesContent":["import type { BehaviorContract } from \"./behaviors.js\";\nimport type { ColorReference } from \"./color-references.js\";\nimport { fontFamily, radius, spacing, typography } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\n/**\n * `Text` 하나로는 만들 수 없는 **의미 있는 글자 조각들**이다 — 단축키, 코드, 인용.\n * 도움말·개발자 문서·약관 본문이 반복해서 필요로 하고, 지금까지는 제품이 `<kbd>`에\n * 직접 CSS를 붙여 만들었다.\n *\n * 왜 `Text`의 variant가 아닌가: 이들은 **크기가 아니라 요소**다. `<kbd>`, `<code>`,\n * `<blockquote>`는 각자 의미를 가진 HTML 요소이고 보조기기가 다르게 읽는다. variant로\n * 두면 `<span>`에 코드처럼 보이는 스타일만 입히게 되고, 그 차이가 사라진다.\n */\nexport type TextFormatKind = \"kbd\" | \"code\" | \"quote\";\n\nexport const textFormatRecipe = {\n slots: [\"root\"] as const,\n kbd: {\n fontFamily: fontFamily.code,\n textVariant: \"label\" as const,\n paddingHorizontal: spacing.xxs,\n radius: radius.sm,\n background: semanticColors.surface.sunken,\n color: semanticColors.content.body,\n border: semanticColors.border.default,\n },\n code: {\n fontFamily: fontFamily.code,\n textVariant: \"body\" as const,\n paddingHorizontal: spacing.xxs,\n radius: radius.sm,\n background: semanticColors.surface.sunken,\n color: semanticColors.content.primary,\n },\n quote: {\n textVariant: \"body\" as const,\n paddingInlineStart: spacing.md,\n borderWidth: 2,\n border: semanticColors.border.default,\n color: semanticColors.content.body,\n },\n} as const satisfies {\n slots: readonly [\"root\"];\n kbd: {\n fontFamily: typeof fontFamily.code;\n textVariant: keyof typeof typography;\n paddingHorizontal: number;\n radius: number;\n background: ColorReference;\n color: ColorReference;\n border: ColorReference;\n };\n code: {\n fontFamily: typeof fontFamily.code;\n textVariant: keyof typeof typography;\n paddingHorizontal: number;\n radius: number;\n background: ColorReference;\n color: ColorReference;\n };\n quote: {\n textVariant: keyof typeof typography;\n paddingInlineStart: number;\n borderWidth: number;\n border: ColorReference;\n color: ColorReference;\n };\n};\n\nexport const textFormatBehavior = {\n controlled: [],\n inputs: [\"kind\"],\n configuration: { kind: [\"kbd\", \"code\", \"quote\"] },\n stateAxes: {},\n web: { roles: [], keyboard: [], focus: \"none\" },\n native: { roles: [], states: [], actions: [] },\n scenarios: [\n \"each-kind-emits-its-own-html-element-so-assistive-technology-reads-it-as-what-it-is\",\n \"these-are-elements-not-text-sizes-which-is-why-they-are-not-a-text-variant\",\n \"a-key-name-is-product-copy-because-the-same-key-is-called-different-things-per-platform\",\n ],\n} as const satisfies BehaviorContract;\n"]}
@@ -0,0 +1,111 @@
1
+ import { type CheckboxSelectionItem } from "./selection-helpers.js";
2
+ /**
3
+ * 여러 개를 동시에 켜고 끄는 버튼 묶음이다 — 굵게/기울임/밑줄, 필터 칩 여러 개처럼.
4
+ *
5
+ * `SegmentedControl`과 경계가 분명하다. Segmented는 **하나를 고른다**(어떤 화면을 볼지),
6
+ * ToggleGroup은 **여러 개를 켠다**(무엇을 적용할지). 그래서 Segmented에는 "선택 없음"이
7
+ * 없고 ToggleGroup에는 있다. 둘을 한 컴포넌트의 `mode` 축으로 합치면 그 차이가 사라지고,
8
+ * "아무것도 선택되지 않은 Segmented"라는 표현 불가능한 상태가 타입에 생긴다.
9
+ *
10
+ * `CheckboxGroup`과도 다르다. CheckboxGroup은 목록 안의 동의/선택이라 각 항목이 자기
11
+ * 줄과 설명을 갖는다. ToggleGroup은 도구 모음이라 한 줄에 붙어 있고 라벨이 짧다.
12
+ * 판정(무엇을 켤 수 있고 비활성은 어떻게 되는가)은 같은 helper를 공유한다.
13
+ */
14
+ export type ToggleGroupItemDescriptor<Id extends string = string> = CheckboxSelectionItem<Id>;
15
+ export type ToggleGroupDescriptor<Id extends string = string> = Readonly<{
16
+ /** 묶음 전체의 접근성 이름. 개별 버튼 이름으로 대체할 수 없다. */
17
+ accessibilityLabel: string;
18
+ items: readonly ToggleGroupItemDescriptor<Id>[];
19
+ }>;
20
+ export type ToggleGroupSize = "small" | "medium";
21
+ export declare function validateToggleGroupDescriptor<Id extends string>(descriptor: ToggleGroupDescriptor<Id>): void;
22
+ /** 하나를 켜고 끈다. 비활성 보호는 공용 helper가 이미 갖고 있다. */
23
+ export declare function toggleGroupSelection<Id extends string>(descriptor: ToggleGroupDescriptor<Id>, pressedIds: ReadonlySet<Id>, id: Id): ReadonlySet<Id>;
24
+ /** 목록에서 사라진 항목의 눌림 상태는 남기지 않는다. */
25
+ export declare function reconcileToggleGroupSelection<Id extends string>(descriptor: ToggleGroupDescriptor<Id>, pressedIds: ReadonlySet<Id>): ReadonlySet<Id>;
26
+ export declare const toggleGroupRecipe: {
27
+ readonly slots: readonly ["root", "item", "label"];
28
+ readonly defaults: {
29
+ readonly size: ToggleGroupSize;
30
+ };
31
+ readonly sizes: {
32
+ readonly small: {
33
+ readonly minHeight: 44;
34
+ readonly paddingHorizontal: 12;
35
+ readonly textVariant: "label";
36
+ };
37
+ readonly medium: {
38
+ readonly minHeight: 44;
39
+ readonly paddingHorizontal: 16;
40
+ readonly textVariant: "body";
41
+ };
42
+ };
43
+ readonly radius: 12;
44
+ readonly gap: 4;
45
+ readonly idle: {
46
+ readonly background: Readonly<{
47
+ source: "theme";
48
+ key: "surface";
49
+ alpha?: number;
50
+ }>;
51
+ readonly color: Readonly<{
52
+ source: "theme";
53
+ key: "textBody";
54
+ alpha?: number;
55
+ }>;
56
+ readonly border: Readonly<{
57
+ source: "theme";
58
+ key: "borderControl";
59
+ alpha?: number;
60
+ }>;
61
+ };
62
+ readonly pressed: {
63
+ readonly background: Readonly<{
64
+ source: "theme";
65
+ key: "primary";
66
+ alpha?: number;
67
+ }>;
68
+ readonly color: Readonly<{
69
+ source: "theme";
70
+ key: "contentBrand";
71
+ alpha?: number;
72
+ }>;
73
+ readonly border: Readonly<{
74
+ source: "theme";
75
+ key: "contentBrand";
76
+ alpha?: number;
77
+ }>;
78
+ };
79
+ readonly states: {
80
+ readonly focus: {
81
+ readonly color: Readonly<{
82
+ source: "theme";
83
+ key: "contentBrand";
84
+ alpha?: number;
85
+ }>;
86
+ readonly width: 2;
87
+ readonly offset: 2;
88
+ };
89
+ };
90
+ };
91
+ export declare const toggleGroupBehavior: {
92
+ readonly controlled: readonly ["pressedIds", "defaultPressedIds", "onPressedIdsChange"];
93
+ readonly inputs: readonly ["items", "accessibilityLabel", "size"];
94
+ readonly stateAxes: {
95
+ readonly availability: readonly ["enabled", "disabled"];
96
+ readonly value: readonly ["pressed", "unpressed"];
97
+ };
98
+ readonly web: {
99
+ readonly roles: readonly ["group", "button"];
100
+ readonly keyboard: readonly ["Tab", "Enter", "Space"];
101
+ /** Toolbar-style roving focus is not used: each toggle is its own tab stop, like a row of checkboxes. */
102
+ readonly focus: "native";
103
+ };
104
+ readonly native: {
105
+ readonly roles: readonly ["button"];
106
+ readonly states: readonly ["selected", "disabled"];
107
+ readonly actions: readonly ["toggle"];
108
+ };
109
+ readonly scenarios: readonly ["several-items-can-be-pressed-at-once-and-none-is-a-valid-state", "pressed-state-rides-on-aria-pressed-not-on-color-alone", "a-disabled-item-never-toggles-and-never-enters-the-pressed-set", "removing-an-item-drops-its-pressed-state-instead-of-keeping-an-orphan-id", "single-choice-belongs-to-segmented-control-this-contract-never-adds-a-single-mode", "the-group-carries-its-own-accessible-name-separate-from-each-buttons-name"];
110
+ };
111
+ //# sourceMappingURL=toggle-group.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toggle-group.d.ts","sourceRoot":"","sources":["../src/toggle-group.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,wBAAwB,CAAC;AAEhC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,yBAAyB,CAAC,EAAE,SAAS,MAAM,GAAG,MAAM,IAC9D,qBAAqB,CAAC,EAAE,CAAC,CAAC;AAE5B,MAAM,MAAM,qBAAqB,CAAC,EAAE,SAAS,MAAM,GAAG,MAAM,IAAI,QAAQ,CAAC;IACvE,0CAA0C;IAC1C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,SAAS,yBAAyB,CAAC,EAAE,CAAC,EAAE,CAAC;CACjD,CAAC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,QAAQ,CAAC;AAQjD,wBAAgB,6BAA6B,CAAC,EAAE,SAAS,MAAM,EAC7D,UAAU,EAAE,qBAAqB,CAAC,EAAE,CAAC,GACpC,IAAI,CAYN;AAED,8CAA8C;AAC9C,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,MAAM,EACpD,UAAU,EAAE,qBAAqB,CAAC,EAAE,CAAC,EACrC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC,EAC3B,EAAE,EAAE,EAAE,GACL,WAAW,CAAC,EAAE,CAAC,CAGjB;AAED,mCAAmC;AACnC,wBAAgB,6BAA6B,CAAC,EAAE,SAAS,MAAM,EAC7D,UAAU,EAAE,qBAAqB,CAAC,EAAE,CAAC,EACrC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC,GAC1B,WAAW,CAAC,EAAE,CAAC,CAGjB;AAED,eAAO,MAAM,iBAAiB;;;uBAEE,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B9C,CAAC;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;;QAU5B,yGAAyG;;;;;;;;;CAYxE,CAAC"}
@@ -0,0 +1,78 @@
1
+ import { collectionItemContract, focusIndicatorContract } from "./component-contracts.js";
2
+ import { control, radius, spacing } from "./foundations.js";
3
+ import { semanticColors } from "./semantic-colors.js";
4
+ import { reconcileCheckboxSelection, toggleCheckboxSelection, } from "./selection-helpers.js";
5
+ function assertNonEmpty(value, field) {
6
+ if (typeof value !== "string" || value.trim().length === 0) {
7
+ throw new TypeError(`ToggleGroup ${field} must not be empty`);
8
+ }
9
+ }
10
+ export function validateToggleGroupDescriptor(descriptor) {
11
+ assertNonEmpty(descriptor.accessibilityLabel, "accessibilityLabel");
12
+ if (!Array.isArray(descriptor.items) || descriptor.items.length === 0) {
13
+ throw new RangeError("ToggleGroup must contain at least one item");
14
+ }
15
+ const ids = new Set();
16
+ for (const item of descriptor.items) {
17
+ assertNonEmpty(item.id, "item id");
18
+ assertNonEmpty(item.label, "item label");
19
+ if (ids.has(item.id))
20
+ throw new TypeError(`Duplicate ToggleGroup item id: ${item.id}`);
21
+ ids.add(item.id);
22
+ }
23
+ }
24
+ /** 하나를 켜고 끈다. 비활성 보호는 공용 helper가 이미 갖고 있다. */
25
+ export function toggleGroupSelection(descriptor, pressedIds, id) {
26
+ validateToggleGroupDescriptor(descriptor);
27
+ return toggleCheckboxSelection(descriptor.items, pressedIds, id);
28
+ }
29
+ /** 목록에서 사라진 항목의 눌림 상태는 남기지 않는다. */
30
+ export function reconcileToggleGroupSelection(descriptor, pressedIds) {
31
+ validateToggleGroupDescriptor(descriptor);
32
+ return reconcileCheckboxSelection(descriptor.items, pressedIds);
33
+ }
34
+ export const toggleGroupRecipe = {
35
+ slots: ["root", "item", "label"],
36
+ defaults: { size: "medium" },
37
+ sizes: {
38
+ small: { minHeight: control.minTouchTarget, paddingHorizontal: spacing.sm, textVariant: "label" },
39
+ medium: { minHeight: 44, paddingHorizontal: spacing.md, textVariant: "body" },
40
+ },
41
+ radius: radius.md,
42
+ gap: spacing.xxs,
43
+ idle: {
44
+ background: semanticColors.surface.default,
45
+ color: semanticColors.content.body,
46
+ border: semanticColors.border.control,
47
+ },
48
+ pressed: {
49
+ background: collectionItemContract.selectedBackground,
50
+ color: semanticColors.content.brand,
51
+ border: semanticColors.border.focus,
52
+ },
53
+ states: { focus: focusIndicatorContract },
54
+ };
55
+ export const toggleGroupBehavior = {
56
+ controlled: ["pressedIds", "defaultPressedIds", "onPressedIdsChange"],
57
+ inputs: ["items", "accessibilityLabel", "size"],
58
+ stateAxes: {
59
+ availability: ["enabled", "disabled"],
60
+ value: ["pressed", "unpressed"],
61
+ },
62
+ web: {
63
+ roles: ["group", "button"],
64
+ keyboard: ["Tab", "Enter", "Space"],
65
+ /** Toolbar-style roving focus is not used: each toggle is its own tab stop, like a row of checkboxes. */
66
+ focus: "native",
67
+ },
68
+ native: { roles: ["button"], states: ["selected", "disabled"], actions: ["toggle"] },
69
+ scenarios: [
70
+ "several-items-can-be-pressed-at-once-and-none-is-a-valid-state",
71
+ "pressed-state-rides-on-aria-pressed-not-on-color-alone",
72
+ "a-disabled-item-never-toggles-and-never-enters-the-pressed-set",
73
+ "removing-an-item-drops-its-pressed-state-instead-of-keeping-an-orphan-id",
74
+ "single-choice-belongs-to-segmented-control-this-contract-never-adds-a-single-mode",
75
+ "the-group-carries-its-own-accessible-name-separate-from-each-buttons-name",
76
+ ],
77
+ };
78
+ //# sourceMappingURL=toggle-group.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toggle-group.js","sourceRoot":"","sources":["../src/toggle-group.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BehaviorContract } from \"./behaviors.js\";\nimport type { ColorReference } from \"./color-references.js\";\nimport { collectionItemContract, focusIndicatorContract } from \"./component-contracts.js\";\nimport { control, radius, spacing } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\nimport {\n reconcileCheckboxSelection,\n toggleCheckboxSelection,\n type CheckboxSelectionItem,\n} from \"./selection-helpers.js\";\n\n/**\n * 여러 개를 동시에 켜고 끄는 버튼 묶음이다 — 굵게/기울임/밑줄, 필터 칩 여러 개처럼.\n *\n * `SegmentedControl`과 경계가 분명하다. Segmented는 **하나를 고른다**(어떤 화면을 볼지),\n * ToggleGroup은 **여러 개를 켠다**(무엇을 적용할지). 그래서 Segmented에는 \"선택 없음\"이\n * 없고 ToggleGroup에는 있다. 둘을 한 컴포넌트의 `mode` 축으로 합치면 그 차이가 사라지고,\n * \"아무것도 선택되지 않은 Segmented\"라는 표현 불가능한 상태가 타입에 생긴다.\n *\n * `CheckboxGroup`과도 다르다. CheckboxGroup은 목록 안의 동의/선택이라 각 항목이 자기\n * 줄과 설명을 갖는다. ToggleGroup은 도구 모음이라 한 줄에 붙어 있고 라벨이 짧다.\n * 판정(무엇을 켤 수 있고 비활성은 어떻게 되는가)은 같은 helper를 공유한다.\n */\nexport type ToggleGroupItemDescriptor<Id extends string = string> =\n CheckboxSelectionItem<Id>;\n\nexport type ToggleGroupDescriptor<Id extends string = string> = Readonly<{\n /** 묶음 전체의 접근성 이름. 개별 버튼 이름으로 대체할 수 없다. */\n accessibilityLabel: string;\n items: readonly ToggleGroupItemDescriptor<Id>[];\n}>;\n\nexport type ToggleGroupSize = \"small\" | \"medium\";\n\nfunction assertNonEmpty(value: string, field: string): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new TypeError(`ToggleGroup ${field} must not be empty`);\n }\n}\n\nexport function validateToggleGroupDescriptor<Id extends string>(\n descriptor: ToggleGroupDescriptor<Id>,\n): void {\n assertNonEmpty(descriptor.accessibilityLabel, \"accessibilityLabel\");\n if (!Array.isArray(descriptor.items) || descriptor.items.length === 0) {\n throw new RangeError(\"ToggleGroup must contain at least one item\");\n }\n const ids = new Set<Id>();\n for (const item of descriptor.items) {\n assertNonEmpty(item.id, \"item id\");\n assertNonEmpty(item.label, \"item label\");\n if (ids.has(item.id)) throw new TypeError(`Duplicate ToggleGroup item id: ${item.id}`);\n ids.add(item.id);\n }\n}\n\n/** 하나를 켜고 끈다. 비활성 보호는 공용 helper가 이미 갖고 있다. */\nexport function toggleGroupSelection<Id extends string>(\n descriptor: ToggleGroupDescriptor<Id>,\n pressedIds: ReadonlySet<Id>,\n id: Id,\n): ReadonlySet<Id> {\n validateToggleGroupDescriptor(descriptor);\n return toggleCheckboxSelection(descriptor.items, pressedIds, id);\n}\n\n/** 목록에서 사라진 항목의 눌림 상태는 남기지 않는다. */\nexport function reconcileToggleGroupSelection<Id extends string>(\n descriptor: ToggleGroupDescriptor<Id>,\n pressedIds: ReadonlySet<Id>,\n): ReadonlySet<Id> {\n validateToggleGroupDescriptor(descriptor);\n return reconcileCheckboxSelection(descriptor.items, pressedIds);\n}\n\nexport const toggleGroupRecipe = {\n slots: [\"root\", \"item\", \"label\"] as const,\n defaults: { size: \"medium\" as ToggleGroupSize },\n sizes: {\n small: { minHeight: control.minTouchTarget, paddingHorizontal: spacing.sm, textVariant: \"label\" as const },\n medium: { minHeight: 44, paddingHorizontal: spacing.md, textVariant: \"body\" as const },\n },\n radius: radius.md,\n gap: spacing.xxs,\n idle: {\n background: semanticColors.surface.default,\n color: semanticColors.content.body,\n border: semanticColors.border.control,\n },\n pressed: {\n background: collectionItemContract.selectedBackground,\n color: semanticColors.content.brand,\n border: semanticColors.border.focus,\n },\n states: { focus: focusIndicatorContract },\n} as const satisfies {\n slots: readonly [\"root\", \"item\", \"label\"];\n defaults: { size: ToggleGroupSize };\n sizes: Record<ToggleGroupSize, { minHeight: number; paddingHorizontal: number; textVariant: \"label\" | \"body\" }>;\n radius: number;\n gap: number;\n idle: { background: ColorReference; color: ColorReference; border: ColorReference };\n pressed: { background: ColorReference; color: ColorReference; border: ColorReference };\n states: { focus: typeof focusIndicatorContract };\n};\n\nexport const toggleGroupBehavior = {\n controlled: [\"pressedIds\", \"defaultPressedIds\", \"onPressedIdsChange\"],\n inputs: [\"items\", \"accessibilityLabel\", \"size\"],\n stateAxes: {\n availability: [\"enabled\", \"disabled\"],\n value: [\"pressed\", \"unpressed\"],\n },\n web: {\n roles: [\"group\", \"button\"],\n keyboard: [\"Tab\", \"Enter\", \"Space\"],\n /** Toolbar-style roving focus is not used: each toggle is its own tab stop, like a row of checkboxes. */\n focus: \"native\",\n },\n native: { roles: [\"button\"], states: [\"selected\", \"disabled\"], actions: [\"toggle\"] },\n scenarios: [\n \"several-items-can-be-pressed-at-once-and-none-is-a-valid-state\",\n \"pressed-state-rides-on-aria-pressed-not-on-color-alone\",\n \"a-disabled-item-never-toggles-and-never-enters-the-pressed-set\",\n \"removing-an-item-drops-its-pressed-state-instead-of-keeping-an-orphan-id\",\n \"single-choice-belongs-to-segmented-control-this-contract-never-adds-a-single-mode\",\n \"the-group-carries-its-own-accessible-name-separate-from-each-buttons-name\",\n ],\n} as const satisfies BehaviorContract;\n"]}
package/dist/top.d.ts ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * 화면 본문의 **첫 블록**이다. `TopBar`(상단 내비게이션 바)와 다른 자리이고, 그 차이가
3
+ * 이 계약의 존재 이유다.
4
+ *
5
+ * - `TopBar`는 화면에 고정된 크롬이다 — 뒤로가기, 화면 이름, 액션. 스크롤과 무관하게
6
+ * 붙어 있고 safe area를 다룬다.
7
+ * - `Top`은 본문이다 — 스크롤과 함께 올라가고, 사용자가 "이 화면이 무엇을 묻는지"
8
+ * 읽는 문장이 여기 있다. 토스 TDS가 "Top과 ListRow로 화면 대부분을 만든다"고
9
+ * 말하는 그 Top이고, 지금까지 BurnTok `AppScreenHeader`·Taground `screen-shell`·
10
+ * Diairy `DetailPanel`이 각자 다시 만들던 블록이다.
11
+ *
12
+ * `Section`과도 다르다. Section은 본문 **중간**의 묶음 제목이라 heading level이 화면
13
+ * 구조에 종속되지만, Top은 화면당 하나뿐인 첫 제목이라 기본이 `h1`이다. 그래서 두
14
+ * 계약을 합치지 않았다 — 합치면 "이 Section이 화면의 h1인가"를 매번 물어야 한다.
15
+ */
16
+ export type TopSize = "medium" | "large";
17
+ export type TopDescriptor = Readonly<{
18
+ title: string;
19
+ /** 제목 위 한 줄. 카테고리·단계처럼 제목을 한정하는 짧은 말. */
20
+ eyebrow?: string;
21
+ /** 제목 아래 보조 문장. 길면 줄바꿈되며 잘리지 않는다. */
22
+ description?: string;
23
+ size?: TopSize;
24
+ /**
25
+ * 문서 구조상의 제목 단계. 화면당 하나인 Top은 `1`이 기본이지만, 한 라우트가
26
+ * 여러 화면을 담는 경우(시트 안의 화면 등) 제품이 낮출 수 있다.
27
+ */
28
+ headingLevel?: 1 | 2 | 3;
29
+ }>;
30
+ export declare const topDefaults: {
31
+ readonly size: "large";
32
+ readonly headingLevel: 1;
33
+ };
34
+ export declare function validateTopDescriptor(descriptor: TopDescriptor): void;
35
+ /**
36
+ * `large`는 화면의 첫 제목, `medium`은 시트·모달 안의 첫 제목이다. 두 단계뿐인 이유는
37
+ * 세 번째가 필요해지는 자리가 곧 `Section`이기 때문이다 — 크기를 더 늘리는 대신 다른
38
+ * 컴포넌트를 쓰라는 신호로 둔다.
39
+ */
40
+ export declare const topRecipe: {
41
+ readonly slots: readonly ["root", "eyebrow", "title", "description", "trailing"];
42
+ readonly defaults: {
43
+ readonly size: "large";
44
+ };
45
+ readonly sizes: {
46
+ readonly medium: {
47
+ readonly title: {
48
+ readonly fontSize: 20;
49
+ readonly lineHeight: 28;
50
+ readonly fontWeight: "800";
51
+ };
52
+ readonly paddingTop: 16;
53
+ readonly paddingBottom: 12;
54
+ };
55
+ readonly large: {
56
+ readonly title: {
57
+ readonly fontSize: 32;
58
+ readonly lineHeight: 40;
59
+ readonly fontWeight: "800";
60
+ };
61
+ readonly paddingTop: 24;
62
+ readonly paddingBottom: 16;
63
+ };
64
+ };
65
+ readonly eyebrow: {
66
+ readonly color: Readonly<{
67
+ source: "theme";
68
+ key: "contentBrand";
69
+ alpha?: number;
70
+ }>;
71
+ readonly textVariant: "label";
72
+ readonly marginBottom: 4;
73
+ };
74
+ readonly title: {
75
+ readonly color: Readonly<{
76
+ source: "theme";
77
+ key: "text";
78
+ alpha?: number;
79
+ }>;
80
+ };
81
+ readonly description: {
82
+ readonly color: Readonly<{
83
+ source: "theme";
84
+ key: "textBody";
85
+ alpha?: number;
86
+ }>;
87
+ readonly textVariant: "body";
88
+ readonly marginTop: 8;
89
+ };
90
+ /** 제목 줄 오른쪽 보조 행동. 본문이므로 TopBar처럼 아이콘 전용을 강제하지 않는다. */
91
+ readonly trailing: {
92
+ readonly gap: 8;
93
+ };
94
+ readonly gap: 8;
95
+ };
96
+ export declare const topBehavior: {
97
+ /** 상태 축이 없다 — 화면이 제목을 바꾸면 그냥 다른 문자열을 넘긴다. */
98
+ readonly controlled: readonly [];
99
+ readonly inputs: readonly ["title", "eyebrow", "description", "size", "headingLevel"];
100
+ readonly stateAxes: {};
101
+ readonly web: {
102
+ readonly roles: readonly ["heading"];
103
+ readonly keyboard: readonly [];
104
+ readonly focus: "none";
105
+ };
106
+ readonly native: {
107
+ readonly roles: readonly ["header"];
108
+ readonly states: readonly [];
109
+ readonly actions: readonly [];
110
+ };
111
+ readonly scenarios: readonly ["the-title-is-a-real-heading-element-at-the-declared-level-not-styled-text", "top-is-body-content-that-scrolls-away-while-topbar-is-fixed-chrome", "description-wraps-instead-of-truncating-so-large-text-never-hides-the-question", "a-trailing-action-shares-the-title-row-and-drops-below-it-when-space-runs-out", "eyebrow-never-carries-information-the-title-does-not-repeat-in-some-form"];
112
+ };
113
+ //# sourceMappingURL=top.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"top.d.ts","sourceRoot":"","sources":["../src/top.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEzC,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;CAC1B,CAAC,CAAC;AAEH,eAAO,MAAM,WAAW;;;CAGiD,CAAC;AAQ1E,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,aAAa,GAAG,IAAI,CAkBrE;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAkBpB,uDAAuD;;;;;CAexD,CAAC;AAEF,eAAO,MAAM,WAAW;IACtB,6CAA6C;;;;;;;;;;;;;;;CAaV,CAAC"}