@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,247 @@
1
+ import { fieldFrameContract, focusIndicatorContract, formSupportContract, } from "./component-contracts.js";
2
+ import { control, opacity, spacing } from "./foundations.js";
3
+ import { semanticColors } from "./semantic-colors.js";
4
+ export const numericRangeDefaults = {
5
+ step: 1,
6
+ };
7
+ export function assertFiniteNumber(value, field) {
8
+ if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
9
+ throw new TypeError(`${field} must be a finite number`);
10
+ }
11
+ }
12
+ export function validateNumericRangeConfig(config) {
13
+ assertFiniteNumber(config.min, "min");
14
+ assertFiniteNumber(config.max, "max");
15
+ if (config.min >= config.max) {
16
+ throw new RangeError("min must be less than max");
17
+ }
18
+ const step = config.step ?? numericRangeDefaults.step;
19
+ assertFiniteNumber(step, "step");
20
+ if (step <= 0) {
21
+ throw new RangeError("step must be greater than 0");
22
+ }
23
+ if (step > config.max - config.min) {
24
+ throw new RangeError("step must not exceed the min–max span");
25
+ }
26
+ }
27
+ /** Number of decimal digits a finite numeric boundary carries. */
28
+ function decimalPrecision(value) {
29
+ const [coefficient = "", exponentText] = value.toString().toLowerCase().split("e");
30
+ const dotIndex = coefficient.indexOf(".");
31
+ const fractionDigits = dotIndex === -1 ? 0 : coefficient.length - dotIndex - 1;
32
+ const exponent = exponentText === undefined ? 0 : Number(exponentText);
33
+ return Math.max(0, fractionDigits - exponent);
34
+ }
35
+ /**
36
+ * A step grid is anchored at `min`, so its exact decimal precision comes from
37
+ * both the origin and the increment. Looking only at `step` turns the valid
38
+ * sequence 0.05, 0.15, 0.25… into 0.1, 0.2, 0.3… after the first operation.
39
+ */
40
+ function roundToStepGridPrecision(value, min, step) {
41
+ const decimals = Math.max(decimalPrecision(min), decimalPrecision(step));
42
+ if (decimals === 0)
43
+ return Math.round(value);
44
+ const shift = (number, places) => {
45
+ const [coefficient = "0", exponentText] = number.toString().toLowerCase().split("e");
46
+ const exponent = exponentText === undefined ? 0 : Number(exponentText);
47
+ return Number(`${coefficient}e${exponent + places}`);
48
+ };
49
+ return shift(Math.round(shift(value, decimals)), -decimals);
50
+ }
51
+ export function clampToRange(value, config) {
52
+ validateNumericRangeConfig(config);
53
+ assertFiniteNumber(value, "value");
54
+ return Math.min(config.max, Math.max(config.min, value));
55
+ }
56
+ /** Snaps to the nearest step from `min`, then clamps into [min, max]. */
57
+ export function snapToStep(value, config) {
58
+ validateNumericRangeConfig(config);
59
+ assertFiniteNumber(value, "value");
60
+ const step = config.step ?? numericRangeDefaults.step;
61
+ const steps = Math.round((value - config.min) / step);
62
+ const snapped = roundToStepGridPrecision(config.min + steps * step, config.min, step);
63
+ return clampToRange(snapped, config);
64
+ }
65
+ /** Move to the next valid grid boundary in the requested direction. */
66
+ export function stepNumericValue(value, config, direction) {
67
+ const snapped = snapToStep(value, config);
68
+ if (direction === "increment" && snapped > value)
69
+ return snapped;
70
+ if (direction === "decrement" && snapped < value)
71
+ return snapped;
72
+ const step = config.step ?? numericRangeDefaults.step;
73
+ const delta = direction === "increment" ? step : -step;
74
+ return snapToStep(value + delta, config);
75
+ }
76
+ /**
77
+ * Parses the renderer's editable text without guessing at locale, currency,
78
+ * or unit formatting. Empty text is a first-class `null` value; `undefined`
79
+ * means the draft is not a complete finite decimal number yet.
80
+ *
81
+ * Exponent notation is accepted because `String(number)` may produce it for
82
+ * very small or large finite values. Renderers keep incomplete drafts such as
83
+ * `-` or `1e` locally and only commit once this parser succeeds.
84
+ */
85
+ export function parseNumberFieldInput(input) {
86
+ const normalized = input.trim();
87
+ if (normalized.length === 0)
88
+ return null;
89
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(normalized)) {
90
+ return undefined;
91
+ }
92
+ const value = Number(normalized);
93
+ return Number.isFinite(value) ? value : undefined;
94
+ }
95
+ /**
96
+ * Turns an editable draft into a model value at blur/submit boundaries.
97
+ * Direct input is clamped and snapped with the same range math used by the
98
+ * steppers and Slider. Invalid/incomplete text remains `undefined`, allowing a
99
+ * renderer to restore the last committed value without inventing an error.
100
+ */
101
+ export function commitNumberFieldInput(input, config) {
102
+ validateNumericRangeConfig(config);
103
+ const parsed = parseNumberFieldInput(input);
104
+ if (parsed === undefined || parsed === null)
105
+ return parsed;
106
+ return snapToStep(parsed, config);
107
+ }
108
+ export function validateNumberFieldDescriptor(descriptor) {
109
+ validateNumericRangeConfig(descriptor);
110
+ if (descriptor.value !== null) {
111
+ assertFiniteNumber(descriptor.value, "value");
112
+ if (descriptor.value < descriptor.min || descriptor.value > descriptor.max) {
113
+ throw new RangeError("NumberField value must be within min and max");
114
+ }
115
+ }
116
+ }
117
+ export function resolveNumberFieldDescriptor(descriptor) {
118
+ validateNumberFieldDescriptor(descriptor);
119
+ return { ...descriptor, step: descriptor.step ?? numericRangeDefaults.step };
120
+ }
121
+ /** Empty has no boundary yet, so neither stepper direction is disabled. */
122
+ export function resolveNumberFieldStepperState(descriptor) {
123
+ validateNumberFieldDescriptor(descriptor);
124
+ if (descriptor.value === null) {
125
+ return { incrementDisabled: false, decrementDisabled: false };
126
+ }
127
+ return {
128
+ incrementDisabled: descriptor.value >= descriptor.max,
129
+ decrementDisabled: descriptor.value <= descriptor.min,
130
+ };
131
+ }
132
+ /** Resolve stepper availability from the visible draft, falling back safely. */
133
+ export function resolveNumberFieldInputStepperState(input, descriptor) {
134
+ validateNumberFieldDescriptor(descriptor);
135
+ const parsed = parseNumberFieldInput(input);
136
+ if (parsed === undefined)
137
+ return resolveNumberFieldStepperState(descriptor);
138
+ if (parsed === null) {
139
+ return resolveNumberFieldStepperState({ ...descriptor, value: null });
140
+ }
141
+ return resolveNumberFieldStepperState({
142
+ ...descriptor,
143
+ value: clampToRange(parsed, descriptor),
144
+ });
145
+ }
146
+ /** Stepping from empty lands on the boundary you are moving toward. */
147
+ export function stepNumberFieldValue(descriptor, direction) {
148
+ validateNumberFieldDescriptor(descriptor);
149
+ if (descriptor.value === null) {
150
+ return direction === "increment" ? descriptor.min : descriptor.max;
151
+ }
152
+ return stepNumericValue(descriptor.value, descriptor, direction);
153
+ }
154
+ /**
155
+ * Resolves a step action from the editable draft without first snapping and
156
+ * then stepping a second time. Off-grid drafts move to the next valid boundary
157
+ * in the requested direction; invalid drafts fall back to the committed model.
158
+ */
159
+ export function stepNumberFieldInput(input, descriptor, direction) {
160
+ validateNumberFieldDescriptor(descriptor);
161
+ const parsed = parseNumberFieldInput(input);
162
+ if (parsed === undefined)
163
+ return stepNumberFieldValue(descriptor, direction);
164
+ if (parsed === null) {
165
+ return stepNumberFieldValue({ ...descriptor, value: null }, direction);
166
+ }
167
+ return stepNumericValue(parsed, descriptor, direction);
168
+ }
169
+ /**
170
+ * Reuses the Field frame (`fieldFrameContract`) and form support copy
171
+ * (`formSupportContract`) verbatim — a second field frame would drift from
172
+ * Field the first time either one's border or height changes.
173
+ */
174
+ export const numberFieldRecipe = {
175
+ slots: [
176
+ "root",
177
+ "frame",
178
+ "input",
179
+ "decrement",
180
+ "increment",
181
+ "description",
182
+ "error",
183
+ ],
184
+ defaults: { size: "medium" },
185
+ frame: fieldFrameContract,
186
+ support: formSupportContract,
187
+ sizes: {
188
+ medium: {
189
+ minHeight: fieldFrameContract.minHeight,
190
+ paddingHorizontal: fieldFrameContract.paddingHorizontal,
191
+ textVariant: "body",
192
+ stepperDiameter: control.minTouchTarget,
193
+ },
194
+ large: {
195
+ minHeight: control.buttonHeight.large,
196
+ paddingHorizontal: spacing.lg,
197
+ textVariant: "bodyLarge",
198
+ stepperDiameter: control.minTouchTarget,
199
+ },
200
+ },
201
+ value: {
202
+ color: semanticColors.content.body,
203
+ numericVariant: "tabular",
204
+ },
205
+ stepper: {
206
+ color: semanticColors.content.secondary,
207
+ minTarget: control.minTouchTarget,
208
+ },
209
+ states: {
210
+ focus: focusIndicatorContract,
211
+ invalidBorder: semanticColors.border.danger,
212
+ disabledOpacity: opacity.disabled,
213
+ },
214
+ };
215
+ /**
216
+ * Held-repeat on the stepper buttons is deliberately not part of this
217
+ * contract: no product using NumberField has measured demand for press-and-
218
+ * hold, and adding it now would mean guessing at a repeat-rate/acceleration
219
+ * curve nobody has validated. A single activation per press only.
220
+ */
221
+ export const numberFieldBehavior = {
222
+ controlled: ["value", "defaultValue", "onValueChange"],
223
+ stateAxes: {
224
+ availability: ["enabled", "disabled", "readOnly"],
225
+ value: ["empty", "filled"],
226
+ validation: ["valid", "invalid"],
227
+ },
228
+ web: {
229
+ roles: ["spinbutton"],
230
+ keyboard: ["Tab", "ArrowUp", "ArrowDown"],
231
+ focus: "native",
232
+ },
233
+ native: {
234
+ roles: ["text", "button"],
235
+ states: ["disabled"],
236
+ actions: ["focus", "setText", "increment", "decrement"],
237
+ },
238
+ scenarios: [
239
+ "value-and-range-are-independent-controlled-inputs",
240
+ "stepper-buttons-disable-at-min-and-max",
241
+ "empty-disables-neither-stepper-direction",
242
+ "keyboard-arrow-up-down-step-like-the-stepper-buttons",
243
+ "invalid-is-a-numberfield-only-axis-independent-of-range",
244
+ "held-repeat-is-not-part-of-the-contract",
245
+ ],
246
+ };
247
+ //# sourceMappingURL=number-field.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-field.js","sourceRoot":"","sources":["../src/number-field.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ColorReference } from \"./color-references.js\";\nimport type { BehaviorContract } from \"./behaviors.js\";\nimport {\n fieldFrameContract,\n focusIndicatorContract,\n formSupportContract,\n} from \"./component-contracts.js\";\nimport { control, opacity, spacing, type TextVariant } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\n/**\n * Shared by NumberField and Slider: both solve \"pick a number within a range.\"\n * Keeping min/max/step/value judgment in one place keeps the two components\n * from drifting into two different meanings of the same numeric domain.\n */\nexport type NumericRangeConfig = Readonly<{\n min: number;\n max: number;\n step?: number;\n}>;\n\nexport const numericRangeDefaults = {\n step: 1,\n} as const satisfies Readonly<{ step: number }>;\n\nexport function assertFiniteNumber(value: number, field: string): void {\n if (typeof value !== \"number\" || Number.isNaN(value) || !Number.isFinite(value)) {\n throw new TypeError(`${field} must be a finite number`);\n }\n}\n\nexport function validateNumericRangeConfig(config: NumericRangeConfig): void {\n assertFiniteNumber(config.min, \"min\");\n assertFiniteNumber(config.max, \"max\");\n if (config.min >= config.max) {\n throw new RangeError(\"min must be less than max\");\n }\n const step = config.step ?? numericRangeDefaults.step;\n assertFiniteNumber(step, \"step\");\n if (step <= 0) {\n throw new RangeError(\"step must be greater than 0\");\n }\n if (step > config.max - config.min) {\n throw new RangeError(\"step must not exceed the min–max span\");\n }\n}\n\n/** Number of decimal digits a finite numeric boundary carries. */\nfunction decimalPrecision(value: number): number {\n const [coefficient = \"\", exponentText] = value.toString().toLowerCase().split(\"e\");\n const dotIndex = coefficient.indexOf(\".\");\n const fractionDigits = dotIndex === -1 ? 0 : coefficient.length - dotIndex - 1;\n const exponent = exponentText === undefined ? 0 : Number(exponentText);\n return Math.max(0, fractionDigits - exponent);\n}\n\n/**\n * A step grid is anchored at `min`, so its exact decimal precision comes from\n * both the origin and the increment. Looking only at `step` turns the valid\n * sequence 0.05, 0.15, 0.25… into 0.1, 0.2, 0.3… after the first operation.\n */\nfunction roundToStepGridPrecision(value: number, min: number, step: number): number {\n const decimals = Math.max(decimalPrecision(min), decimalPrecision(step));\n if (decimals === 0) return Math.round(value);\n const shift = (number: number, places: number): number => {\n const [coefficient = \"0\", exponentText] = number.toString().toLowerCase().split(\"e\");\n const exponent = exponentText === undefined ? 0 : Number(exponentText);\n return Number(`${coefficient}e${exponent + places}`);\n };\n return shift(Math.round(shift(value, decimals)), -decimals);\n}\n\nexport function clampToRange(value: number, config: NumericRangeConfig): number {\n validateNumericRangeConfig(config);\n assertFiniteNumber(value, \"value\");\n return Math.min(config.max, Math.max(config.min, value));\n}\n\n/** Snaps to the nearest step from `min`, then clamps into [min, max]. */\nexport function snapToStep(value: number, config: NumericRangeConfig): number {\n validateNumericRangeConfig(config);\n assertFiniteNumber(value, \"value\");\n const step = config.step ?? numericRangeDefaults.step;\n const steps = Math.round((value - config.min) / step);\n const snapped = roundToStepGridPrecision(config.min + steps * step, config.min, step);\n return clampToRange(snapped, config);\n}\n\n/** Move to the next valid grid boundary in the requested direction. */\nexport function stepNumericValue(\n value: number,\n config: NumericRangeConfig,\n direction: \"increment\" | \"decrement\",\n): number {\n const snapped = snapToStep(value, config);\n if (direction === \"increment\" && snapped > value) return snapped;\n if (direction === \"decrement\" && snapped < value) return snapped;\n const step = config.step ?? numericRangeDefaults.step;\n const delta = direction === \"increment\" ? step : -step;\n return snapToStep(value + delta, config);\n}\n\n/**\n * `null` means no value has been entered yet — distinct from any in-range\n * number, including `min`. Products decide whether empty is allowed to submit.\n */\nexport type NumberFieldValue = number | null;\n\n/**\n * Parses the renderer's editable text without guessing at locale, currency,\n * or unit formatting. Empty text is a first-class `null` value; `undefined`\n * means the draft is not a complete finite decimal number yet.\n *\n * Exponent notation is accepted because `String(number)` may produce it for\n * very small or large finite values. Renderers keep incomplete drafts such as\n * `-` or `1e` locally and only commit once this parser succeeds.\n */\nexport function parseNumberFieldInput(input: string): NumberFieldValue | undefined {\n const normalized = input.trim();\n if (normalized.length === 0) return null;\n if (!/^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$/.test(normalized)) {\n return undefined;\n }\n const value = Number(normalized);\n return Number.isFinite(value) ? value : undefined;\n}\n\n/**\n * Turns an editable draft into a model value at blur/submit boundaries.\n * Direct input is clamped and snapped with the same range math used by the\n * steppers and Slider. Invalid/incomplete text remains `undefined`, allowing a\n * renderer to restore the last committed value without inventing an error.\n */\nexport function commitNumberFieldInput(\n input: string,\n config: NumericRangeConfig,\n): NumberFieldValue | undefined {\n validateNumericRangeConfig(config);\n const parsed = parseNumberFieldInput(input);\n if (parsed === undefined || parsed === null) return parsed;\n return snapToStep(parsed, config);\n}\n\nexport type NumberFieldDescriptor = Readonly<{\n value: NumberFieldValue;\n min: number;\n max: number;\n step?: number;\n}>;\n\nexport type ResolvedNumberFieldDescriptor = Omit<NumberFieldDescriptor, \"step\"> &\n Readonly<{ step: number }>;\n\nexport function validateNumberFieldDescriptor(descriptor: NumberFieldDescriptor): void {\n validateNumericRangeConfig(descriptor);\n if (descriptor.value !== null) {\n assertFiniteNumber(descriptor.value, \"value\");\n if (descriptor.value < descriptor.min || descriptor.value > descriptor.max) {\n throw new RangeError(\"NumberField value must be within min and max\");\n }\n }\n}\n\nexport function resolveNumberFieldDescriptor(\n descriptor: NumberFieldDescriptor,\n): ResolvedNumberFieldDescriptor {\n validateNumberFieldDescriptor(descriptor);\n return { ...descriptor, step: descriptor.step ?? numericRangeDefaults.step };\n}\n\nexport type NumberFieldStepperState = Readonly<{\n incrementDisabled: boolean;\n decrementDisabled: boolean;\n}>;\n\n/** Empty has no boundary yet, so neither stepper direction is disabled. */\nexport function resolveNumberFieldStepperState(\n descriptor: NumberFieldDescriptor,\n): NumberFieldStepperState {\n validateNumberFieldDescriptor(descriptor);\n if (descriptor.value === null) {\n return { incrementDisabled: false, decrementDisabled: false };\n }\n return {\n incrementDisabled: descriptor.value >= descriptor.max,\n decrementDisabled: descriptor.value <= descriptor.min,\n };\n}\n\n/** Resolve stepper availability from the visible draft, falling back safely. */\nexport function resolveNumberFieldInputStepperState(\n input: string,\n descriptor: NumberFieldDescriptor,\n): NumberFieldStepperState {\n validateNumberFieldDescriptor(descriptor);\n const parsed = parseNumberFieldInput(input);\n if (parsed === undefined) return resolveNumberFieldStepperState(descriptor);\n if (parsed === null) {\n return resolveNumberFieldStepperState({ ...descriptor, value: null });\n }\n return resolveNumberFieldStepperState({\n ...descriptor,\n value: clampToRange(parsed, descriptor),\n });\n}\n\n/** Stepping from empty lands on the boundary you are moving toward. */\nexport function stepNumberFieldValue(\n descriptor: NumberFieldDescriptor,\n direction: \"increment\" | \"decrement\",\n): number {\n validateNumberFieldDescriptor(descriptor);\n if (descriptor.value === null) {\n return direction === \"increment\" ? descriptor.min : descriptor.max;\n }\n return stepNumericValue(descriptor.value, descriptor, direction);\n}\n\n/**\n * Resolves a step action from the editable draft without first snapping and\n * then stepping a second time. Off-grid drafts move to the next valid boundary\n * in the requested direction; invalid drafts fall back to the committed model.\n */\nexport function stepNumberFieldInput(\n input: string,\n descriptor: NumberFieldDescriptor,\n direction: \"increment\" | \"decrement\",\n): number {\n validateNumberFieldDescriptor(descriptor);\n const parsed = parseNumberFieldInput(input);\n if (parsed === undefined) return stepNumberFieldValue(descriptor, direction);\n if (parsed === null) {\n return stepNumberFieldValue({ ...descriptor, value: null }, direction);\n }\n return stepNumericValue(parsed, descriptor, direction);\n}\n\nexport type NumberFieldSize = \"medium\" | \"large\";\n\n/**\n * Reuses the Field frame (`fieldFrameContract`) and form support copy\n * (`formSupportContract`) verbatim — a second field frame would drift from\n * Field the first time either one's border or height changes.\n */\nexport const numberFieldRecipe = {\n slots: [\n \"root\",\n \"frame\",\n \"input\",\n \"decrement\",\n \"increment\",\n \"description\",\n \"error\",\n ] as const,\n defaults: { size: \"medium\" },\n frame: fieldFrameContract,\n support: formSupportContract,\n sizes: {\n medium: {\n minHeight: fieldFrameContract.minHeight,\n paddingHorizontal: fieldFrameContract.paddingHorizontal,\n textVariant: \"body\",\n stepperDiameter: control.minTouchTarget,\n },\n large: {\n minHeight: control.buttonHeight.large,\n paddingHorizontal: spacing.lg,\n textVariant: \"bodyLarge\",\n stepperDiameter: control.minTouchTarget,\n },\n },\n value: {\n color: semanticColors.content.body,\n numericVariant: \"tabular\",\n },\n stepper: {\n color: semanticColors.content.secondary,\n minTarget: control.minTouchTarget,\n },\n states: {\n focus: focusIndicatorContract,\n invalidBorder: semanticColors.border.danger,\n disabledOpacity: opacity.disabled,\n },\n} as const satisfies {\n slots: readonly [\n \"root\",\n \"frame\",\n \"input\",\n \"decrement\",\n \"increment\",\n \"description\",\n \"error\",\n ];\n defaults: { size: NumberFieldSize };\n frame: typeof fieldFrameContract;\n support: typeof formSupportContract;\n sizes: Record<\n NumberFieldSize,\n {\n minHeight: number;\n paddingHorizontal: number;\n textVariant: TextVariant;\n stepperDiameter: number;\n }\n >;\n value: { color: ColorReference; numericVariant: \"tabular\" };\n stepper: { color: ColorReference; minTarget: number };\n states: {\n focus: typeof focusIndicatorContract;\n invalidBorder: ColorReference;\n disabledOpacity: number;\n };\n};\n\n/**\n * Held-repeat on the stepper buttons is deliberately not part of this\n * contract: no product using NumberField has measured demand for press-and-\n * hold, and adding it now would mean guessing at a repeat-rate/acceleration\n * curve nobody has validated. A single activation per press only.\n */\nexport const numberFieldBehavior = {\n controlled: [\"value\", \"defaultValue\", \"onValueChange\"],\n stateAxes: {\n availability: [\"enabled\", \"disabled\", \"readOnly\"],\n value: [\"empty\", \"filled\"],\n validation: [\"valid\", \"invalid\"],\n },\n web: {\n roles: [\"spinbutton\"],\n keyboard: [\"Tab\", \"ArrowUp\", \"ArrowDown\"],\n focus: \"native\",\n },\n native: {\n roles: [\"text\", \"button\"],\n states: [\"disabled\"],\n actions: [\"focus\", \"setText\", \"increment\", \"decrement\"],\n },\n scenarios: [\n \"value-and-range-are-independent-controlled-inputs\",\n \"stepper-buttons-disable-at-min-and-max\",\n \"empty-disables-neither-stepper-direction\",\n \"keyboard-arrow-up-down-step-like-the-stepper-buttons\",\n \"invalid-is-a-numberfield-only-axis-independent-of-range\",\n \"held-repeat-is-not-part-of-the-contract\",\n ],\n} as const satisfies BehaviorContract;\n"]}
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Numeric-only, matching the domain the brief describes (인증번호). An
3
+ * alphanumeric OTP is out of scope until a real product needs one — adding
4
+ * a configurable character set now would mean guessing at a pattern nobody
5
+ * has validated. See docs/otp-field.md.
6
+ */
7
+ export type OtpFieldDescriptor = Readonly<{
8
+ /** Number of visible slots. At least two — a single slot is not the multi-slot problem this component solves (Steps applies the same floor to its own step count for the same reason). */
9
+ length: number;
10
+ /** The one logical value, always dense — index i is filled iff i < value.length. Never sparse; see docs/otp-field.md. */
11
+ value: string;
12
+ }>;
13
+ export declare function validateOtpFieldDescriptor(descriptor: OtpFieldDescriptor): void;
14
+ /** One entry per slot: the digit at that position, or "" for an unfilled slot. Always dense — no gaps. */
15
+ export declare function getOtpFieldSlotValues(descriptor: OtpFieldDescriptor): readonly string[];
16
+ export type ResolvedOtpFieldDescriptor = OtpFieldDescriptor & Readonly<{
17
+ slots: readonly string[];
18
+ complete: boolean;
19
+ }>;
20
+ export declare function resolveOtpFieldDescriptor(descriptor: OtpFieldDescriptor): ResolvedOtpFieldDescriptor;
21
+ /**
22
+ * The single hard accessibility judgment this contract makes: OtpField is
23
+ * ONE focusable, one accessible-name-and-value text control, not N separate
24
+ * focusable slots. N separate real inputs is the well-documented
25
+ * accessibility anti-pattern the brief names directly — a screen reader
26
+ * announcing "slot 3 of 6" tells the user nothing about what they are
27
+ * entering. The N visible boxes are a decorative rendering of one control's
28
+ * characters (`getOtpFieldSlotValues`), the same way a styled `<input>` with
29
+ * letter-spacing can look segmented while remaining one field. See
30
+ * docs/otp-field.md.
31
+ *
32
+ * Because there is one real control, typing, backspacing, and pasting
33
+ * (anywhere, including mid-string) are the platform's own native text
34
+ * editing — insertion, deletion, and paste-over-a-selection are not
35
+ * reimplemented here. The only judgment HJM owns is what happens to the
36
+ * resulting raw string: keep digits only, never exceed `length`. This is
37
+ * intentionally the one function this module needs for every edit path.
38
+ */
39
+ export declare function resolveOtpFieldValue(length: number, rawText: string): string;
40
+ export type OtpFieldSize = "medium" | "large";
41
+ /**
42
+ * The `slot` boxes reuse Field's own border/focus/invalid colors
43
+ * (`fieldFrameContract`) instead of inventing a new palette — each is a
44
+ * small Field-frame look-alike, not a new visual language.
45
+ */
46
+ export declare const otpFieldRecipe: {
47
+ readonly slots: readonly ["root", "input", "slot", "description", "error"];
48
+ readonly defaults: {
49
+ readonly size: "medium";
50
+ };
51
+ readonly support: {
52
+ readonly label: {
53
+ readonly color: Readonly<{
54
+ source: "theme";
55
+ key: "textBody";
56
+ alpha?: number;
57
+ }>;
58
+ readonly textVariant: "body";
59
+ readonly fontWeight: "600";
60
+ };
61
+ readonly hint: {
62
+ readonly color: Readonly<{
63
+ source: "theme";
64
+ key: "textMuted";
65
+ alpha?: number;
66
+ }>;
67
+ readonly textVariant: "label";
68
+ };
69
+ readonly error: {
70
+ readonly color: Readonly<{
71
+ source: "theme";
72
+ key: "danger";
73
+ alpha?: number;
74
+ }>;
75
+ readonly textVariant: "label";
76
+ };
77
+ readonly gap: 8;
78
+ };
79
+ readonly sizes: {
80
+ readonly medium: {
81
+ readonly slotSize: 44;
82
+ readonly gap: 8;
83
+ readonly textVariant: "title";
84
+ };
85
+ readonly large: {
86
+ readonly slotSize: 52;
87
+ readonly gap: 12;
88
+ readonly textVariant: "titleLarge";
89
+ };
90
+ };
91
+ readonly slot: {
92
+ readonly border: Readonly<{
93
+ source: "theme";
94
+ key: "textMuted";
95
+ alpha?: number;
96
+ }>;
97
+ readonly focusBorder: Readonly<{
98
+ source: "theme";
99
+ key: "contentBrand";
100
+ alpha?: number;
101
+ }>;
102
+ readonly invalidBorder: Readonly<{
103
+ source: "theme";
104
+ key: "danger";
105
+ alpha?: number;
106
+ }>;
107
+ readonly filledBorder: Readonly<{
108
+ source: "theme";
109
+ key: "contentBrand";
110
+ alpha?: number;
111
+ }>;
112
+ readonly radius: "md";
113
+ readonly borderWidth: 1;
114
+ readonly content: Readonly<{
115
+ source: "theme";
116
+ key: "text";
117
+ alpha?: number;
118
+ }>;
119
+ };
120
+ readonly states: {
121
+ readonly focus: {
122
+ readonly color: Readonly<{
123
+ source: "theme";
124
+ key: "contentBrand";
125
+ alpha?: number;
126
+ }>;
127
+ readonly width: 2;
128
+ readonly offset: 2;
129
+ };
130
+ readonly disabledOpacity: 0.5;
131
+ };
132
+ };
133
+ export declare const otpFieldBehavior: {
134
+ readonly controlled: readonly ["value", "defaultValue", "onValueChange"];
135
+ readonly stateAxes: {
136
+ readonly availability: readonly ["enabled", "disabled", "readOnly", "busy"];
137
+ readonly value: readonly ["empty", "filled"];
138
+ readonly validation: readonly ["valid", "invalid"];
139
+ };
140
+ readonly web: {
141
+ readonly roles: readonly ["textbox"];
142
+ readonly keyboard: readonly ["Tab", "ArrowLeft", "ArrowRight", "Home", "End"];
143
+ readonly focus: "native";
144
+ };
145
+ readonly native: {
146
+ readonly roles: readonly ["text"];
147
+ readonly states: readonly ["disabled", "busy"];
148
+ readonly actions: readonly ["focus", "setText"];
149
+ };
150
+ readonly scenarios: readonly ["one-accessible-name-and-value-for-the-whole-field-never-per-slot-announcement", "one-tab-stop-native-text-editing-owns-typing-backspacing-and-paste", "paste-anywhere-in-the-value-is-sanitized-to-digits-and-clamped-to-length", "non-digit-characters-are-stripped-not-rejected-outright", "resolveOtpFieldValue-truncates-typed-or-pasted-overflow-instead-of-throwing", "a-too-long-committed-descriptor-still-throws-as-malformed-state", "alphanumeric-otp-is-out-of-scope-until-a-real-product-need-exists"];
151
+ };
152
+ //# sourceMappingURL=otp-field.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otp-field.d.ts","sourceRoot":"","sources":["../src/otp-field.ts"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,0LAA0L;IAC1L,MAAM,EAAE,MAAM,CAAC;IACf,yHAAyH;IACzH,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,kBAAkB,GAAG,IAAI,CAU/E;AAED,0GAA0G;AAC1G,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,kBAAkB,GAC7B,SAAS,MAAM,EAAE,CAGnB;AAED,MAAM,MAAM,0BAA0B,GAAG,kBAAkB,GACzD,QAAQ,CAAC;IACP,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC,CAAC;AAEL,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,kBAAkB,GAC7B,0BAA0B,CAO5B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAQ5E;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkD1B,CAAC;AAEF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;CA0BQ,CAAC"}
@@ -0,0 +1,117 @@
1
+ import { fieldFrameContract, focusIndicatorContract, formSupportContract, } from "./component-contracts.js";
2
+ import { control, opacity, spacing } from "./foundations.js";
3
+ import { semanticColors } from "./semantic-colors.js";
4
+ export function validateOtpFieldDescriptor(descriptor) {
5
+ if (!Number.isInteger(descriptor.length) || descriptor.length < 2) {
6
+ throw new RangeError("OtpField length must be an integer of at least two slots");
7
+ }
8
+ if (typeof descriptor.value !== "string" || !/^\d*$/.test(descriptor.value)) {
9
+ throw new TypeError("OtpField value must be a string of digits only");
10
+ }
11
+ if (descriptor.value.length > descriptor.length) {
12
+ throw new RangeError("OtpField value must not be longer than length");
13
+ }
14
+ }
15
+ /** One entry per slot: the digit at that position, or "" for an unfilled slot. Always dense — no gaps. */
16
+ export function getOtpFieldSlotValues(descriptor) {
17
+ validateOtpFieldDescriptor(descriptor);
18
+ return Array.from({ length: descriptor.length }, (_, index) => descriptor.value[index] ?? "");
19
+ }
20
+ export function resolveOtpFieldDescriptor(descriptor) {
21
+ validateOtpFieldDescriptor(descriptor);
22
+ return {
23
+ ...descriptor,
24
+ slots: getOtpFieldSlotValues(descriptor),
25
+ complete: descriptor.value.length === descriptor.length,
26
+ };
27
+ }
28
+ /**
29
+ * The single hard accessibility judgment this contract makes: OtpField is
30
+ * ONE focusable, one accessible-name-and-value text control, not N separate
31
+ * focusable slots. N separate real inputs is the well-documented
32
+ * accessibility anti-pattern the brief names directly — a screen reader
33
+ * announcing "slot 3 of 6" tells the user nothing about what they are
34
+ * entering. The N visible boxes are a decorative rendering of one control's
35
+ * characters (`getOtpFieldSlotValues`), the same way a styled `<input>` with
36
+ * letter-spacing can look segmented while remaining one field. See
37
+ * docs/otp-field.md.
38
+ *
39
+ * Because there is one real control, typing, backspacing, and pasting
40
+ * (anywhere, including mid-string) are the platform's own native text
41
+ * editing — insertion, deletion, and paste-over-a-selection are not
42
+ * reimplemented here. The only judgment HJM owns is what happens to the
43
+ * resulting raw string: keep digits only, never exceed `length`. This is
44
+ * intentionally the one function this module needs for every edit path.
45
+ */
46
+ export function resolveOtpFieldValue(length, rawText) {
47
+ if (!Number.isInteger(length) || length < 2) {
48
+ throw new RangeError("OtpField length must be an integer of at least two slots");
49
+ }
50
+ if (typeof rawText !== "string") {
51
+ throw new TypeError("OtpField rawText must be a string");
52
+ }
53
+ return rawText.replace(/\D/g, "").slice(0, length);
54
+ }
55
+ /**
56
+ * The `slot` boxes reuse Field's own border/focus/invalid colors
57
+ * (`fieldFrameContract`) instead of inventing a new palette — each is a
58
+ * small Field-frame look-alike, not a new visual language.
59
+ */
60
+ export const otpFieldRecipe = {
61
+ slots: ["root", "input", "slot", "description", "error"],
62
+ defaults: { size: "medium" },
63
+ support: formSupportContract,
64
+ sizes: {
65
+ medium: {
66
+ slotSize: control.minTouchTarget,
67
+ gap: spacing.xs,
68
+ textVariant: "title",
69
+ },
70
+ large: {
71
+ slotSize: control.buttonHeight.large,
72
+ gap: spacing.sm,
73
+ textVariant: "titleLarge",
74
+ },
75
+ },
76
+ slot: {
77
+ border: fieldFrameContract.border,
78
+ focusBorder: fieldFrameContract.focusBorder,
79
+ invalidBorder: fieldFrameContract.invalidBorder,
80
+ filledBorder: semanticColors.content.brand,
81
+ radius: fieldFrameContract.radius,
82
+ borderWidth: fieldFrameContract.borderWidth,
83
+ content: semanticColors.content.primary,
84
+ },
85
+ states: {
86
+ focus: focusIndicatorContract,
87
+ disabledOpacity: opacity.disabled,
88
+ },
89
+ };
90
+ export const otpFieldBehavior = {
91
+ controlled: ["value", "defaultValue", "onValueChange"],
92
+ stateAxes: {
93
+ availability: ["enabled", "disabled", "readOnly", "busy"],
94
+ value: ["empty", "filled"],
95
+ validation: ["valid", "invalid"],
96
+ },
97
+ web: {
98
+ roles: ["textbox"],
99
+ keyboard: ["Tab", "ArrowLeft", "ArrowRight", "Home", "End"],
100
+ focus: "native",
101
+ },
102
+ native: {
103
+ roles: ["text"],
104
+ states: ["disabled", "busy"],
105
+ actions: ["focus", "setText"],
106
+ },
107
+ scenarios: [
108
+ "one-accessible-name-and-value-for-the-whole-field-never-per-slot-announcement",
109
+ "one-tab-stop-native-text-editing-owns-typing-backspacing-and-paste",
110
+ "paste-anywhere-in-the-value-is-sanitized-to-digits-and-clamped-to-length",
111
+ "non-digit-characters-are-stripped-not-rejected-outright",
112
+ "resolveOtpFieldValue-truncates-typed-or-pasted-overflow-instead-of-throwing",
113
+ "a-too-long-committed-descriptor-still-throws-as-malformed-state",
114
+ "alphanumeric-otp-is-out-of-scope-until-a-real-product-need-exists",
115
+ ],
116
+ };
117
+ //# sourceMappingURL=otp-field.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otp-field.js","sourceRoot":"","sources":["../src/otp-field.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BehaviorContract } from \"./behaviors.js\";\nimport type { ColorReference } from \"./color-references.js\";\nimport {\n fieldFrameContract,\n focusIndicatorContract,\n formSupportContract,\n} from \"./component-contracts.js\";\nimport { control, opacity, spacing, type TextVariant } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\n/**\n * Numeric-only, matching the domain the brief describes (인증번호). An\n * alphanumeric OTP is out of scope until a real product needs one — adding\n * a configurable character set now would mean guessing at a pattern nobody\n * has validated. See docs/otp-field.md.\n */\nexport type OtpFieldDescriptor = Readonly<{\n /** Number of visible slots. At least two — a single slot is not the multi-slot problem this component solves (Steps applies the same floor to its own step count for the same reason). */\n length: number;\n /** The one logical value, always dense — index i is filled iff i < value.length. Never sparse; see docs/otp-field.md. */\n value: string;\n}>;\n\nexport function validateOtpFieldDescriptor(descriptor: OtpFieldDescriptor): void {\n if (!Number.isInteger(descriptor.length) || descriptor.length < 2) {\n throw new RangeError(\"OtpField length must be an integer of at least two slots\");\n }\n if (typeof descriptor.value !== \"string\" || !/^\\d*$/.test(descriptor.value)) {\n throw new TypeError(\"OtpField value must be a string of digits only\");\n }\n if (descriptor.value.length > descriptor.length) {\n throw new RangeError(\"OtpField value must not be longer than length\");\n }\n}\n\n/** One entry per slot: the digit at that position, or \"\" for an unfilled slot. Always dense — no gaps. */\nexport function getOtpFieldSlotValues(\n descriptor: OtpFieldDescriptor,\n): readonly string[] {\n validateOtpFieldDescriptor(descriptor);\n return Array.from({ length: descriptor.length }, (_, index) => descriptor.value[index] ?? \"\");\n}\n\nexport type ResolvedOtpFieldDescriptor = OtpFieldDescriptor &\n Readonly<{\n slots: readonly string[];\n complete: boolean;\n }>;\n\nexport function resolveOtpFieldDescriptor(\n descriptor: OtpFieldDescriptor,\n): ResolvedOtpFieldDescriptor {\n validateOtpFieldDescriptor(descriptor);\n return {\n ...descriptor,\n slots: getOtpFieldSlotValues(descriptor),\n complete: descriptor.value.length === descriptor.length,\n };\n}\n\n/**\n * The single hard accessibility judgment this contract makes: OtpField is\n * ONE focusable, one accessible-name-and-value text control, not N separate\n * focusable slots. N separate real inputs is the well-documented\n * accessibility anti-pattern the brief names directly — a screen reader\n * announcing \"slot 3 of 6\" tells the user nothing about what they are\n * entering. The N visible boxes are a decorative rendering of one control's\n * characters (`getOtpFieldSlotValues`), the same way a styled `<input>` with\n * letter-spacing can look segmented while remaining one field. See\n * docs/otp-field.md.\n *\n * Because there is one real control, typing, backspacing, and pasting\n * (anywhere, including mid-string) are the platform's own native text\n * editing — insertion, deletion, and paste-over-a-selection are not\n * reimplemented here. The only judgment HJM owns is what happens to the\n * resulting raw string: keep digits only, never exceed `length`. This is\n * intentionally the one function this module needs for every edit path.\n */\nexport function resolveOtpFieldValue(length: number, rawText: string): string {\n if (!Number.isInteger(length) || length < 2) {\n throw new RangeError(\"OtpField length must be an integer of at least two slots\");\n }\n if (typeof rawText !== \"string\") {\n throw new TypeError(\"OtpField rawText must be a string\");\n }\n return rawText.replace(/\\D/g, \"\").slice(0, length);\n}\n\nexport type OtpFieldSize = \"medium\" | \"large\";\n\n/**\n * The `slot` boxes reuse Field's own border/focus/invalid colors\n * (`fieldFrameContract`) instead of inventing a new palette — each is a\n * small Field-frame look-alike, not a new visual language.\n */\nexport const otpFieldRecipe = {\n slots: [\"root\", \"input\", \"slot\", \"description\", \"error\"] as const,\n defaults: { size: \"medium\" },\n support: formSupportContract,\n sizes: {\n medium: {\n slotSize: control.minTouchTarget,\n gap: spacing.xs,\n textVariant: \"title\",\n },\n large: {\n slotSize: control.buttonHeight.large,\n gap: spacing.sm,\n textVariant: \"titleLarge\",\n },\n },\n slot: {\n border: fieldFrameContract.border,\n focusBorder: fieldFrameContract.focusBorder,\n invalidBorder: fieldFrameContract.invalidBorder,\n filledBorder: semanticColors.content.brand,\n radius: fieldFrameContract.radius,\n borderWidth: fieldFrameContract.borderWidth,\n content: semanticColors.content.primary,\n },\n states: {\n focus: focusIndicatorContract,\n disabledOpacity: opacity.disabled,\n },\n} as const satisfies {\n slots: readonly string[];\n defaults: { size: OtpFieldSize };\n support: typeof formSupportContract;\n sizes: Record<\n OtpFieldSize,\n { slotSize: number; gap: number; textVariant: TextVariant }\n >;\n slot: {\n border: ColorReference;\n focusBorder: ColorReference;\n invalidBorder: ColorReference;\n filledBorder: ColorReference;\n radius: typeof fieldFrameContract.radius;\n borderWidth: number;\n content: ColorReference;\n };\n states: {\n focus: typeof focusIndicatorContract;\n disabledOpacity: number;\n };\n};\n\nexport const otpFieldBehavior = {\n controlled: [\"value\", \"defaultValue\", \"onValueChange\"],\n stateAxes: {\n availability: [\"enabled\", \"disabled\", \"readOnly\", \"busy\"],\n value: [\"empty\", \"filled\"],\n validation: [\"valid\", \"invalid\"],\n },\n web: {\n roles: [\"textbox\"],\n keyboard: [\"Tab\", \"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"],\n focus: \"native\",\n },\n native: {\n roles: [\"text\"],\n states: [\"disabled\", \"busy\"],\n actions: [\"focus\", \"setText\"],\n },\n scenarios: [\n \"one-accessible-name-and-value-for-the-whole-field-never-per-slot-announcement\",\n \"one-tab-stop-native-text-editing-owns-typing-backspacing-and-paste\",\n \"paste-anywhere-in-the-value-is-sanitized-to-digits-and-clamped-to-length\",\n \"non-digit-characters-are-stripped-not-rejected-outright\",\n \"resolveOtpFieldValue-truncates-typed-or-pasted-overflow-instead-of-throwing\",\n \"a-too-long-committed-descriptor-still-throws-as-malformed-state\",\n \"alphanumeric-otp-is-out-of-scope-until-a-real-product-need-exists\",\n ],\n} as const satisfies BehaviorContract;\n"]}