@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.
Files changed (353) hide show
  1. package/README.md +208 -0
  2. package/dist/alert-dialog.d.ts +102 -0
  3. package/dist/alert-dialog.d.ts.map +1 -0
  4. package/dist/alert-dialog.js +136 -0
  5. package/dist/alert-dialog.js.map +1 -0
  6. package/dist/base-recipes.d.ts +184 -0
  7. package/dist/base-recipes.d.ts.map +1 -0
  8. package/dist/base-recipes.js +129 -0
  9. package/dist/base-recipes.js.map +1 -0
  10. package/dist/behaviors.d.ts +1254 -0
  11. package/dist/behaviors.d.ts.map +1 -0
  12. package/dist/behaviors.js +972 -0
  13. package/dist/behaviors.js.map +1 -0
  14. package/dist/bottom-navigation-defaults.d.ts +13 -0
  15. package/dist/bottom-navigation-defaults.d.ts.map +1 -0
  16. package/dist/bottom-navigation-defaults.js +10 -0
  17. package/dist/bottom-navigation-defaults.js.map +1 -0
  18. package/dist/bottom-navigation.d.ts +114 -0
  19. package/dist/bottom-navigation.d.ts.map +1 -0
  20. package/dist/bottom-navigation.js +223 -0
  21. package/dist/bottom-navigation.js.map +1 -0
  22. package/dist/breadcrumb.d.ts +97 -0
  23. package/dist/breadcrumb.d.ts.map +1 -0
  24. package/dist/breadcrumb.js +99 -0
  25. package/dist/breadcrumb.js.map +1 -0
  26. package/dist/calendar.d.ts +285 -0
  27. package/dist/calendar.d.ts.map +1 -0
  28. package/dist/calendar.js +297 -0
  29. package/dist/calendar.js.map +1 -0
  30. package/dist/card.d.ts +39 -0
  31. package/dist/card.d.ts.map +1 -0
  32. package/dist/card.js +50 -0
  33. package/dist/card.js.map +1 -0
  34. package/dist/carousel.d.ts +180 -0
  35. package/dist/carousel.d.ts.map +1 -0
  36. package/dist/carousel.js +172 -0
  37. package/dist/carousel.js.map +1 -0
  38. package/dist/catalog.d.ts +7158 -0
  39. package/dist/catalog.d.ts.map +1 -0
  40. package/dist/catalog.js +220 -0
  41. package/dist/catalog.js.map +1 -0
  42. package/dist/collection.d.ts +123 -0
  43. package/dist/collection.d.ts.map +1 -0
  44. package/dist/collection.js +211 -0
  45. package/dist/collection.js.map +1 -0
  46. package/dist/color-references.d.ts +46 -0
  47. package/dist/color-references.d.ts.map +1 -0
  48. package/dist/color-references.js +39 -0
  49. package/dist/color-references.js.map +1 -0
  50. package/dist/colors.d.ts +56 -0
  51. package/dist/colors.d.ts.map +1 -0
  52. package/dist/colors.js +90 -0
  53. package/dist/colors.js.map +1 -0
  54. package/dist/command-palette.d.ts +240 -0
  55. package/dist/command-palette.d.ts.map +1 -0
  56. package/dist/command-palette.js +97 -0
  57. package/dist/command-palette.js.map +1 -0
  58. package/dist/component-contracts.d.ts +137 -0
  59. package/dist/component-contracts.d.ts.map +1 -0
  60. package/dist/component-contracts.js +53 -0
  61. package/dist/component-contracts.js.map +1 -0
  62. package/dist/component-definitions.d.ts +135 -0
  63. package/dist/component-definitions.d.ts.map +1 -0
  64. package/dist/component-definitions.js +142 -0
  65. package/dist/component-definitions.js.map +1 -0
  66. package/dist/component-recipes.d.ts +3583 -0
  67. package/dist/component-recipes.d.ts.map +1 -0
  68. package/dist/component-recipes.js +1503 -0
  69. package/dist/component-recipes.js.map +1 -0
  70. package/dist/component-references.d.ts +416 -0
  71. package/dist/component-references.d.ts.map +1 -0
  72. package/dist/component-references.js +140 -0
  73. package/dist/component-references.js.map +1 -0
  74. package/dist/content-state.d.ts +104 -0
  75. package/dist/content-state.d.ts.map +1 -0
  76. package/dist/content-state.js +116 -0
  77. package/dist/content-state.js.map +1 -0
  78. package/dist/counter-badge-recipe.d.ts +80 -0
  79. package/dist/counter-badge-recipe.d.ts.map +1 -0
  80. package/dist/counter-badge-recipe.js +44 -0
  81. package/dist/counter-badge-recipe.js.map +1 -0
  82. package/dist/counter-badge.d.ts +12 -0
  83. package/dist/counter-badge.d.ts.map +1 -0
  84. package/dist/counter-badge.js +21 -0
  85. package/dist/counter-badge.js.map +1 -0
  86. package/dist/data-table.d.ts +203 -0
  87. package/dist/data-table.d.ts.map +1 -0
  88. package/dist/data-table.js +182 -0
  89. package/dist/data-table.js.map +1 -0
  90. package/dist/date-picker.d.ts +268 -0
  91. package/dist/date-picker.d.ts.map +1 -0
  92. package/dist/date-picker.js +168 -0
  93. package/dist/date-picker.js.map +1 -0
  94. package/dist/description-list.d.ts +68 -0
  95. package/dist/description-list.d.ts.map +1 -0
  96. package/dist/description-list.js +85 -0
  97. package/dist/description-list.js.map +1 -0
  98. package/dist/design-system-provider.d.ts +80 -0
  99. package/dist/design-system-provider.d.ts.map +1 -0
  100. package/dist/design-system-provider.js +147 -0
  101. package/dist/design-system-provider.js.map +1 -0
  102. package/dist/evidence.d.ts +49 -0
  103. package/dist/evidence.d.ts.map +1 -0
  104. package/dist/evidence.js +133 -0
  105. package/dist/evidence.js.map +1 -0
  106. package/dist/file-picker.d.ts +183 -0
  107. package/dist/file-picker.d.ts.map +1 -0
  108. package/dist/file-picker.js +224 -0
  109. package/dist/file-picker.js.map +1 -0
  110. package/dist/floating-action-button.d.ts +143 -0
  111. package/dist/floating-action-button.d.ts.map +1 -0
  112. package/dist/floating-action-button.js +149 -0
  113. package/dist/floating-action-button.js.map +1 -0
  114. package/dist/form.d.ts +143 -0
  115. package/dist/form.d.ts.map +1 -0
  116. package/dist/form.js +206 -0
  117. package/dist/form.js.map +1 -0
  118. package/dist/foundations.d.ts +300 -0
  119. package/dist/foundations.d.ts.map +1 -0
  120. package/dist/foundations.js +238 -0
  121. package/dist/foundations.js.map +1 -0
  122. package/dist/grid.d.ts +75 -0
  123. package/dist/grid.d.ts.map +1 -0
  124. package/dist/grid.js +133 -0
  125. package/dist/grid.js.map +1 -0
  126. package/dist/icon-button-recipe.d.ts +99 -0
  127. package/dist/icon-button-recipe.d.ts.map +1 -0
  128. package/dist/icon-button-recipe.js +53 -0
  129. package/dist/icon-button-recipe.js.map +1 -0
  130. package/dist/icon.d.ts +41 -0
  131. package/dist/icon.d.ts.map +1 -0
  132. package/dist/icon.js +147 -0
  133. package/dist/icon.js.map +1 -0
  134. package/dist/image.d.ts +82 -0
  135. package/dist/image.d.ts.map +1 -0
  136. package/dist/image.js +100 -0
  137. package/dist/image.js.map +1 -0
  138. package/dist/index.d.ts +58 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +66 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/layout.d.ts +118 -0
  143. package/dist/layout.d.ts.map +1 -0
  144. package/dist/layout.js +118 -0
  145. package/dist/layout.js.map +1 -0
  146. package/dist/link.d.ts +57 -0
  147. package/dist/link.d.ts.map +1 -0
  148. package/dist/link.js +142 -0
  149. package/dist/link.js.map +1 -0
  150. package/dist/load-more.d.ts +64 -0
  151. package/dist/load-more.d.ts.map +1 -0
  152. package/dist/load-more.js +124 -0
  153. package/dist/load-more.js.map +1 -0
  154. package/dist/mentions.d.ts +67 -0
  155. package/dist/mentions.d.ts.map +1 -0
  156. package/dist/mentions.js +107 -0
  157. package/dist/mentions.js.map +1 -0
  158. package/dist/number-field.d.ts +208 -0
  159. package/dist/number-field.d.ts.map +1 -0
  160. package/dist/number-field.js +247 -0
  161. package/dist/number-field.js.map +1 -0
  162. package/dist/otp-field.d.ts +152 -0
  163. package/dist/otp-field.d.ts.map +1 -0
  164. package/dist/otp-field.js +117 -0
  165. package/dist/otp-field.js.map +1 -0
  166. package/dist/pagination.d.ts +181 -0
  167. package/dist/pagination.d.ts.map +1 -0
  168. package/dist/pagination.js +226 -0
  169. package/dist/pagination.js.map +1 -0
  170. package/dist/password-field.d.ts +187 -0
  171. package/dist/password-field.d.ts.map +1 -0
  172. package/dist/password-field.js +120 -0
  173. package/dist/password-field.js.map +1 -0
  174. package/dist/popover.d.ts +152 -0
  175. package/dist/popover.d.ts.map +1 -0
  176. package/dist/popover.js +137 -0
  177. package/dist/popover.js.map +1 -0
  178. package/dist/progress-recipe.d.ts +43 -0
  179. package/dist/progress-recipe.d.ts.map +1 -0
  180. package/dist/progress-recipe.js +16 -0
  181. package/dist/progress-recipe.js.map +1 -0
  182. package/dist/recipes.d.ts +31 -0
  183. package/dist/recipes.d.ts.map +1 -0
  184. package/dist/recipes.js +46 -0
  185. package/dist/recipes.js.map +1 -0
  186. package/dist/responsive.d.ts +27 -0
  187. package/dist/responsive.d.ts.map +1 -0
  188. package/dist/responsive.js +66 -0
  189. package/dist/responsive.js.map +1 -0
  190. package/dist/result.d.ts +111 -0
  191. package/dist/result.d.ts.map +1 -0
  192. package/dist/result.js +97 -0
  193. package/dist/result.js.map +1 -0
  194. package/dist/selection-helpers.d.ts +16 -0
  195. package/dist/selection-helpers.d.ts.map +1 -0
  196. package/dist/selection-helpers.js +52 -0
  197. package/dist/selection-helpers.js.map +1 -0
  198. package/dist/semantic-colors.d.ts +275 -0
  199. package/dist/semantic-colors.d.ts.map +1 -0
  200. package/dist/semantic-colors.js +84 -0
  201. package/dist/semantic-colors.js.map +1 -0
  202. package/dist/sheet.d.ts +51 -0
  203. package/dist/sheet.d.ts.map +1 -0
  204. package/dist/sheet.js +69 -0
  205. package/dist/sheet.js.map +1 -0
  206. package/dist/showcase.d.ts +153 -0
  207. package/dist/showcase.d.ts.map +1 -0
  208. package/dist/showcase.js +210 -0
  209. package/dist/showcase.js.map +1 -0
  210. package/dist/side-panel.d.ts +199 -0
  211. package/dist/side-panel.d.ts.map +1 -0
  212. package/dist/side-panel.js +111 -0
  213. package/dist/side-panel.js.map +1 -0
  214. package/dist/slider.d.ts +138 -0
  215. package/dist/slider.d.ts.map +1 -0
  216. package/dist/slider.js +150 -0
  217. package/dist/slider.js.map +1 -0
  218. package/dist/splitter.d.ts +113 -0
  219. package/dist/splitter.d.ts.map +1 -0
  220. package/dist/splitter.js +99 -0
  221. package/dist/splitter.js.map +1 -0
  222. package/dist/statistic.d.ts +41 -0
  223. package/dist/statistic.d.ts.map +1 -0
  224. package/dist/statistic.js +76 -0
  225. package/dist/statistic.js.map +1 -0
  226. package/dist/steps.d.ts +196 -0
  227. package/dist/steps.d.ts.map +1 -0
  228. package/dist/steps.js +160 -0
  229. package/dist/steps.js.map +1 -0
  230. package/dist/tag.d.ts +128 -0
  231. package/dist/tag.d.ts.map +1 -0
  232. package/dist/tag.js +93 -0
  233. package/dist/tag.js.map +1 -0
  234. package/dist/timeline.d.ts +147 -0
  235. package/dist/timeline.d.ts.map +1 -0
  236. package/dist/timeline.js +127 -0
  237. package/dist/timeline.js.map +1 -0
  238. package/dist/toast.d.ts +164 -0
  239. package/dist/toast.d.ts.map +1 -0
  240. package/dist/toast.js +529 -0
  241. package/dist/toast.js.map +1 -0
  242. package/dist/tokens.d.ts +9 -0
  243. package/dist/tokens.d.ts.map +1 -0
  244. package/dist/tokens.js +9 -0
  245. package/dist/tokens.js.map +1 -0
  246. package/dist/tooltip.d.ts +44 -0
  247. package/dist/tooltip.d.ts.map +1 -0
  248. package/dist/tooltip.js +88 -0
  249. package/dist/tooltip.js.map +1 -0
  250. package/dist/tour.d.ts +218 -0
  251. package/dist/tour.d.ts.map +1 -0
  252. package/dist/tour.js +211 -0
  253. package/dist/tour.js.map +1 -0
  254. package/dist/transfer-list.d.ts +207 -0
  255. package/dist/transfer-list.d.ts.map +1 -0
  256. package/dist/transfer-list.js +193 -0
  257. package/dist/transfer-list.js.map +1 -0
  258. package/dist/tree-select.d.ts +78 -0
  259. package/dist/tree-select.d.ts.map +1 -0
  260. package/dist/tree-select.js +132 -0
  261. package/dist/tree-select.js.map +1 -0
  262. package/dist/tree.d.ts +206 -0
  263. package/dist/tree.d.ts.map +1 -0
  264. package/dist/tree.js +223 -0
  265. package/dist/tree.js.map +1 -0
  266. package/dist/upload-item.d.ts +173 -0
  267. package/dist/upload-item.d.ts.map +1 -0
  268. package/dist/upload-item.js +156 -0
  269. package/dist/upload-item.js.map +1 -0
  270. package/dist/version.d.ts +3 -0
  271. package/dist/version.d.ts.map +1 -0
  272. package/dist/version.js +3 -0
  273. package/dist/version.js.map +1 -0
  274. package/docs/affix.md +63 -0
  275. package/docs/anchor.md +59 -0
  276. package/docs/ant-design-coverage.md +119 -0
  277. package/docs/app-provider.md +50 -0
  278. package/docs/app-rn-adoption.md +227 -0
  279. package/docs/architecture.md +320 -0
  280. package/docs/authoring-brief.md +87 -0
  281. package/docs/border-beam.md +66 -0
  282. package/docs/bottom-navigation.md +127 -0
  283. package/docs/breadcrumb.md +82 -0
  284. package/docs/calendar.md +154 -0
  285. package/docs/carousel.md +130 -0
  286. package/docs/cascader.md +93 -0
  287. package/docs/catalog-decision-status.md +306 -0
  288. package/docs/color-picker.md +72 -0
  289. package/docs/command-palette.md +116 -0
  290. package/docs/confirm-popover.md +93 -0
  291. package/docs/consistency-audit.md +383 -0
  292. package/docs/consumer-release-gate.md +90 -0
  293. package/docs/content-state.md +156 -0
  294. package/docs/context-panel.md +88 -0
  295. package/docs/cross-platform-core-normalization.md +118 -0
  296. package/docs/data-table.md +79 -0
  297. package/docs/date-picker.md +84 -0
  298. package/docs/description-list.md +75 -0
  299. package/docs/design-system-provider.md +139 -0
  300. package/docs/dropdown.md +78 -0
  301. package/docs/expansion-roadmap.md +285 -0
  302. package/docs/file-picker.md +59 -0
  303. package/docs/floating-action-button.md +111 -0
  304. package/docs/form.md +134 -0
  305. package/docs/generated/component-maturity.md +103 -0
  306. package/docs/generated/renderer-evidence.json +5597 -0
  307. package/docs/generated/renderer-evidence.md +132 -0
  308. package/docs/generated/showcase-manifest.json +3732 -0
  309. package/docs/icon.md +22 -0
  310. package/docs/identity.md +121 -0
  311. package/docs/image.md +84 -0
  312. package/docs/implementation-0.5.md +85 -0
  313. package/docs/layout-primitives.md +107 -0
  314. package/docs/layout.md +83 -0
  315. package/docs/library-reference-decisions.md +123 -0
  316. package/docs/link.md +67 -0
  317. package/docs/load-more.md +31 -0
  318. package/docs/mentions.md +82 -0
  319. package/docs/migration-0.2.md +108 -0
  320. package/docs/migration-0.3.md +72 -0
  321. package/docs/migration-0.5.md +82 -0
  322. package/docs/migration-0.6.md +197 -0
  323. package/docs/notification.md +54 -0
  324. package/docs/number-field.md +82 -0
  325. package/docs/otp-field.md +103 -0
  326. package/docs/pagination.md +139 -0
  327. package/docs/password-field.md +98 -0
  328. package/docs/popover.md +124 -0
  329. package/docs/promotion-candidates.md +169 -0
  330. package/docs/qr-code.md +69 -0
  331. package/docs/rating.md +58 -0
  332. package/docs/responsive-grid.md +90 -0
  333. package/docs/result.md +96 -0
  334. package/docs/showcase.md +97 -0
  335. package/docs/side-panel.md +71 -0
  336. package/docs/slider.md +79 -0
  337. package/docs/splitter.md +55 -0
  338. package/docs/statistic.md +16 -0
  339. package/docs/steps.md +115 -0
  340. package/docs/tag.md +57 -0
  341. package/docs/time-picker.md +88 -0
  342. package/docs/timeline.md +146 -0
  343. package/docs/toast.md +127 -0
  344. package/docs/tooltip.md +58 -0
  345. package/docs/tour.md +136 -0
  346. package/docs/transfer-list.md +94 -0
  347. package/docs/tree-select.md +101 -0
  348. package/docs/tree.md +123 -0
  349. package/docs/upload-item.md +68 -0
  350. package/docs/utility.md +53 -0
  351. package/docs/virtual-list.md +70 -0
  352. package/docs/watermark.md +58 -0
  353. package/package.json +402 -0
@@ -0,0 +1,1503 @@
1
+ import { collectionItemContract, fieldFrameContract, floatingSurfaceContract, focusIndicatorContract, formSupportContract, } from "./component-contracts.js";
2
+ import { backdrop, breakpoint, control, glyph, layer, layout, fontWeight, motion, motionPreset, opacity, radius, spacing, stroke, } from "./foundations.js";
3
+ import { semanticColors } from "./semantic-colors.js";
4
+ import { tooltipDescriptorDefaults } from "./tooltip.js";
5
+ import { bottomNavigationRecipeDefaults } from "./bottom-navigation-defaults.js";
6
+ export { iconButtonRecipe, resolveIconButtonPresentation, } from "./icon-button-recipe.js";
7
+ export { counterBadgeDefaults, counterBadgeRecipe, formatCounterBadgeCount, } from "./counter-badge-recipe.js";
8
+ export { progressRecipe, } from "./progress-recipe.js";
9
+ export const textRecipe = {
10
+ slots: ["root"],
11
+ defaults: { variant: "body", tone: "primary", emphasis: "regular" },
12
+ tones: {
13
+ primary: semanticColors.content.primary,
14
+ body: semanticColors.content.body,
15
+ muted: semanticColors.content.secondary,
16
+ subtle: semanticColors.content.tertiary,
17
+ weak: semanticColors.content.decorative,
18
+ brand: semanticColors.content.brand,
19
+ danger: semanticColors.content.danger,
20
+ inverse: semanticColors.content.inverse,
21
+ },
22
+ emphasis: {
23
+ regular: fontWeight.regular,
24
+ medium: fontWeight.semibold,
25
+ strong: fontWeight.bold,
26
+ },
27
+ };
28
+ export const iconRecipe = {
29
+ slots: ["root"],
30
+ defaults: {
31
+ size: "md",
32
+ tone: "secondary",
33
+ weight: "regular",
34
+ decorative: true,
35
+ },
36
+ sizes: glyph,
37
+ tones: {
38
+ primary: semanticColors.content.primary,
39
+ secondary: semanticColors.content.secondary,
40
+ decorative: semanticColors.content.decorative,
41
+ brand: semanticColors.content.brand,
42
+ info: semanticColors.feedback.info.foreground,
43
+ success: semanticColors.feedback.success.foreground,
44
+ warning: semanticColors.feedback.warning.foreground,
45
+ danger: semanticColors.content.danger,
46
+ inverse: semanticColors.content.inverse,
47
+ },
48
+ weights: { regular: 2, strong: 2.5 },
49
+ stroke: {
50
+ lineCap: "round",
51
+ lineJoin: "round",
52
+ scaling: "proportional",
53
+ },
54
+ };
55
+ export const stackRecipe = {
56
+ slots: ["root"],
57
+ defaults: { axis: "block", gap: "md", align: "stretch", justify: "start", wrap: false },
58
+ axes: { block: "column", inline: "row" },
59
+ gaps: spacing,
60
+ align: ["start", "center", "end", "stretch"],
61
+ justify: ["start", "center", "end", "between"],
62
+ };
63
+ export const linkRecipe = {
64
+ slots: ["root", "leading", "label", "trailing"],
65
+ defaults: { tone: "brand", variant: "inline" },
66
+ tones: {
67
+ brand: semanticColors.content.brand,
68
+ neutral: semanticColors.content.body,
69
+ },
70
+ variants: {
71
+ inline: {
72
+ textVariant: "body",
73
+ fontWeight: fontWeight.semibold,
74
+ underline: "always",
75
+ minHeight: null,
76
+ },
77
+ standalone: {
78
+ textVariant: "body",
79
+ fontWeight: fontWeight.bold,
80
+ underline: "hover",
81
+ minHeight: control.minTouchTarget,
82
+ },
83
+ },
84
+ gap: spacing.xxs,
85
+ icon: { glyph: "xs", inheritsTone: true },
86
+ states: {
87
+ pressedOpacity: opacity.pressed,
88
+ focus: focusIndicatorContract,
89
+ },
90
+ };
91
+ export const selectionGroupRecipe = {
92
+ slots: ["root", "label", "requiredIndicator", "description", "items", "error"],
93
+ defaults: { orientation: "vertical", presentation: "card" },
94
+ orientations: {
95
+ vertical: {
96
+ direction: "column",
97
+ // `grouped` rows sit flush inside one shared card, so the group owns
98
+ // the only outer radius and the rows share edges instead of floating
99
+ // apart with their own gap — the same shape as `selectionControlRecipe`'s
100
+ // `grouped` presentation, which this gap pairs with.
101
+ gap: { plain: spacing.xxs, card: spacing.xs, grouped: 0 },
102
+ },
103
+ horizontal: {
104
+ direction: "row",
105
+ gap: { plain: spacing.sm, card: spacing.md, grouped: 0 },
106
+ },
107
+ },
108
+ label: formSupportContract.label,
109
+ requiredIndicator: { color: semanticColors.content.danger },
110
+ description: formSupportContract.hint,
111
+ error: formSupportContract.error,
112
+ supportGap: formSupportContract.gap,
113
+ states: { disabledOpacity: opacity.disabled },
114
+ };
115
+ export const badgeRecipe = {
116
+ slots: ["root", "icon", "label"],
117
+ defaults: { tone: "neutral", size: "medium", variant: "filled" },
118
+ variants: {
119
+ filled: {
120
+ usesToneBackground: true,
121
+ borderFallback: null,
122
+ },
123
+ outline: {
124
+ usesToneBackground: false,
125
+ borderFallback: semanticColors.border.default,
126
+ },
127
+ },
128
+ tones: {
129
+ neutral: {
130
+ content: semanticColors.content.secondary,
131
+ outlineContent: semanticColors.content.secondary,
132
+ background: semanticColors.surface.sunken,
133
+ border: null,
134
+ },
135
+ strong: {
136
+ content: semanticColors.content.inverse,
137
+ outlineContent: semanticColors.content.primary,
138
+ background: semanticColors.content.primary,
139
+ border: null,
140
+ },
141
+ brand: {
142
+ content: semanticColors.content.brand,
143
+ outlineContent: semanticColors.content.brand,
144
+ // The brand tint now means "selected" everywhere (see
145
+ // `selectionControlRecipe`/`segmentedControlRecipe`), so a badge that
146
+ // only labels something — a season, a grade, a question number —
147
+ // cannot wear it: rows of static badges started reading as a filter
148
+ // with one option switched on. The plate goes neutral and the brand
149
+ // stays in the copy.
150
+ background: semanticColors.surface.sunken,
151
+ border: null,
152
+ },
153
+ info: {
154
+ content: semanticColors.feedback.info.foreground,
155
+ outlineContent: semanticColors.feedback.info.foreground,
156
+ background: semanticColors.feedback.info.badgeBackground,
157
+ border: semanticColors.feedback.info.border,
158
+ },
159
+ success: {
160
+ content: semanticColors.feedback.success.foreground,
161
+ outlineContent: semanticColors.feedback.success.foreground,
162
+ background: semanticColors.feedback.success.badgeBackground,
163
+ border: semanticColors.feedback.success.border,
164
+ },
165
+ warning: {
166
+ content: semanticColors.feedback.warning.foreground,
167
+ outlineContent: semanticColors.feedback.warning.foreground,
168
+ background: semanticColors.feedback.warning.badgeBackground,
169
+ border: semanticColors.feedback.warning.border,
170
+ },
171
+ attention: {
172
+ content: semanticColors.feedback.attention.foreground,
173
+ outlineContent: semanticColors.feedback.attention.foreground,
174
+ background: semanticColors.feedback.attention.badgeBackground,
175
+ border: semanticColors.feedback.attention.border,
176
+ },
177
+ danger: {
178
+ content: semanticColors.feedback.danger.foreground,
179
+ outlineContent: semanticColors.feedback.danger.foreground,
180
+ background: semanticColors.feedback.danger.badgeBackground,
181
+ border: semanticColors.feedback.danger.border,
182
+ },
183
+ },
184
+ sizes: {
185
+ small: {
186
+ minHeight: 20,
187
+ paddingHorizontal: spacing.xxs,
188
+ gap: spacing.xxs,
189
+ textVariant: "caption",
190
+ },
191
+ medium: {
192
+ minHeight: 24,
193
+ paddingHorizontal: spacing.xs,
194
+ gap: spacing.xxs,
195
+ textVariant: "caption",
196
+ },
197
+ },
198
+ radius: "full",
199
+ borderWidth: stroke.default,
200
+ fontWeight: fontWeight.bold,
201
+ };
202
+ /** Search is a field specialization with stable affordance and clear targets. */
203
+ export const searchFieldRecipe = {
204
+ slots: ["root", "leading", "input", "trailing", "clear", "spinner"],
205
+ defaults: { size: "medium", shape: "medium" },
206
+ sizes: {
207
+ medium: {
208
+ minHeight: control.minTouchTarget,
209
+ paddingHorizontal: spacing.sm,
210
+ gap: spacing.xs,
211
+ glyph: "sm",
212
+ clearDiameter: 36,
213
+ clearHitSlop: 4,
214
+ textVariant: "body",
215
+ },
216
+ large: {
217
+ minHeight: control.buttonHeight.large,
218
+ paddingHorizontal: spacing.md,
219
+ gap: spacing.sm,
220
+ glyph: "sm",
221
+ clearDiameter: control.minTouchTarget,
222
+ clearHitSlop: 0,
223
+ textVariant: "bodyLarge",
224
+ },
225
+ },
226
+ colors: {
227
+ background: semanticColors.surface.default,
228
+ content: semanticColors.content.primary,
229
+ placeholder: semanticColors.content.secondary,
230
+ leading: semanticColors.content.decorative,
231
+ clear: semanticColors.content.secondary,
232
+ border: semanticColors.content.secondary,
233
+ focus: focusIndicatorContract.color,
234
+ invalid: semanticColors.border.danger,
235
+ },
236
+ /**
237
+ * 모양은 **축이다** — 값 하나로 못 박지 않는다.
238
+ *
239
+ * 원래 여기는 `radius: "full"` 하나였다. 그런데 `fieldRecipe`는 같은 결정을
240
+ * `shapes: { medium, large, full }` 축으로 열어 두고 있었고, 그래서 **한 앱 안에서
241
+ * 평범한 입력과 찾기 입력이 서로 다른 모양이 되는 것을 계약이 막지 못했다.** 실제로
242
+ * 갈라졌다 — app-rn은 `"md"`로 그리고 있었는데 이 계약은 `"full"`이라고 말하고
243
+ * 있었고, 어느 쪽이 옳은지 양쪽 문서 어디에도 없었다.
244
+ *
245
+ * 찾기 입력이 알약이어야 하는지는 **제품이 정할 일**이고(iOS 관례는 알약, 이 앱은
246
+ * 사각), 계약이 할 일은 그 선택지를 `fieldRecipe`와 **같은 이름으로** 주는 것이다.
247
+ * 그러면 두 입력의 모양을 나란히 맞추는 것과 일부러 다르게 하는 것이 둘 다 표현된다.
248
+ */
249
+ shapes: { medium: "md", large: "lg", full: "full" },
250
+ borderWidth: stroke.default,
251
+ focusRingWidth: focusIndicatorContract.width,
252
+ focusRingOffset: focusIndicatorContract.offset,
253
+ states: {
254
+ disabledOpacity: opacity.disabled,
255
+ pressedOpacity: opacity.pressed,
256
+ },
257
+ };
258
+ export const chipRecipe = {
259
+ slots: ["root", "leading", "indicator", "label", "trailing"],
260
+ defaults: { size: "small", selected: false },
261
+ sizes: {
262
+ small: {
263
+ height: control.chipHeight.small,
264
+ hitSlop: 4,
265
+ paddingHorizontal: spacing.sm,
266
+ gap: spacing.xxs,
267
+ textVariant: "body",
268
+ },
269
+ medium: {
270
+ height: control.chipHeight.medium,
271
+ hitSlop: 0,
272
+ paddingHorizontal: spacing.md,
273
+ gap: spacing.xs,
274
+ textVariant: "body",
275
+ },
276
+ },
277
+ states: {
278
+ idle: {
279
+ background: semanticColors.surface.default,
280
+ content: semanticColors.content.secondary,
281
+ // A resting chip wears the shared hairline, not a text-strength
282
+ // outline. Drawing it in `content.secondary` made an unselected chip
283
+ // read heavier than the selected one beside it, inverting the one
284
+ // signal that matters in a filter row.
285
+ border: semanticColors.border.default,
286
+ },
287
+ selected: {
288
+ background: semanticColors.surface.brand,
289
+ content: semanticColors.content.brand,
290
+ border: semanticColors.border.focus,
291
+ },
292
+ pressedOpacity: opacity.pressed,
293
+ disabledOpacity: opacity.disabled,
294
+ },
295
+ radius: "full",
296
+ borderWidth: stroke.default,
297
+ label: { fontWeight: fontWeight.semibold, selectedFontWeight: fontWeight.bold },
298
+ selectionIndicator: {
299
+ color: semanticColors.content.brand,
300
+ glyph: "xs",
301
+ },
302
+ focus: focusIndicatorContract,
303
+ };
304
+ export const dividerRecipe = {
305
+ slots: ["root", "label"],
306
+ defaults: { orientation: "horizontal", inset: "none" },
307
+ color: semanticColors.border.default,
308
+ thickness: stroke.subtle,
309
+ insets: {
310
+ none: 0,
311
+ start: spacing.md,
312
+ both: spacing.md,
313
+ },
314
+ };
315
+ export const listRecipe = {
316
+ slots: ["root", "item", "separator"],
317
+ defaults: { separator: "indented" },
318
+ separators: {
319
+ none: null,
320
+ full: { insetStart: 0, insetEnd: 0 },
321
+ indented: { insetStart: 40 + spacing.sm, insetEnd: 0 },
322
+ },
323
+ background: null,
324
+ };
325
+ export const listRowRecipe = {
326
+ slots: ["root", "leading", "content", "title", "description", "trailing"],
327
+ defaults: { density: "comfortable", selected: false },
328
+ density: {
329
+ compact: {
330
+ oneLineMinHeight: control.minTouchTarget,
331
+ twoLineMinHeight: 60,
332
+ paddingHorizontal: spacing.xs,
333
+ paddingVertical: spacing.xxs,
334
+ },
335
+ comfortable: {
336
+ oneLineMinHeight: layout.rowHeight.singleLine,
337
+ twoLineMinHeight: layout.rowHeight.twoLine,
338
+ paddingHorizontal: spacing.xs,
339
+ paddingVertical: spacing.xs,
340
+ },
341
+ },
342
+ gap: spacing.sm,
343
+ leadingSize: 40,
344
+ title: { color: semanticColors.content.body, textVariant: "bodyLarge", fontWeight: fontWeight.bold },
345
+ description: { color: semanticColors.content.secondary, textVariant: "body" },
346
+ trailing: {
347
+ textColor: semanticColors.content.secondary,
348
+ iconColor: semanticColors.content.decorative,
349
+ glyph: "sm",
350
+ textVariant: "caption",
351
+ },
352
+ states: {
353
+ pressedBackground: semanticColors.interaction.pressed,
354
+ selectedBackground: semanticColors.surface.brand,
355
+ disabledOpacity: opacity.disabled,
356
+ },
357
+ };
358
+ export const accordionRecipe = {
359
+ slots: ["root", "item", "header", "trigger", "title", "indicator", "panel", "divider"],
360
+ defaults: { density: "comfortable", allowsMultipleExpanded: false },
361
+ density: {
362
+ compact: { triggerMinHeight: control.minTouchTarget, paddingVertical: spacing.xs },
363
+ comfortable: { triggerMinHeight: layout.rowHeight.singleLine, paddingVertical: spacing.sm },
364
+ },
365
+ paddingHorizontal: spacing.xs,
366
+ gap: spacing.sm,
367
+ title: { color: semanticColors.content.body, textVariant: "bodyLarge", fontWeight: fontWeight.bold },
368
+ indicator: { color: semanticColors.content.secondary, glyph: "sm" },
369
+ panel: {
370
+ color: semanticColors.content.body,
371
+ textVariant: "body",
372
+ paddingBottom: spacing.md,
373
+ paddingInlineStart: spacing.xs,
374
+ },
375
+ divider: semanticColors.border.default,
376
+ states: {
377
+ pressedBackground: semanticColors.interaction.pressed,
378
+ focus: focusIndicatorContract,
379
+ disabledOpacity: opacity.disabled,
380
+ },
381
+ transition: motionPreset.enter,
382
+ };
383
+ export const menuRecipe = {
384
+ slots: [
385
+ "trigger",
386
+ "content",
387
+ "viewport",
388
+ "section",
389
+ "sectionLabel",
390
+ "item",
391
+ "leading",
392
+ "copy",
393
+ "label",
394
+ "description",
395
+ "trailing",
396
+ "shortcut",
397
+ "indicator",
398
+ "dangerIndicator",
399
+ "separator",
400
+ ],
401
+ defaults: { density: "comfortable", itemTone: "neutral" },
402
+ surface: floatingSurfaceContract,
403
+ density: {
404
+ compact: { ...collectionItemContract, minHeight: control.minTouchTarget },
405
+ comfortable: { ...collectionItemContract, minHeight: layout.rowHeight.singleLine },
406
+ },
407
+ tones: {
408
+ neutral: collectionItemContract.label.color,
409
+ danger: collectionItemContract.danger,
410
+ },
411
+ sectionLabel: {
412
+ color: semanticColors.content.secondary,
413
+ textVariant: "label",
414
+ paddingHorizontal: spacing.sm,
415
+ paddingVertical: spacing.xs,
416
+ },
417
+ leading: { color: semanticColors.content.secondary, glyph: "sm" },
418
+ shortcut: { color: semanticColors.content.secondary, textVariant: "caption" },
419
+ indicator: { color: collectionItemContract.selectedIndicator, glyph: "sm" },
420
+ separator: semanticColors.border.default,
421
+ dangerIndicator: {
422
+ color: collectionItemContract.danger,
423
+ glyph: "sm",
424
+ mark: "alert",
425
+ },
426
+ minWidth: 220,
427
+ maxWidth: 320,
428
+ maxHeight: 360,
429
+ sideOffset: spacing.xs,
430
+ collisionPadding: spacing.xs,
431
+ states: { disabledOpacity: opacity.disabled },
432
+ transition: { enter: motionPreset.enter, exit: motionPreset.exit },
433
+ };
434
+ /**
435
+ * Select is visually one field plus one collection, even though Web renders
436
+ * the collection in a popover and Native renders it in a modal sheet.
437
+ */
438
+ export const selectRecipe = {
439
+ slots: [
440
+ "root",
441
+ "label",
442
+ "trigger",
443
+ "leading",
444
+ "value",
445
+ "placeholder",
446
+ "busyIndicator",
447
+ "indicator",
448
+ "description",
449
+ "error",
450
+ "popover",
451
+ "viewport",
452
+ "section",
453
+ "sectionLabel",
454
+ "option",
455
+ "optionLeading",
456
+ "optionCopy",
457
+ "optionLabel",
458
+ "optionDescription",
459
+ "selectionIndicator",
460
+ "stateMessage",
461
+ ],
462
+ defaults: { size: "medium", density: "comfortable" },
463
+ adaptive: { web: "popover", native: "sheet" },
464
+ frame: fieldFrameContract,
465
+ sizes: {
466
+ medium: {
467
+ minHeight: fieldFrameContract.minHeight,
468
+ paddingHorizontal: fieldFrameContract.paddingHorizontal,
469
+ textVariant: "body",
470
+ glyph: "sm",
471
+ },
472
+ large: {
473
+ minHeight: control.buttonHeight.large,
474
+ paddingHorizontal: spacing.lg,
475
+ textVariant: "bodyLarge",
476
+ glyph: "md",
477
+ },
478
+ },
479
+ support: formSupportContract,
480
+ value: {
481
+ color: semanticColors.content.body,
482
+ placeholderColor: semanticColors.content.secondary,
483
+ gap: spacing.sm,
484
+ },
485
+ leading: { color: semanticColors.content.secondary },
486
+ indicator: { color: semanticColors.content.secondary },
487
+ busyIndicator: { color: semanticColors.content.brand, glyph: "sm" },
488
+ popover: {
489
+ ...floatingSurfaceContract,
490
+ minWidth: 220,
491
+ maxWidth: 420,
492
+ maxHeight: 360,
493
+ sideOffset: spacing.xs,
494
+ collisionPadding: spacing.xs,
495
+ },
496
+ density: {
497
+ compact: { ...collectionItemContract, minHeight: control.minTouchTarget },
498
+ comfortable: { ...collectionItemContract, minHeight: layout.rowHeight.singleLine },
499
+ },
500
+ sectionLabel: {
501
+ color: semanticColors.content.secondary,
502
+ textVariant: "label",
503
+ paddingHorizontal: spacing.sm,
504
+ paddingVertical: spacing.xs,
505
+ },
506
+ optionLeading: { color: semanticColors.content.secondary, glyph: "sm" },
507
+ optionLabel: { fontWeight: fontWeight.semibold, selectedFontWeight: fontWeight.bold },
508
+ selectionIndicator: { color: semanticColors.border.focus, glyph: "sm" },
509
+ stateMessage: {
510
+ color: semanticColors.content.secondary,
511
+ textVariant: "body",
512
+ minHeight: control.minTouchTarget,
513
+ paddingHorizontal: spacing.sm,
514
+ paddingVertical: spacing.sm,
515
+ },
516
+ states: {
517
+ hoverBackground: semanticColors.interaction.hover,
518
+ pressedBackground: semanticColors.interaction.pressed,
519
+ focus: focusIndicatorContract,
520
+ invalidBorder: semanticColors.border.danger,
521
+ disabledOpacity: opacity.disabled,
522
+ },
523
+ transition: {
524
+ web: { enter: motionPreset.micro, exit: motionPreset.exit },
525
+ native: { enter: motionPreset.enter, exit: motionPreset.exit },
526
+ },
527
+ };
528
+ /** Editable input anatomy layered on the exact same collection grammar. */
529
+ export const comboboxRecipe = {
530
+ slots: [
531
+ "root",
532
+ "label",
533
+ "field",
534
+ "leading",
535
+ "input",
536
+ "indicator",
537
+ "clear",
538
+ "loadingIndicator",
539
+ "description",
540
+ "error",
541
+ "popover",
542
+ "viewport",
543
+ "section",
544
+ "sectionLabel",
545
+ "option",
546
+ "optionLeading",
547
+ "optionCopy",
548
+ "optionLabel",
549
+ "optionDescription",
550
+ "selectionIndicator",
551
+ "stateMessage",
552
+ ],
553
+ defaults: selectRecipe.defaults,
554
+ adaptive: selectRecipe.adaptive,
555
+ frame: selectRecipe.frame,
556
+ sizes: selectRecipe.sizes,
557
+ support: selectRecipe.support,
558
+ input: selectRecipe.value,
559
+ leading: selectRecipe.leading,
560
+ indicator: selectRecipe.indicator,
561
+ clear: {
562
+ diameter: control.buttonHeight.small,
563
+ hitSlop: control.buttonHitSlop.small,
564
+ glyph: "xs",
565
+ color: semanticColors.content.secondary,
566
+ },
567
+ loadingIndicator: { color: semanticColors.content.brand, glyph: "sm" },
568
+ popover: selectRecipe.popover,
569
+ density: selectRecipe.density,
570
+ sectionLabel: selectRecipe.sectionLabel,
571
+ optionLeading: selectRecipe.optionLeading,
572
+ optionLabel: selectRecipe.optionLabel,
573
+ selectionIndicator: selectRecipe.selectionIndicator,
574
+ stateMessage: selectRecipe.stateMessage,
575
+ states: selectRecipe.states,
576
+ transition: selectRecipe.transition,
577
+ };
578
+ export const segmentedControlRecipe = {
579
+ slots: ["root", "item", "label", "indicator"],
580
+ defaults: { size: "medium" },
581
+ adaptive: {
582
+ // Equal-width rows stop being comparable when every short label wraps one
583
+ // Hangul syllable per line. Native renderers stack the options before that
584
+ // point so each choice remains a readable phrase and a 44pt target.
585
+ largeTextLayout: "stacked",
586
+ stackAtFontScale: 1.6,
587
+ },
588
+ /**
589
+ * A recessed track with a raised white thumb. The fills used to be
590
+ * inverted — a white track with a canvas-coloured selected segment — so the
591
+ * chosen option read as the recessed, disabled-looking one. `surface.sunken`
592
+ * is too close to white to carry the step, so the track takes the canvas
593
+ * tone and a border keeps the control legible when it sits on that canvas.
594
+ */
595
+ container: {
596
+ background: semanticColors.canvas,
597
+ border: semanticColors.border.default,
598
+ borderWidth: stroke.subtle,
599
+ radius: "lg",
600
+ padding: spacing.xxs,
601
+ gap: spacing.xxs,
602
+ },
603
+ sizes: {
604
+ small: {
605
+ minHeight: 36,
606
+ hitSlop: 4,
607
+ paddingHorizontal: spacing.sm,
608
+ textVariant: "label",
609
+ },
610
+ medium: {
611
+ minHeight: control.minTouchTarget,
612
+ hitSlop: 0,
613
+ paddingHorizontal: spacing.md,
614
+ textVariant: "body",
615
+ },
616
+ },
617
+ item: {
618
+ radius: "md",
619
+ gap: spacing.xxs,
620
+ idleContent: semanticColors.content.secondary,
621
+ fontWeight: fontWeight.semibold,
622
+ // "Selected" is the brand tint, not a plain raised plate — a raised
623
+ // white segment beside a selected brand-tinted chip spoke two different
624
+ // visual languages for the same state. `surface.brand` is also the one
625
+ // *opaque* brand role: a product author who tried a translucent wash of
626
+ // `primary` here instead (same idea, see
627
+ // `selectionControlRecipe.states.selectedBackground` below) found it
628
+ // changed value with whatever sat behind it — #E8EFFB on a white card,
629
+ // #DCE5F3 on the canvas — so the same "selected" read as three different
630
+ // colors. Using the opaque tint keeps it one color everywhere.
631
+ selectedBackground: semanticColors.surface.brand,
632
+ selectedContent: semanticColors.content.brand,
633
+ /**
634
+ * The ring stays the selection signal so the control keeps a non-text
635
+ * contrast of at least 3:1; a fill-only thumb cannot reach that against a
636
+ * neutral track.
637
+ */
638
+ selectedBorder: semanticColors.border.focus,
639
+ selectedBorderWidth: stroke.strong,
640
+ selectedFontWeight: fontWeight.bold,
641
+ focusRing: semanticColors.border.focus,
642
+ pressedOpacity: opacity.pressed,
643
+ disabledOpacity: opacity.disabled,
644
+ },
645
+ };
646
+ export const switchRecipe = {
647
+ slots: ["root", "track", "thumb", "label", "description"],
648
+ defaults: { size: "medium" },
649
+ sizes: {
650
+ small: { width: 44, height: 26, thumb: 22, inset: 2 },
651
+ medium: { width: 52, height: 32, thumb: 28, inset: 2 },
652
+ },
653
+ colors: {
654
+ trackOff: semanticColors.surface.sunken,
655
+ trackOffBorder: semanticColors.content.secondary,
656
+ trackOn: semanticColors.content.brand,
657
+ trackOnBorder: semanticColors.border.focus,
658
+ thumbOff: semanticColors.canvas,
659
+ thumbOffBorder: semanticColors.content.secondary,
660
+ thumbOn: semanticColors.canvas,
661
+ // A disabled switch still has to report what it is set to, so disabled
662
+ // state changes hue instead of fading uniformly with `states.disabledOpacity`
663
+ // — a product author found that a flat opacity made "on" and "off" nearly
664
+ // identical, turning a saved setting unreadable while its backing request
665
+ // was in flight. The on track keeps a recognisable (if muted) brand wash,
666
+ // the off track and both thumbs drop to neutral tones, and every part
667
+ // keeps a hairline border so the shape stays legible at reduced contrast.
668
+ trackOffDisabled: semanticColors.border.default,
669
+ trackOffBorderDisabled: semanticColors.border.default,
670
+ trackOnDisabled: { ...semanticColors.content.brand, alpha: 0.38 },
671
+ trackOnBorderDisabled: { ...semanticColors.content.brand, alpha: 0.38 },
672
+ thumbDisabled: semanticColors.canvas,
673
+ thumbDisabledBorder: semanticColors.content.decorative,
674
+ },
675
+ states: {
676
+ disabledOpacity: opacity.disabled,
677
+ pressedOpacity: opacity.pressed,
678
+ },
679
+ rowMinHeight: control.minTouchTarget,
680
+ // A switch with a description is a two-line row like any other list row,
681
+ // and a product author found that letting content decide the height gave
682
+ // neighbouring rows different heights — this pins it to the same value as
683
+ // `listRowRecipe.density.comfortable.twoLineMinHeight`.
684
+ rowTwoLineMinHeight: 68,
685
+ };
686
+ export const selectionControlRecipe = {
687
+ slots: [
688
+ "root",
689
+ "control",
690
+ "indicator",
691
+ "leading",
692
+ "content",
693
+ "label",
694
+ "description",
695
+ ],
696
+ defaults: { kind: "checkbox", size: "medium", presentation: "card" },
697
+ sizes: {
698
+ small: {
699
+ rowMinHeight: control.minTouchTarget,
700
+ control: 20,
701
+ hitSlop: 12,
702
+ glyph: "xs",
703
+ gap: spacing.xs,
704
+ paddingHorizontal: spacing.sm,
705
+ paddingVertical: spacing.xs,
706
+ labelVariant: "label",
707
+ descriptionVariant: "caption",
708
+ },
709
+ medium: {
710
+ rowMinHeight: layout.rowHeight.singleLine,
711
+ control: control.selectionIndicator,
712
+ hitSlop: 10,
713
+ glyph: "sm",
714
+ gap: spacing.sm,
715
+ paddingHorizontal: spacing.md,
716
+ paddingVertical: spacing.sm,
717
+ labelVariant: "body",
718
+ descriptionVariant: "label",
719
+ },
720
+ },
721
+ shapes: { checkbox: "sm", radio: "full" },
722
+ presentations: {
723
+ plain: {
724
+ background: null,
725
+ border: null,
726
+ borderWidth: 0,
727
+ radius: "md",
728
+ useSizePadding: false,
729
+ labelColor: null,
730
+ },
731
+ card: {
732
+ background: semanticColors.surface.default,
733
+ border: semanticColors.border.default,
734
+ borderWidth: stroke.default,
735
+ radius: "md",
736
+ useSizePadding: true,
737
+ labelColor: null,
738
+ },
739
+ // A row inside a card shared by the whole group (paired with
740
+ // `selectionGroupRecipe.orientations.*.gap.grouped`). The item owns its
741
+ // own padding so the selected fill runs the full width of the group, and
742
+ // only the first and last rows round — an item-level radius on every row
743
+ // left a visible white sliver in the group's corners. `labelColor` is
744
+ // pinned here (rather than left to the row) because a grouped row's
745
+ // label sits directly on the shared card background, not on its own
746
+ // plate — the row needs to know the label reads correctly there.
747
+ grouped: {
748
+ background: null,
749
+ border: null,
750
+ borderWidth: 0,
751
+ radius: "lg",
752
+ useSizePadding: true,
753
+ labelColor: semanticColors.content.primary,
754
+ },
755
+ },
756
+ label: {
757
+ color: semanticColors.content.body,
758
+ fontWeight: fontWeight.semibold,
759
+ checkedFontWeight: fontWeight.bold,
760
+ },
761
+ leading: { size: "md", color: semanticColors.content.secondary },
762
+ description: { color: semanticColors.content.secondary },
763
+ indicators: {
764
+ checkbox: { checked: "check", mixed: "dash" },
765
+ radio: { checked: "dot", mixed: null },
766
+ },
767
+ radioDotRatio: 0.42,
768
+ states: {
769
+ idleBackground: semanticColors.canvas,
770
+ idleBorder: semanticColors.content.secondary,
771
+ checkedBackground: semanticColors.action.brand.background,
772
+ checkedBorder: semanticColors.border.focus,
773
+ indicator: semanticColors.action.brand.content,
774
+ // The brand tint is one opaque role, not a translucent wash. A product
775
+ // author found that a 10% wash of `primary` (the previous value here)
776
+ // changed value with whatever sat behind it — #E8EFFB on a white card,
777
+ // #DCE5F3 on the canvas — so the same "selected" read as three different
778
+ // colors. `surface.brand` is the one opaque fill (same fix, same reason,
779
+ // as `segmentedControlRecipe.item.selectedBackground` above).
780
+ selectedBackground: semanticColors.surface.brand,
781
+ selectedBorder: semanticColors.border.focus,
782
+ hoverBackground: semanticColors.interaction.hover,
783
+ pressedBackground: semanticColors.interaction.pressed,
784
+ invalidBorder: semanticColors.border.danger,
785
+ focus: focusIndicatorContract,
786
+ disabledOpacity: opacity.disabled,
787
+ },
788
+ };
789
+ /**
790
+ * Persistent top-level navigation. The recipe reserves an optional center gap
791
+ * for a sibling primary action but never treats that action as a destination.
792
+ */
793
+ export const bottomNavigationRecipe = {
794
+ slots: [
795
+ "root",
796
+ "surface",
797
+ "list",
798
+ "item",
799
+ "indicator",
800
+ "icon",
801
+ "label",
802
+ "badge",
803
+ ],
804
+ defaults: bottomNavigationRecipeDefaults,
805
+ adaptive: {
806
+ web: "fixed-compact-viewport",
807
+ native: "navigator-tab-bar",
808
+ },
809
+ presentations: {
810
+ bar: {
811
+ background: semanticColors.canvas,
812
+ border: semanticColors.border.default,
813
+ borderWidth: stroke.default,
814
+ borderEdges: ["block-start"],
815
+ radius: null,
816
+ shadow: null,
817
+ maxWidth: null,
818
+ outerPaddingHorizontal: 0,
819
+ outerPaddingTop: 0,
820
+ },
821
+ floating: {
822
+ background: semanticColors.canvas,
823
+ border: semanticColors.border.default,
824
+ borderWidth: stroke.default,
825
+ borderEdges: ["all"],
826
+ radius: "xl",
827
+ shadow: floatingSurfaceContract.shadow,
828
+ maxWidth: 384,
829
+ outerPaddingHorizontal: spacing.md,
830
+ outerPaddingTop: spacing.xs,
831
+ },
832
+ },
833
+ distributions: {
834
+ equal: { centerGap: 0, requiresEvenItemCount: false },
835
+ "center-gap": {
836
+ centerGap: control.buttonHeight.large + spacing.md,
837
+ requiresEvenItemCount: true,
838
+ },
839
+ },
840
+ density: {
841
+ compact: {
842
+ itemMinWidth: 52,
843
+ itemMinHeight: 52,
844
+ padding: spacing.xxs,
845
+ gap: 2,
846
+ icon: "sm",
847
+ label: "caption",
848
+ },
849
+ regular: {
850
+ itemMinWidth: 56,
851
+ itemMinHeight: 64,
852
+ padding: spacing.xs,
853
+ gap: spacing.xxs,
854
+ icon: "md",
855
+ label: "caption",
856
+ },
857
+ },
858
+ colors: {
859
+ idle: semanticColors.content.secondary,
860
+ selectedIcon: semanticColors.content.brand,
861
+ selectedLabel: semanticColors.content.brand,
862
+ },
863
+ /**
864
+ * Stable icon/badge layout anchor. Selection never paints this slot as a
865
+ * pill: the icon and label below own the non-color selected evidence.
866
+ */
867
+ indicator: {
868
+ minWidth: 40,
869
+ minHeight: 28,
870
+ radius: "full",
871
+ visual: "none",
872
+ background: null,
873
+ border: null,
874
+ borderWidth: 0,
875
+ },
876
+ icon: {
877
+ /**
878
+ * Adapters apply at least one supported emphasis. Stroke-oriented glyphs
879
+ * use strokeWidth; renderers without stroke control use scale instead.
880
+ */
881
+ selectedEmphasis: {
882
+ minimumAdaptations: 1,
883
+ strokeWidth: { idle: stroke.strong, selected: 3 },
884
+ scale: { idle: 1, selected: 1.06 },
885
+ },
886
+ },
887
+ label: {
888
+ fontWeight: fontWeight.semibold,
889
+ selectedFontWeight: fontWeight.bold,
890
+ textAlign: "center",
891
+ wrap: true,
892
+ fixedLines: null,
893
+ },
894
+ badge: {
895
+ size: "small",
896
+ variant: "floating",
897
+ anchor: { blockStart: -spacing.xxs, inlineEnd: -spacing.xs },
898
+ subtreeHiddenFromAccessibility: true,
899
+ },
900
+ safeArea: {
901
+ mode: "additive",
902
+ minimumBottomPadding: spacing.xs,
903
+ },
904
+ keyboard: { defaultBehavior: "hide", movesAboveKeyboard: false },
905
+ largeText: {
906
+ allowFontScaling: true,
907
+ // Persistent navigation has to keep every destination visible at once.
908
+ // Let labels respond to Dynamic Type, but cap the visual chrome before a
909
+ // three-character destination turns into a three-line column. The full,
910
+ // uncapped label remains the item's accessible name.
911
+ maxFontSizeMultiplier: 1.4,
912
+ fixedItemHeight: false,
913
+ labelWraps: true,
914
+ },
915
+ direction: {
916
+ itemOrder: "logical",
917
+ badgeAnchor: "inline-end",
918
+ },
919
+ states: {
920
+ hoverBackground: semanticColors.interaction.hover,
921
+ pressedBackground: semanticColors.interaction.pressed,
922
+ focus: focusIndicatorContract,
923
+ disabledOpacity: opacity.disabled,
924
+ selectedNonColorEvidence: {
925
+ target: "icon-and-label",
926
+ label: "font-weight",
927
+ icon: "emphasis",
928
+ },
929
+ selectedFocusSeparation: {
930
+ selectedTarget: "icon-and-label",
931
+ focusTarget: "item",
932
+ minimumGap: focusIndicatorContract.offset,
933
+ },
934
+ },
935
+ transition: motionPreset.micro,
936
+ };
937
+ export const tabsRecipe = {
938
+ slots: ["root", "list", "tab", "label", "indicator", "panel"],
939
+ defaults: { size: "medium", layout: "content", overflow: "scroll" },
940
+ sizes: {
941
+ small: {
942
+ minHeight: control.minTouchTarget,
943
+ paddingHorizontal: spacing.sm,
944
+ textVariant: "label",
945
+ },
946
+ medium: {
947
+ minHeight: 48,
948
+ paddingHorizontal: spacing.md,
949
+ textVariant: "body",
950
+ },
951
+ },
952
+ colors: {
953
+ idle: semanticColors.content.secondary,
954
+ selected: semanticColors.content.primary,
955
+ indicator: semanticColors.border.focus,
956
+ divider: semanticColors.border.default,
957
+ },
958
+ gap: spacing.xs,
959
+ icon: { glyph: "xs" },
960
+ label: { fontWeight: fontWeight.semibold, selectedFontWeight: fontWeight.bold },
961
+ layouts: {
962
+ content: { fitted: false },
963
+ fitted: { fitted: true },
964
+ },
965
+ overflow: {
966
+ scroll: { scrollable: true, wrap: false },
967
+ clip: { scrollable: false, wrap: false },
968
+ },
969
+ indicatorHeight: stroke.strong,
970
+ states: {
971
+ pressedBackground: semanticColors.interaction.pressed,
972
+ focus: focusIndicatorContract,
973
+ disabledOpacity: opacity.disabled,
974
+ },
975
+ };
976
+ export const noticeRecipe = {
977
+ slots: ["root", "icon", "content", "title", "description", "action"],
978
+ defaults: { tone: "info" },
979
+ tones: {
980
+ ...semanticColors.feedback,
981
+ info: {
982
+ ...semanticColors.feedback.info,
983
+ // The informational wash landed bluer and darker than the selection
984
+ // tint itself over canvas, so a banner outranked the control the
985
+ // reader was supposed to act on. `info` alone goes neutral — its
986
+ // accent stays in the icon, the title, and the border; `success`/
987
+ // `warning`/`attention`/`danger` keep the tinted surface because a
988
+ // product author confirmed those readings didn't have the same
989
+ // conflict with a selection state.
990
+ background: semanticColors.surface.sunken,
991
+ },
992
+ },
993
+ radius: "md",
994
+ padding: spacing.md,
995
+ gap: spacing.sm,
996
+ contentGap: spacing.xxs,
997
+ iconSize: "sm",
998
+ borderWidth: stroke.default,
999
+ title: { textVariant: "body", fontWeight: fontWeight.bold },
1000
+ description: { color: semanticColors.content.body, textVariant: "body" },
1001
+ };
1002
+ export const skeletonRecipe = {
1003
+ slots: ["root"],
1004
+ defaults: { shape: "block", animated: false },
1005
+ background: semanticColors.surface.sunken,
1006
+ shapes: {
1007
+ block: { radius: "md", defaultHeight: spacing.xxl },
1008
+ text: { radius: "sm", defaultHeight: spacing.md },
1009
+ circle: { radius: "full", defaultHeight: glyph.xxl },
1010
+ },
1011
+ animation: {
1012
+ duration: motion.slow * 4,
1013
+ easing: "standard",
1014
+ fromOpacity: 0.56,
1015
+ toOpacity: 1,
1016
+ reducedMotion: "static",
1017
+ },
1018
+ };
1019
+ export const emptyStateRecipe = {
1020
+ slots: ["root", "icon", "title", "description", "action"],
1021
+ defaults: { density: "regular" },
1022
+ density: {
1023
+ compact: { paddingVertical: spacing.xl },
1024
+ regular: { paddingVertical: spacing.xxxl },
1025
+ },
1026
+ paddingHorizontal: spacing.xl,
1027
+ gap: spacing.xs,
1028
+ icon: { size: "lg", color: semanticColors.content.decorative },
1029
+ title: { textVariant: "body", color: semanticColors.content.primary, fontWeight: fontWeight.semibold },
1030
+ description: { textVariant: "label", color: semanticColors.content.secondary },
1031
+ };
1032
+ export const spinnerRecipe = {
1033
+ slots: ["root"],
1034
+ defaults: { size: "medium", tone: "brand" },
1035
+ sizes: { small: glyph.xs, medium: glyph.sm, large: glyph.lg },
1036
+ tones: {
1037
+ brand: semanticColors.content.brand,
1038
+ neutral: semanticColors.content.secondary,
1039
+ inverse: semanticColors.content.inverse,
1040
+ },
1041
+ strokeWidth: stroke.strong,
1042
+ animation: {
1043
+ duration: 800,
1044
+ easing: "standard",
1045
+ reducedMotion: "static",
1046
+ },
1047
+ };
1048
+ export const loadMoreRecipe = {
1049
+ slots: [
1050
+ "root",
1051
+ "status",
1052
+ "spinner",
1053
+ "trigger",
1054
+ "error",
1055
+ "retry",
1056
+ "end",
1057
+ ],
1058
+ defaults: { mode: "automatic", density: "regular" },
1059
+ density: {
1060
+ compact: { paddingVertical: spacing.sm, gap: spacing.xs },
1061
+ regular: { paddingVertical: spacing.lg, gap: spacing.sm },
1062
+ },
1063
+ status: {
1064
+ color: semanticColors.content.secondary,
1065
+ textVariant: "label",
1066
+ },
1067
+ error: {
1068
+ color: semanticColors.content.danger,
1069
+ textVariant: "label",
1070
+ },
1071
+ end: {
1072
+ color: semanticColors.content.secondary,
1073
+ textVariant: "caption",
1074
+ },
1075
+ trigger: {
1076
+ minHeight: control.minTouchTarget,
1077
+ paddingHorizontal: spacing.md,
1078
+ radius: "md",
1079
+ color: semanticColors.content.brand,
1080
+ textVariant: "label",
1081
+ fontWeight: fontWeight.bold,
1082
+ },
1083
+ spinner: { size: "small", tone: "brand" },
1084
+ states: {
1085
+ focus: focusIndicatorContract,
1086
+ pressedOpacity: opacity.pressed,
1087
+ disabledOpacity: opacity.disabled,
1088
+ },
1089
+ };
1090
+ export const avatarRecipe = {
1091
+ slots: ["root", "image", "fallback", "badge"],
1092
+ defaults: { size: "medium", shape: "circle" },
1093
+ sizes: { small: 32, medium: 40, large: 48, xlarge: 64 },
1094
+ shapes: { rounded: "md", circle: "full" },
1095
+ background: semanticColors.surface.sunken,
1096
+ content: semanticColors.content.secondary,
1097
+ border: semanticColors.border.default,
1098
+ };
1099
+ export const statisticRecipe = {
1100
+ slots: [
1101
+ "group",
1102
+ "root",
1103
+ "label",
1104
+ "valueRow",
1105
+ "prefix",
1106
+ "value",
1107
+ "suffix",
1108
+ "trend",
1109
+ "trendMark",
1110
+ "hint",
1111
+ ],
1112
+ defaults: { density: "comfortable", presentation: "plain", columns: 3 },
1113
+ density: {
1114
+ compact: {
1115
+ padding: spacing.sm,
1116
+ gap: spacing.xxs,
1117
+ labelVariant: "caption",
1118
+ valueVariant: "title",
1119
+ },
1120
+ comfortable: {
1121
+ padding: spacing.md,
1122
+ gap: spacing.xs,
1123
+ labelVariant: "label",
1124
+ valueVariant: "heading",
1125
+ },
1126
+ },
1127
+ presentations: {
1128
+ plain: { background: null, border: null, borderWidth: 0, radius: "md" },
1129
+ surface: {
1130
+ background: semanticColors.surface.default,
1131
+ border: semanticColors.border.default,
1132
+ borderWidth: stroke.default,
1133
+ radius: "md",
1134
+ },
1135
+ },
1136
+ group: {
1137
+ gap: spacing.xs,
1138
+ minItemWidth: 120,
1139
+ columns: [1, 2, 3, 4],
1140
+ },
1141
+ label: {
1142
+ color: semanticColors.content.secondary,
1143
+ fontWeight: fontWeight.semibold,
1144
+ },
1145
+ value: {
1146
+ color: semanticColors.content.primary,
1147
+ fontWeight: fontWeight.heavy,
1148
+ numericVariant: "tabular",
1149
+ maxLines: null,
1150
+ },
1151
+ affix: {
1152
+ color: semanticColors.content.body,
1153
+ textVariant: "body",
1154
+ fontWeight: fontWeight.semibold,
1155
+ },
1156
+ hint: {
1157
+ color: semanticColors.content.secondary,
1158
+ textVariant: "caption",
1159
+ },
1160
+ trend: {
1161
+ textVariant: "caption",
1162
+ fontWeight: fontWeight.bold,
1163
+ gap: spacing.xxs,
1164
+ marks: {
1165
+ up: "trendUp",
1166
+ down: "trendDown",
1167
+ flat: "trendFlat",
1168
+ },
1169
+ tones: {
1170
+ neutral: semanticColors.content.secondary,
1171
+ success: semanticColors.feedback.success.foreground,
1172
+ warning: semanticColors.feedback.warning.foreground,
1173
+ danger: semanticColors.content.danger,
1174
+ },
1175
+ },
1176
+ };
1177
+ export const sheetRecipe = {
1178
+ slots: ["backdrop", "positioner", "content", "handle", "header", "title", "body", "footer", "close"],
1179
+ defaults: { placement: "bottom" },
1180
+ backdrop: backdrop.modal,
1181
+ content: {
1182
+ background: semanticColors.canvas,
1183
+ border: semanticColors.border.default,
1184
+ borderWidth: floatingSurfaceContract.borderWidth,
1185
+ radius: "xl",
1186
+ shadow: floatingSurfaceContract.shadow,
1187
+ maxHeightRatio: 0.9,
1188
+ paddingHorizontal: spacing.lg,
1189
+ paddingTop: spacing.sm,
1190
+ paddingBottom: spacing.sm,
1191
+ },
1192
+ web: { maxWidth: 640 },
1193
+ safeArea: {
1194
+ edge: "bottom",
1195
+ mode: "additive",
1196
+ minimumPadding: spacing.sm,
1197
+ },
1198
+ handle: {
1199
+ width: 36,
1200
+ height: 4,
1201
+ radius: "full",
1202
+ color: semanticColors.content.secondary,
1203
+ visibleByDefault: false,
1204
+ showWhen: "swipe-dismiss-enabled",
1205
+ },
1206
+ header: { minHeight: control.minTouchTarget, gap: spacing.sm },
1207
+ title: {
1208
+ color: semanticColors.content.primary,
1209
+ textVariant: "title",
1210
+ fontWeight: fontWeight.bold,
1211
+ gap: spacing.xxs,
1212
+ },
1213
+ body: {
1214
+ color: semanticColors.content.body,
1215
+ textVariant: "body",
1216
+ gap: spacing.md,
1217
+ },
1218
+ footer: {
1219
+ color: semanticColors.content.body,
1220
+ textVariant: "body",
1221
+ gap: spacing.sm,
1222
+ paddingTop: spacing.sm,
1223
+ },
1224
+ transition: { enter: motionPreset.enter, exit: motionPreset.exit },
1225
+ };
1226
+ export const dialogRecipe = {
1227
+ slots: ["backdrop", "positioner", "content", "header", "title", "description", "body", "footer", "close"],
1228
+ defaults: { size: "medium", dismissible: true },
1229
+ sizes: {
1230
+ small: { maxWidth: 320, padding: spacing.lg },
1231
+ medium: { maxWidth: 420, padding: spacing.xl },
1232
+ large: { maxWidth: 640, padding: spacing.xl },
1233
+ },
1234
+ backdrop: backdrop.modal,
1235
+ content: {
1236
+ background: semanticColors.canvas,
1237
+ border: semanticColors.border.default,
1238
+ borderWidth: floatingSurfaceContract.borderWidth,
1239
+ radius: "lg",
1240
+ gap: spacing.md,
1241
+ shadow: floatingSurfaceContract.shadow,
1242
+ },
1243
+ transition: { enter: motionPreset.enter, exit: motionPreset.exit },
1244
+ };
1245
+ export const alertDialogRecipe = {
1246
+ slots: [
1247
+ "backdrop",
1248
+ "positioner",
1249
+ "content",
1250
+ "icon",
1251
+ "title",
1252
+ "description",
1253
+ "status",
1254
+ "error",
1255
+ "actions",
1256
+ "cancel",
1257
+ "confirm",
1258
+ ],
1259
+ defaults: { tone: "attention", size: "small" },
1260
+ sizes: dialogRecipe.sizes,
1261
+ backdrop: dialogRecipe.backdrop,
1262
+ content: dialogRecipe.content,
1263
+ tones: {
1264
+ attention: {
1265
+ icon: semanticColors.feedback.attention.foreground,
1266
+ iconBackground: semanticColors.feedback.attention.background,
1267
+ confirm: semanticColors.action.brand.background,
1268
+ confirmContent: semanticColors.action.brand.content,
1269
+ },
1270
+ danger: {
1271
+ icon: semanticColors.content.danger,
1272
+ iconBackground: semanticColors.feedback.danger.background,
1273
+ confirm: semanticColors.action.danger.background,
1274
+ confirmContent: semanticColors.action.danger.content,
1275
+ },
1276
+ },
1277
+ icon: { containerSize: control.minTouchTarget, glyph: "md", radius: "full" },
1278
+ title: {
1279
+ color: semanticColors.content.primary,
1280
+ textVariant: "title",
1281
+ fontWeight: fontWeight.bold,
1282
+ },
1283
+ description: {
1284
+ color: semanticColors.content.secondary,
1285
+ textVariant: "body",
1286
+ },
1287
+ error: formSupportContract.error,
1288
+ actions: {
1289
+ gap: spacing.sm,
1290
+ stackBelow: breakpoint.medium,
1291
+ minButtonWidth: 96,
1292
+ },
1293
+ transition: dialogRecipe.transition,
1294
+ };
1295
+ export const toastRecipe = {
1296
+ slots: [
1297
+ "viewport",
1298
+ "root",
1299
+ "toneMark",
1300
+ "icon",
1301
+ "content",
1302
+ "title",
1303
+ "description",
1304
+ "action",
1305
+ "close",
1306
+ ],
1307
+ defaults: { tone: "neutral", placement: "bottom", durationMs: 5000 },
1308
+ adaptive: { web: "fixed-viewport", native: "safe-area-overlay" },
1309
+ viewport: {
1310
+ layer: layer.toast,
1311
+ gap: spacing.sm,
1312
+ inset: spacing.md,
1313
+ maxWidth: 420,
1314
+ safeAreaMode: "additive",
1315
+ },
1316
+ placements: {
1317
+ top: { blockEdge: "top", inlineEdge: "center", stackFrom: "top" },
1318
+ "top-start": { blockEdge: "top", inlineEdge: "start", stackFrom: "top" },
1319
+ "top-end": { blockEdge: "top", inlineEdge: "end", stackFrom: "top" },
1320
+ bottom: { blockEdge: "bottom", inlineEdge: "center", stackFrom: "bottom" },
1321
+ "bottom-start": { blockEdge: "bottom", inlineEdge: "start", stackFrom: "bottom" },
1322
+ "bottom-end": { blockEdge: "bottom", inlineEdge: "end", stackFrom: "bottom" },
1323
+ },
1324
+ tones: {
1325
+ neutral: {
1326
+ foreground: semanticColors.content.primary,
1327
+ accent: semanticColors.content.secondary,
1328
+ mark: "notifications",
1329
+ },
1330
+ info: {
1331
+ foreground: semanticColors.content.primary,
1332
+ accent: semanticColors.feedback.info.foreground,
1333
+ mark: "info",
1334
+ },
1335
+ success: {
1336
+ foreground: semanticColors.content.primary,
1337
+ accent: semanticColors.feedback.success.foreground,
1338
+ mark: "success",
1339
+ },
1340
+ warning: {
1341
+ foreground: semanticColors.content.primary,
1342
+ accent: semanticColors.feedback.warning.foreground,
1343
+ mark: "warning",
1344
+ },
1345
+ danger: {
1346
+ foreground: semanticColors.content.primary,
1347
+ accent: semanticColors.content.danger,
1348
+ mark: "alert",
1349
+ },
1350
+ },
1351
+ surface: {
1352
+ background: semanticColors.canvas,
1353
+ border: semanticColors.border.strong,
1354
+ borderWidth: stroke.default,
1355
+ radius: "md",
1356
+ shadow: floatingSurfaceContract.shadow,
1357
+ minHeight: layout.rowHeight.singleLine,
1358
+ padding: spacing.md,
1359
+ gap: spacing.sm,
1360
+ maxWidth: 420,
1361
+ },
1362
+ toneMark: { width: stroke.strong, radius: "full" },
1363
+ icon: { glyph: "sm" },
1364
+ content: { gap: spacing.xxs },
1365
+ title: {
1366
+ color: semanticColors.content.primary,
1367
+ textVariant: "body",
1368
+ fontWeight: fontWeight.bold,
1369
+ },
1370
+ description: {
1371
+ color: semanticColors.content.body,
1372
+ textVariant: "body",
1373
+ },
1374
+ action: {
1375
+ color: semanticColors.content.brand,
1376
+ textVariant: "body",
1377
+ fontWeight: fontWeight.bold,
1378
+ minHeight: control.minTouchTarget,
1379
+ paddingHorizontal: spacing.xs,
1380
+ },
1381
+ close: {
1382
+ color: semanticColors.content.secondary,
1383
+ diameter: control.minTouchTarget,
1384
+ glyph: "xs",
1385
+ },
1386
+ states: {
1387
+ pressedOpacity: opacity.pressed,
1388
+ draggedOpacity: opacity.dragged,
1389
+ focus: focusIndicatorContract,
1390
+ },
1391
+ gesture: { dismissThreshold: 50 },
1392
+ transition: {
1393
+ web: { enter: motionPreset.micro, exit: motionPreset.exit },
1394
+ native: { enter: motionPreset.enter, exit: motionPreset.exit },
1395
+ },
1396
+ };
1397
+ export const tooltipRecipe = {
1398
+ slots: ["provider", "trigger", "content", "arrow"],
1399
+ defaults: tooltipDescriptorDefaults,
1400
+ layer: layer.tooltip,
1401
+ surface: {
1402
+ ...floatingSurfaceContract,
1403
+ background: semanticColors.content.primary,
1404
+ border: semanticColors.content.primary,
1405
+ radius: "sm",
1406
+ padding: spacing.xs,
1407
+ },
1408
+ content: {
1409
+ color: semanticColors.canvas,
1410
+ textVariant: "label",
1411
+ maxWidth: 280,
1412
+ },
1413
+ arrow: {
1414
+ width: 10,
1415
+ height: 5,
1416
+ color: semanticColors.content.primary,
1417
+ padding: spacing.xs,
1418
+ },
1419
+ positioning: {
1420
+ sideOffset: spacing.xxs,
1421
+ collisionPadding: spacing.sm,
1422
+ },
1423
+ transition: { enter: motionPreset.micro, exit: motionPreset.exit },
1424
+ };
1425
+ export const topBarRecipe = {
1426
+ slots: [
1427
+ "root",
1428
+ "leading",
1429
+ "title",
1430
+ "titleLeading",
1431
+ "titleAction",
1432
+ "trailing",
1433
+ "action",
1434
+ "actionLabel",
1435
+ ],
1436
+ defaults: { centered: true },
1437
+ minHeight: control.buttonHeight.large,
1438
+ sideMinWidth: control.minTouchTarget,
1439
+ paddingHorizontal: spacing.md,
1440
+ gap: spacing.xs,
1441
+ largeTextThreshold: 1.6,
1442
+ title: { textVariant: "bodyLarge", color: semanticColors.content.primary, fontWeight: fontWeight.bold },
1443
+ titleAction: {
1444
+ minHeight: control.minTouchTarget,
1445
+ minWidth: control.minTouchTarget,
1446
+ gap: spacing.xs,
1447
+ pressedOpacity: opacity.pressed,
1448
+ },
1449
+ action: {
1450
+ minHeight: control.minTouchTarget,
1451
+ minWidth: control.minTouchTarget,
1452
+ gap: spacing.xxs,
1453
+ paddingHorizontal: spacing.xxs,
1454
+ pressedOpacity: opacity.pressed,
1455
+ disabledOpacity: opacity.disabled,
1456
+ },
1457
+ actionLabel: {
1458
+ textVariant: "caption",
1459
+ color: semanticColors.content.primary,
1460
+ fontWeight: fontWeight.medium,
1461
+ },
1462
+ background: semanticColors.canvas,
1463
+ };
1464
+ export const bottomCtaRecipe = {
1465
+ slots: ["root", "description", "secondaryAction", "primaryAction"],
1466
+ minHeight: 64,
1467
+ paddingHorizontal: layout.pagePadding.regular,
1468
+ paddingTop: spacing.sm,
1469
+ paddingBottom: spacing.sm,
1470
+ gap: spacing.sm,
1471
+ // A footer painted in the canvas tone left only a 1pt hairline between it
1472
+ // and the content scrolling underneath — a product author found that the
1473
+ // line cut a glyph in half at whatever scroll position the reader stopped
1474
+ // at, reading as a broken card rather than a footer. Layering by surface
1475
+ // (one step up from canvas) instead of by shadow alone is how this
1476
+ // component builds the separation; `shadow` below is reserved for the one
1477
+ // case that still needs it — see its comment.
1478
+ background: semanticColors.surface.default,
1479
+ border: semanticColors.border.default,
1480
+ borderWidth: stroke.default,
1481
+ // The cut described above is the scroll viewport's edge, not a spacing
1482
+ // problem — content clips mid-glyph, and a hairline between two
1483
+ // similarly-light planes can't carry that on its own. The footer casts
1484
+ // upward instead: the one case where a shadow, rather than a surface step,
1485
+ // is the right tool, because the footer genuinely overlaps the content
1486
+ // beneath it.
1487
+ shadow: {
1488
+ color: "#000000",
1489
+ opacity: 0.08,
1490
+ radius: 8,
1491
+ offsetY: -2,
1492
+ elevation: 8,
1493
+ },
1494
+ };
1495
+ export const sectionRecipe = {
1496
+ slots: ["root", "header", "copy", "title", "description", "action", "content"],
1497
+ gap: spacing.xs,
1498
+ headerGap: spacing.sm,
1499
+ copyGap: spacing.xxs,
1500
+ title: { textVariant: "title", color: semanticColors.content.body, fontWeight: fontWeight.bold },
1501
+ description: { textVariant: "caption", color: semanticColors.content.secondary },
1502
+ };
1503
+ //# sourceMappingURL=component-recipes.js.map