decantr 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/AGENTS.md +868 -0
  2. package/CHANGELOG.md +255 -0
  3. package/CLAUDE.md +178 -0
  4. package/LICENSE +21 -0
  5. package/README.md +229 -0
  6. package/cli/art.js +127 -0
  7. package/cli/commands/a11y.js +61 -0
  8. package/cli/commands/audit.js +225 -0
  9. package/cli/commands/build.js +38 -0
  10. package/cli/commands/dev.js +18 -0
  11. package/cli/commands/doctor.js +197 -0
  12. package/cli/commands/figma-sync.js +48 -0
  13. package/cli/commands/figma-tokens.js +55 -0
  14. package/cli/commands/generate.js +26 -0
  15. package/cli/commands/init.js +116 -0
  16. package/cli/commands/lint.js +209 -0
  17. package/cli/commands/mcp.js +530 -0
  18. package/cli/commands/migrate.js +175 -0
  19. package/cli/commands/test.js +38 -0
  20. package/cli/commands/validate.js +354 -0
  21. package/cli/index.js +113 -0
  22. package/package.json +95 -0
  23. package/reference/atoms.md +517 -0
  24. package/reference/behaviors.md +384 -0
  25. package/reference/build-tooling.md +275 -0
  26. package/reference/color-guidelines.md +965 -0
  27. package/reference/component-lifecycle.md +137 -0
  28. package/reference/compound-spacing.md +95 -0
  29. package/reference/decantation-process.md +499 -0
  30. package/reference/dev-server-routes.md +93 -0
  31. package/reference/form-system.md +253 -0
  32. package/reference/i18n.md +336 -0
  33. package/reference/icons.md +576 -0
  34. package/reference/llm-primer.md +953 -0
  35. package/reference/plugins.md +252 -0
  36. package/reference/registry-consumption.md +76 -0
  37. package/reference/router.md +217 -0
  38. package/reference/shells.md +116 -0
  39. package/reference/spatial-guidelines.md +541 -0
  40. package/reference/ssr.md +234 -0
  41. package/reference/state-data.md +215 -0
  42. package/reference/state-patterns.md +166 -0
  43. package/reference/state.md +194 -0
  44. package/reference/style-system.md +110 -0
  45. package/reference/tokens.md +460 -0
  46. package/src/app.js +19 -0
  47. package/src/chart/_animate.js +266 -0
  48. package/src/chart/_base.js +109 -0
  49. package/src/chart/_data.js +209 -0
  50. package/src/chart/_format.js +106 -0
  51. package/src/chart/_interact.js +364 -0
  52. package/src/chart/_palette.js +105 -0
  53. package/src/chart/_renderer.js +52 -0
  54. package/src/chart/_scene.js +262 -0
  55. package/src/chart/_shared.js +371 -0
  56. package/src/chart/index.js +637 -0
  57. package/src/chart/layouts/_layout-base.js +328 -0
  58. package/src/chart/layouts/cartesian.js +148 -0
  59. package/src/chart/layouts/hierarchy.js +562 -0
  60. package/src/chart/layouts/polar.js +101 -0
  61. package/src/chart/renderers/canvas.js +179 -0
  62. package/src/chart/renderers/svg.js +256 -0
  63. package/src/chart/renderers/webgpu.js +715 -0
  64. package/src/chart/types/_type-base.js +26 -0
  65. package/src/chart/types/area.js +134 -0
  66. package/src/chart/types/bar.js +173 -0
  67. package/src/chart/types/box-plot.js +125 -0
  68. package/src/chart/types/bubble.js +63 -0
  69. package/src/chart/types/candlestick.js +115 -0
  70. package/src/chart/types/chord.js +85 -0
  71. package/src/chart/types/combination.js +108 -0
  72. package/src/chart/types/funnel.js +68 -0
  73. package/src/chart/types/gauge.js +163 -0
  74. package/src/chart/types/heatmap.js +98 -0
  75. package/src/chart/types/histogram.js +71 -0
  76. package/src/chart/types/line.js +111 -0
  77. package/src/chart/types/org-chart.js +93 -0
  78. package/src/chart/types/pie.js +81 -0
  79. package/src/chart/types/radar.js +96 -0
  80. package/src/chart/types/radial.js +68 -0
  81. package/src/chart/types/range-area.js +55 -0
  82. package/src/chart/types/range-bar.js +61 -0
  83. package/src/chart/types/sankey.js +73 -0
  84. package/src/chart/types/scatter.js +66 -0
  85. package/src/chart/types/sparkline.js +81 -0
  86. package/src/chart/types/sunburst.js +69 -0
  87. package/src/chart/types/swimlane.js +88 -0
  88. package/src/chart/types/treemap.js +62 -0
  89. package/src/chart/types/waterfall.js +100 -0
  90. package/src/components/_base.js +1658 -0
  91. package/src/components/_behaviors.js +1140 -0
  92. package/src/components/_primitives.js +534 -0
  93. package/src/components/_qr-encoder.js +539 -0
  94. package/src/components/accordion.js +207 -0
  95. package/src/components/affix.js +62 -0
  96. package/src/components/alert-dialog.js +75 -0
  97. package/src/components/alert.js +47 -0
  98. package/src/components/aspect-ratio.js +24 -0
  99. package/src/components/avatar-group.js +55 -0
  100. package/src/components/avatar.js +38 -0
  101. package/src/components/back-top.js +75 -0
  102. package/src/components/badge.js +74 -0
  103. package/src/components/banner.js +68 -0
  104. package/src/components/breadcrumb.js +162 -0
  105. package/src/components/button.js +115 -0
  106. package/src/components/calendar.js +131 -0
  107. package/src/components/card.js +192 -0
  108. package/src/components/carousel.js +98 -0
  109. package/src/components/cascader.js +261 -0
  110. package/src/components/checkbox.js +80 -0
  111. package/src/components/chip.js +81 -0
  112. package/src/components/code-block.js +82 -0
  113. package/src/components/collapsible.js +50 -0
  114. package/src/components/color-palette.js +438 -0
  115. package/src/components/color-picker.js +314 -0
  116. package/src/components/combobox.js +181 -0
  117. package/src/components/command.js +174 -0
  118. package/src/components/comment.js +206 -0
  119. package/src/components/context-menu.js +76 -0
  120. package/src/components/data-table.js +724 -0
  121. package/src/components/date-picker.js +217 -0
  122. package/src/components/date-range-picker.js +244 -0
  123. package/src/components/datetime-picker.js +271 -0
  124. package/src/components/descriptions.js +68 -0
  125. package/src/components/drawer.js +179 -0
  126. package/src/components/dropdown.js +88 -0
  127. package/src/components/empty.js +41 -0
  128. package/src/components/float-button.js +90 -0
  129. package/src/components/form.js +106 -0
  130. package/src/components/hover-card.js +49 -0
  131. package/src/components/icon.js +87 -0
  132. package/src/components/image.js +97 -0
  133. package/src/components/index.js +117 -0
  134. package/src/components/input-group.js +75 -0
  135. package/src/components/input-number.js +155 -0
  136. package/src/components/input-otp.js +178 -0
  137. package/src/components/input.js +91 -0
  138. package/src/components/kbd.js +36 -0
  139. package/src/components/label.js +25 -0
  140. package/src/components/list.js +118 -0
  141. package/src/components/masked-input.js +236 -0
  142. package/src/components/mentions.js +165 -0
  143. package/src/components/menu.js +259 -0
  144. package/src/components/message.js +80 -0
  145. package/src/components/modal.js +147 -0
  146. package/src/components/navigation-menu.js +166 -0
  147. package/src/components/notification.js +84 -0
  148. package/src/components/pagination.js +104 -0
  149. package/src/components/placeholder.js +132 -0
  150. package/src/components/popconfirm.js +70 -0
  151. package/src/components/popover.js +58 -0
  152. package/src/components/progress.js +61 -0
  153. package/src/components/qrcode.js +251 -0
  154. package/src/components/radiogroup.js +120 -0
  155. package/src/components/range-slider.js +176 -0
  156. package/src/components/rate.js +186 -0
  157. package/src/components/resizable.js +83 -0
  158. package/src/components/result.js +57 -0
  159. package/src/components/scroll-area.js +43 -0
  160. package/src/components/segmented.js +97 -0
  161. package/src/components/select.js +165 -0
  162. package/src/components/separator.js +31 -0
  163. package/src/components/shell.js +407 -0
  164. package/src/components/skeleton.js +39 -0
  165. package/src/components/slider.js +141 -0
  166. package/src/components/sortable-list.js +176 -0
  167. package/src/components/space.js +42 -0
  168. package/src/components/spinner.js +112 -0
  169. package/src/components/splitter.js +147 -0
  170. package/src/components/statistic.js +136 -0
  171. package/src/components/steps.js +99 -0
  172. package/src/components/switch.js +95 -0
  173. package/src/components/table.js +44 -0
  174. package/src/components/tabs.js +216 -0
  175. package/src/components/tag.js +115 -0
  176. package/src/components/textarea.js +82 -0
  177. package/src/components/time-picker.js +153 -0
  178. package/src/components/time-range-picker.js +170 -0
  179. package/src/components/timeline.js +226 -0
  180. package/src/components/toast.js +71 -0
  181. package/src/components/toggle.js +213 -0
  182. package/src/components/tooltip.js +57 -0
  183. package/src/components/tour.js +159 -0
  184. package/src/components/transfer.js +163 -0
  185. package/src/components/tree-select.js +274 -0
  186. package/src/components/tree.js +141 -0
  187. package/src/components/typography.js +136 -0
  188. package/src/components/upload.js +118 -0
  189. package/src/components/visually-hidden.js +20 -0
  190. package/src/components/watermark.js +124 -0
  191. package/src/core/index.js +539 -0
  192. package/src/core/lifecycle.js +69 -0
  193. package/src/css/atoms.js +651 -0
  194. package/src/css/components.js +940 -0
  195. package/src/css/derive.js +1296 -0
  196. package/src/css/index.js +265 -0
  197. package/src/css/runtime.js +268 -0
  198. package/src/css/styles/addons/bioluminescent.js +93 -0
  199. package/src/css/styles/addons/clay.js +70 -0
  200. package/src/css/styles/addons/clean.js +57 -0
  201. package/src/css/styles/addons/command-center.js +143 -0
  202. package/src/css/styles/addons/dopamine.js +83 -0
  203. package/src/css/styles/addons/editorial.js +80 -0
  204. package/src/css/styles/addons/glassmorphism.js +99 -0
  205. package/src/css/styles/addons/liquid-glass.js +105 -0
  206. package/src/css/styles/addons/prismatic.js +100 -0
  207. package/src/css/styles/addons/retro.js +63 -0
  208. package/src/css/styles/auradecantism.js +96 -0
  209. package/src/css/theme-registry.js +444 -0
  210. package/src/data/entity.js +281 -0
  211. package/src/data/index.js +13 -0
  212. package/src/data/persist.js +225 -0
  213. package/src/data/query.js +839 -0
  214. package/src/data/realtime.js +299 -0
  215. package/src/data/url.js +177 -0
  216. package/src/data/worker.js +134 -0
  217. package/src/explorer/archetypes.js +243 -0
  218. package/src/explorer/atoms.js +228 -0
  219. package/src/explorer/charts.js +497 -0
  220. package/src/explorer/components.js +129 -0
  221. package/src/explorer/foundations.js +949 -0
  222. package/src/explorer/icons.js +178 -0
  223. package/src/explorer/patterns.js +247 -0
  224. package/src/explorer/recipes.js +194 -0
  225. package/src/explorer/shared/pattern-examples.js +1337 -0
  226. package/src/explorer/shared/showcase-renderer.js +958 -0
  227. package/src/explorer/shared/spec-table.js +41 -0
  228. package/src/explorer/shared/usage-links.js +87 -0
  229. package/src/explorer/shell-config.js +10 -0
  230. package/src/explorer/shells.js +551 -0
  231. package/src/explorer/styles.js +161 -0
  232. package/src/explorer/tokens.js +262 -0
  233. package/src/explorer/tools.js +525 -0
  234. package/src/form/index.js +804 -0
  235. package/src/i18n/index.js +251 -0
  236. package/src/icons/essential.js +479 -0
  237. package/src/icons/index.js +53 -0
  238. package/src/plugins/index.js +282 -0
  239. package/src/registry/archetypes/content-site.json +71 -0
  240. package/src/registry/archetypes/docs-explorer.json +23 -0
  241. package/src/registry/archetypes/ecommerce.json +104 -0
  242. package/src/registry/archetypes/financial-dashboard.json +77 -0
  243. package/src/registry/archetypes/index.json +41 -0
  244. package/src/registry/archetypes/portfolio.json +82 -0
  245. package/src/registry/archetypes/recipe-community.json +159 -0
  246. package/src/registry/archetypes/saas-dashboard.json +86 -0
  247. package/src/registry/architect/cross-cutting.json +45 -0
  248. package/src/registry/architect/domains/ecommerce.json +294 -0
  249. package/src/registry/architect/domains/financial-services.json +302 -0
  250. package/src/registry/architect/index.json +26 -0
  251. package/src/registry/architect/traits.json +379 -0
  252. package/src/registry/atoms.json +16 -0
  253. package/src/registry/chart-showcase.json +160 -0
  254. package/src/registry/chart.json +136 -0
  255. package/src/registry/components.json +8616 -0
  256. package/src/registry/core.json +216 -0
  257. package/src/registry/css.json +319 -0
  258. package/src/registry/data.json +135 -0
  259. package/src/registry/foundations.json +11 -0
  260. package/src/registry/icons.json +463 -0
  261. package/src/registry/index.json +101 -0
  262. package/src/registry/patterns/activity-feed.json +37 -0
  263. package/src/registry/patterns/article-content.json +27 -0
  264. package/src/registry/patterns/auth-form.json +37 -0
  265. package/src/registry/patterns/author-card.json +20 -0
  266. package/src/registry/patterns/card-grid.json +127 -0
  267. package/src/registry/patterns/category-nav.json +26 -0
  268. package/src/registry/patterns/chart-grid.json +36 -0
  269. package/src/registry/patterns/chat-interface.json +37 -0
  270. package/src/registry/patterns/checklist-card.json +55 -0
  271. package/src/registry/patterns/comparison-panel.json +27 -0
  272. package/src/registry/patterns/component-showcase.json +24 -0
  273. package/src/registry/patterns/contact-form.json +31 -0
  274. package/src/registry/patterns/cta-section.json +20 -0
  275. package/src/registry/patterns/data-table.json +37 -0
  276. package/src/registry/patterns/detail-header.json +83 -0
  277. package/src/registry/patterns/detail-panel.json +27 -0
  278. package/src/registry/patterns/explorer-shell.json +22 -0
  279. package/src/registry/patterns/filter-bar.json +33 -0
  280. package/src/registry/patterns/filter-sidebar.json +27 -0
  281. package/src/registry/patterns/form-sections.json +110 -0
  282. package/src/registry/patterns/goal-tracker.json +27 -0
  283. package/src/registry/patterns/hero.json +107 -0
  284. package/src/registry/patterns/index.json +47 -0
  285. package/src/registry/patterns/kpi-grid.json +36 -0
  286. package/src/registry/patterns/media-gallery.json +20 -0
  287. package/src/registry/patterns/order-history.json +20 -0
  288. package/src/registry/patterns/pagination.json +19 -0
  289. package/src/registry/patterns/photo-to-recipe.json +36 -0
  290. package/src/registry/patterns/pipeline-tracker.json +28 -0
  291. package/src/registry/patterns/post-list.json +27 -0
  292. package/src/registry/patterns/pricing-table.json +32 -0
  293. package/src/registry/patterns/scorecard.json +28 -0
  294. package/src/registry/patterns/search-bar.json +20 -0
  295. package/src/registry/patterns/specimen-grid.json +19 -0
  296. package/src/registry/patterns/stat-card.json +55 -0
  297. package/src/registry/patterns/stats-bar.json +55 -0
  298. package/src/registry/patterns/steps-card.json +55 -0
  299. package/src/registry/patterns/table-of-contents.json +19 -0
  300. package/src/registry/patterns/testimonials.json +21 -0
  301. package/src/registry/patterns/timeline.json +27 -0
  302. package/src/registry/patterns/token-inspector.json +21 -0
  303. package/src/registry/patterns/wizard.json +27 -0
  304. package/src/registry/recipe-auradecantism.json +69 -0
  305. package/src/registry/recipe-clean.json +65 -0
  306. package/src/registry/recipe-command-center.json +78 -0
  307. package/src/registry/router.json +73 -0
  308. package/src/registry/schema/README.md +197 -0
  309. package/src/registry/skeletons.json +259 -0
  310. package/src/registry/state.json +137 -0
  311. package/src/registry/tokens.json +40 -0
  312. package/src/router/hash.js +17 -0
  313. package/src/router/history.js +18 -0
  314. package/src/router/index.js +598 -0
  315. package/src/ssr/index.js +922 -0
  316. package/src/state/arrays.js +181 -0
  317. package/src/state/devtools.js +647 -0
  318. package/src/state/index.js +498 -0
  319. package/src/state/middleware.js +288 -0
  320. package/src/state/scheduler.js +206 -0
  321. package/src/state/store.js +300 -0
  322. package/src/tags/index.js +19 -0
  323. package/src/tannins/auth.js +396 -0
  324. package/src/test/dom.js +352 -0
  325. package/src/test/index.js +62 -0
  326. package/src/test/state.js +306 -0
  327. package/tools/a11y-audit.js +487 -0
  328. package/tools/analyzer.js +315 -0
  329. package/tools/audit.js +706 -0
  330. package/tools/builder.js +1422 -0
  331. package/tools/css-extract.js +188 -0
  332. package/tools/dev-server.js +316 -0
  333. package/tools/dts-gen.js +1260 -0
  334. package/tools/figma-components.js +329 -0
  335. package/tools/figma-patterns.js +516 -0
  336. package/tools/figma-plugin/code.js +453 -0
  337. package/tools/figma-plugin/manifest.json +14 -0
  338. package/tools/figma-plugin/ui.html +268 -0
  339. package/tools/figma-render.js +293 -0
  340. package/tools/figma-tokens.js +712 -0
  341. package/tools/figma-upload.js +318 -0
  342. package/tools/generate.js +738 -0
  343. package/tools/icons.js +133 -0
  344. package/tools/init-templates.js +265 -0
  345. package/tools/install-hooks.sh +5 -0
  346. package/tools/migrations/0.5.0.js +53 -0
  347. package/tools/migrations/0.6.0.js +95 -0
  348. package/tools/minify.js +170 -0
  349. package/tools/pre-commit +4 -0
  350. package/tools/registry.js +662 -0
  351. package/tools/reset-playground.js +61 -0
  352. package/tools/starter-templates/content-site/app.js +49 -0
  353. package/tools/starter-templates/content-site/essence.js +19 -0
  354. package/tools/starter-templates/content-site/pages.js +31 -0
  355. package/tools/starter-templates/ecommerce/app.js +50 -0
  356. package/tools/starter-templates/ecommerce/essence.js +19 -0
  357. package/tools/starter-templates/ecommerce/pages.js +31 -0
  358. package/tools/starter-templates/landing-page/app.js +38 -0
  359. package/tools/starter-templates/landing-page/essence.js +18 -0
  360. package/tools/starter-templates/landing-page/pages.js +21 -0
  361. package/tools/starter-templates/portfolio/app.js +45 -0
  362. package/tools/starter-templates/portfolio/essence.js +19 -0
  363. package/tools/starter-templates/portfolio/pages.js +33 -0
  364. package/tools/starter-templates/saas-dashboard/app.js +70 -0
  365. package/tools/starter-templates/saas-dashboard/essence.js +19 -0
  366. package/tools/starter-templates/saas-dashboard/pages.js +31 -0
  367. package/tools/verify-pack.js +203 -0
  368. package/types/chart.d.ts +77 -0
  369. package/types/components.d.ts +587 -0
  370. package/types/core.d.ts +89 -0
  371. package/types/css.d.ts +149 -0
  372. package/types/data.d.ts +238 -0
  373. package/types/form.d.ts +164 -0
  374. package/types/i18n.d.ts +51 -0
  375. package/types/icons.d.ts +27 -0
  376. package/types/index.d.ts +13 -0
  377. package/types/router.d.ts +116 -0
  378. package/types/ssr.d.ts +102 -0
  379. package/types/state.d.ts +83 -0
  380. package/types/tags.d.ts +62 -0
  381. package/types/tannins.d.ts +63 -0
  382. package/types/test.d.ts +48 -0
@@ -0,0 +1,253 @@
1
+ # Form System Reference
2
+
3
+ `import { createForm, validators, useFormField } from 'decantr/form';`
4
+
5
+ ## createForm(config)
6
+
7
+ ### Config Shape
8
+
9
+ | Key | Type | Default | Description |
10
+ |-----|------|---------|-------------|
11
+ | `fields` | `{ [name]: FieldConfig }` | `{}` | Field definitions |
12
+ | `onSubmit` | `async (values, form) => void` | — | Submit handler |
13
+ | `validate` | `(values) => { [name]: string[] }` | — | Cross-field validation |
14
+ | `validateOn` | `'onChange' \| 'onBlur' \| 'onSubmit'` | `'onBlur'` | Validation trigger mode |
15
+
16
+ ### FieldConfig
17
+
18
+ | Key | Type | Default | Description |
19
+ |-----|------|---------|-------------|
20
+ | `value` | `any` | `''` | Initial value |
21
+ | `validators` | `Function[]` | `[]` | Array of validator functions |
22
+ | `transform` | `(raw) => transformed` | — | Transform applied on every `setValue` |
23
+
24
+ ### FormInstance API
25
+
26
+ | Member | Type | Description |
27
+ |--------|------|-------------|
28
+ | `field(name)` | `(string) => FieldInstance` | Get/create field by name |
29
+ | `fields` | `Proxy` | Dot-access fields: `form.fields.email.value()` |
30
+ | `values()` | `() => Object` | Memo — all current field + fieldArray values |
31
+ | `errors()` | `() => { [name]: string[] }` | Memo — all fields with errors |
32
+ | `isValid()` | `() => boolean` | Memo — true when all fields have zero errors |
33
+ | `isDirty()` | `() => boolean` | Memo — true when any field differs from initial |
34
+ | `isSubmitting()` | `() => boolean` | Signal — true during `onSubmit` execution |
35
+ | `isSubmitted()` | `() => boolean` | Signal — true after successful submit |
36
+ | `submitCount()` | `() => number` | Signal — number of submit attempts |
37
+ | `submit()` | `() => Promise<void>` | Touch all fields, validate, call `onSubmit` if valid |
38
+ | `reset(values?)` | `(Object?) => void` | Reset all fields; optional new initial values |
39
+ | `setValues(partial)` | `(Object) => void` | Batch-set multiple field values |
40
+ | `setErrors(errs)` | `(Object) => void` | Batch-set errors: `{ name: string \| string[] }` |
41
+ | `validate()` | `() => Promise<boolean>` | Run all field + cross-field validators |
42
+ | `fieldArray(name)` | `(string) => FieldArrayInstance` | Get/create field array |
43
+ | `watch(name, cb)` | `(string, (val, prev) => void) => unsub` | Watch single field changes |
44
+ | `watchAll(cb)` | `((values) => void) => unsub` | Watch any field change |
45
+
46
+ ### Submit Flow
47
+
48
+ 1. Increment `submitCount`
49
+ 2. Touch all fields (batch)
50
+ 3. Run all field validators (sync + async)
51
+ 4. Run `config.validate` cross-field validator (if provided)
52
+ 5. If all valid, set `isSubmitting(true)`, call `onSubmit(values, form)`
53
+ 6. On completion, set `isSubmitted(true)`, `isSubmitting(false)`
54
+
55
+ ## FieldInstance API
56
+
57
+ Returned by `form.field(name)` or `form.fields.name`.
58
+
59
+ | Member | Type | Description |
60
+ |--------|------|-------------|
61
+ | `name` | `string` | Field name |
62
+ | `value()` | `() => any` | Signal — current value |
63
+ | `error()` | `() => string \| null` | Memo — first error or null |
64
+ | `errors()` | `() => string[]` | Signal — all error messages |
65
+ | `touched()` | `() => boolean` | Signal — true after blur |
66
+ | `dirty()` | `() => boolean` | Memo — true when value differs from initial |
67
+ | `valid()` | `() => boolean` | Memo — true when no errors |
68
+ | `setValue(v)` | `(any \| (prev) => any) => void` | Set value (accepts updater fn); triggers validation per `validateOn` |
69
+ | `setTouched()` | `() => void` | Mark as touched; triggers validation if `validateOn: 'onBlur'` |
70
+ | `setError(msg)` | `(string) => void` | Set a single error manually |
71
+ | `reset(val?)` | `(any?) => void` | Reset to initial (or provided) value, clear errors/touched |
72
+ | `validate()` | `() => Promise<boolean>` | Run all validators imperatively |
73
+ | `bind()` | `() => BindProps` | Returns props object for component binding |
74
+
75
+ ### bind() Return Shape
76
+
77
+ ```
78
+ { value, onchange, onblur, error }
79
+ ```
80
+
81
+ - `value` — signal getter
82
+ - `onchange` — auto-extracts `e.target.value` from DOM events or accepts raw values
83
+ - `onblur` — calls `setTouched()`
84
+ - `error` — signal getter (first error or null)
85
+
86
+ Spread into any form component: `Input({ ...form.field('email').bind() })`
87
+
88
+ ## validators
89
+
90
+ All return `(value, allValues) => string|null`. Custom message via last `msg` param.
91
+
92
+ | Validator | Signature | Default Message |
93
+ |-----------|-----------|-----------------|
94
+ | `required` | `(msg?)` | `'Required'` |
95
+ | `minLength` | `(n, msg?)` | `'Must be at least {n} characters'` |
96
+ | `maxLength` | `(n, msg?)` | `'Must be at most {n} characters'` |
97
+ | `min` | `(n, msg?)` | `'Must be at least {n}'` |
98
+ | `max` | `(n, msg?)` | `'Must be at most {n}'` |
99
+ | `pattern` | `(regex, msg?)` | `'Invalid format'` |
100
+ | `email` | `(msg?)` | `'Invalid email address'` |
101
+ | `match` | `(fieldName, msg?)` | `'Must match {fieldName}'` |
102
+ | `custom` | `(fn, msg?)` | `'Invalid'` |
103
+ | `async` | `(fn, msg?)` | `'Invalid'` |
104
+
105
+ ### Validator behavior
106
+
107
+ - **Sync validators** run immediately on trigger (`onChange`/`onBlur`/`onSubmit`)
108
+ - **Async validators** (`validators.async()`) auto-debounce at 300ms; only run if all sync validators pass
109
+ - `custom(fn)` — `fn(value, allValues)` returns `true` (valid) or error string
110
+ - `async(fn)` — `fn(value, allValues)` returns `Promise<true|string>`
111
+ - `match(fieldName)` — cross-field equality check via `allValues[fieldName]`
112
+
113
+ ## useFormField(form, name)
114
+
115
+ Convenience hook — delegates to `form.field(name)`.
116
+
117
+ | Key | Type | Source |
118
+ |-----|------|--------|
119
+ | `value` | `() => any` | `field.value` |
120
+ | `setValue` | `(any) => void` | `field.setValue` |
121
+ | `error` | `() => string \| null` | `field.error` |
122
+ | `errors` | `() => string[]` | `field.errors` |
123
+ | `touched` | `() => boolean` | `field.touched` |
124
+ | `dirty` | `() => boolean` | `field.dirty` |
125
+ | `valid` | `() => boolean` | `field.valid` |
126
+ | `onBlur` | `() => void` | `field.setTouched` |
127
+ | `bind` | `() => BindProps` | `field.bind` |
128
+
129
+ ## fieldArray(name)
130
+
131
+ Returned by `form.fieldArray(name)`. Initial value from `config.fields[name].value` (must be array).
132
+
133
+ | Member | Type | Description |
134
+ |--------|------|-------------|
135
+ | `items()` | `() => any[]` | Signal — current array |
136
+ | `length()` | `() => number` | Memo — item count |
137
+ | `append(value)` | `(any) => void` | Add to end |
138
+ | `prepend(value)` | `(any) => void` | Add to beginning |
139
+ | `remove(index)` | `(number) => void` | Remove at index |
140
+ | `move(from, to)` | `(number, number) => void` | Move item between indices |
141
+ | `swap(a, b)` | `(number, number) => void` | Swap two items |
142
+ | `replace(index, value)` | `(number, any) => void` | Replace item at index |
143
+
144
+ ## Integration Example
145
+
146
+ ```javascript
147
+ import { tags } from 'decantr/tags';
148
+ import { css } from 'decantr/css';
149
+ import { createForm, validators } from 'decantr/form';
150
+ import { Input, Button, Text } from 'decantr/components';
151
+ import { cond, text } from 'decantr/core';
152
+
153
+ const { form: formEl, div } = tags;
154
+
155
+ const form = createForm({
156
+ fields: {
157
+ email: { value: '', validators: [validators.required(), validators.email()] },
158
+ password: { value: '', validators: [validators.required(), validators.minLength(8)] },
159
+ },
160
+ validateOn: 'onBlur',
161
+ async onSubmit(values) {
162
+ await fetch('/api/login', { method: 'POST', body: JSON.stringify(values) });
163
+ },
164
+ });
165
+
166
+ formEl({ class: css('_flex _col _gap4 _mw[24rem]'), onsubmit: (e) => { e.preventDefault(); form.submit(); } },
167
+ div({ class: css('_flex _col _gap1') },
168
+ Input({ type: 'email', placeholder: 'Email', ...form.field('email').bind() }),
169
+ cond(() => form.fields.email.touched() && form.fields.email.error(),
170
+ () => Text({ class: css('_fgdestructive _textSm') }, text(() => form.fields.email.error()))
171
+ ),
172
+ ),
173
+ div({ class: css('_flex _col _gap1') },
174
+ Input({ type: 'password', placeholder: 'Password', ...form.field('password').bind() }),
175
+ cond(() => form.fields.password.touched() && form.fields.password.error(),
176
+ () => Text({ class: css('_fgdestructive _textSm') }, text(() => form.fields.password.error()))
177
+ ),
178
+ ),
179
+ Button({ type: 'submit', disabled: () => form.isSubmitting() }, 'Login'),
180
+ );
181
+ ```
182
+
183
+ ## Async Validation
184
+
185
+ ```javascript
186
+ const form = createForm({
187
+ fields: {
188
+ username: {
189
+ value: '',
190
+ validators: [
191
+ validators.required(),
192
+ validators.minLength(3),
193
+ validators.async(async (value) => {
194
+ const res = await fetch(`/api/check-username?u=${value}`);
195
+ const { available } = await res.json();
196
+ return available ? true : 'Username already taken';
197
+ }),
198
+ ],
199
+ },
200
+ },
201
+ });
202
+ ```
203
+
204
+ Async validators auto-debounce (300ms). They only execute when all sync validators pass. Pending async validation does not block — errors update reactively when the promise resolves.
205
+
206
+ ## Error Handling
207
+
208
+ ### Server-side errors
209
+
210
+ ```javascript
211
+ const form = createForm({
212
+ fields: { email: { value: '' }, password: { value: '' } },
213
+ async onSubmit(values, form) {
214
+ const res = await fetch('/api/login', { method: 'POST', body: JSON.stringify(values) });
215
+ if (!res.ok) {
216
+ const { errors } = await res.json();
217
+ form.setErrors(errors); // { email: 'Not found', password: 'Incorrect' }
218
+ }
219
+ },
220
+ });
221
+ ```
222
+
223
+ ### Cross-field validation
224
+
225
+ ```javascript
226
+ const form = createForm({
227
+ fields: {
228
+ password: { value: '' },
229
+ confirm: { value: '' },
230
+ },
231
+ validate(values) {
232
+ const errs = {};
233
+ if (values.password !== values.confirm) {
234
+ errs.confirm = ['Passwords do not match'];
235
+ }
236
+ return errs;
237
+ },
238
+ });
239
+ ```
240
+
241
+ Cross-field `validate` runs during `form.validate()` and `form.submit()`, after all per-field validators pass.
242
+
243
+ ### Per-field imperative errors
244
+
245
+ ```javascript
246
+ form.field('email').setError('This email is banned');
247
+ ```
248
+
249
+ Cleared on next `setValue` or `reset`.
250
+
251
+ ---
252
+
253
+ **See also:** `reference/component-lifecycle.md`, `reference/atoms.md`
@@ -0,0 +1,336 @@
1
+ # Internationalization (i18n)
2
+
3
+ Decantr's i18n module provides reactive locale management, translation with interpolation, pluralization via `Intl.PluralRules`, RTL/LTR direction control, and runtime message merging. Zero external dependencies.
4
+
5
+ ## Quick Start
6
+
7
+ ```js
8
+ import { createI18n } from 'decantr/i18n';
9
+
10
+ const { t, setLocale, setDirection } = createI18n({
11
+ locale: 'en',
12
+ fallbackLocale: 'en',
13
+ messages: {
14
+ en: {
15
+ nav: { home: 'Home', about: 'About' },
16
+ greeting: 'Hello, {name}!',
17
+ items_one: '{count} item',
18
+ items_other: '{count} items'
19
+ },
20
+ fr: {
21
+ nav: { home: 'Accueil', about: 'A propos' },
22
+ greeting: 'Bonjour, {name} !',
23
+ items_one: '{count} article',
24
+ items_other: '{count} articles'
25
+ }
26
+ }
27
+ });
28
+
29
+ t('nav.home'); // "Home"
30
+ t('greeting', { name: 'Alice' }); // "Hello, Alice!"
31
+ t('items', { count: 3 }); // "3 items"
32
+
33
+ setLocale('fr');
34
+ t('nav.home'); // "Accueil"
35
+ ```
36
+
37
+ ## API
38
+
39
+ ### `createI18n(config): I18nInstance`
40
+
41
+ Creates a reactive i18n instance.
42
+
43
+ **Config:**
44
+
45
+ | Field | Type | Required | Description |
46
+ |-------|------|----------|-------------|
47
+ | `locale` | `string` | Yes | Initial locale (e.g. `'en'`, `'fr'`, `'ar'`) |
48
+ | `messages` | `Record<string, Record<string, any>>` | Yes | Messages keyed by locale |
49
+ | `fallbackLocale` | `string` | No | Locale to use when key is missing in the active locale |
50
+
51
+ **Returns:**
52
+
53
+ | Method | Signature | Description |
54
+ |--------|-----------|-------------|
55
+ | `t` | `(key: string, params?: Record<string, any>) => string` | Translate a key |
56
+ | `locale` | `() => string` | Signal getter for current locale |
57
+ | `setLocale` | `(locale: string) => void` | Set the active locale |
58
+ | `setDirection` | `(dir: 'ltr' \| 'rtl') => void` | Set `dir` attribute on `<html>` |
59
+ | `addMessages` | `(locale: string, messages: Record<string, any>) => void` | Merge messages into a locale |
60
+
61
+ ## Translation Keys
62
+
63
+ ### Dot Notation
64
+
65
+ Keys use dot-delimited paths to navigate nested message objects:
66
+
67
+ ```js
68
+ const messages = {
69
+ en: {
70
+ nav: {
71
+ home: 'Home',
72
+ settings: {
73
+ profile: 'Profile',
74
+ security: 'Security'
75
+ }
76
+ }
77
+ }
78
+ };
79
+
80
+ t('nav.home'); // "Home"
81
+ t('nav.settings.profile'); // "Profile"
82
+ ```
83
+
84
+ ### Interpolation
85
+
86
+ Use `{param}` placeholders. Values are converted to strings automatically:
87
+
88
+ ```js
89
+ const messages = {
90
+ en: {
91
+ welcome: 'Welcome, {name}!',
92
+ stats: '{count} views in {days} days'
93
+ }
94
+ };
95
+
96
+ t('welcome', { name: 'Alice' }); // "Welcome, Alice!"
97
+ t('stats', { count: 1200, days: 7 }); // "1200 views in 7 days"
98
+ ```
99
+
100
+ Unmatched placeholders are left as-is:
101
+
102
+ ```js
103
+ t('welcome'); // "Welcome, {name}!"
104
+ t('welcome', {}); // "Welcome, {name}!"
105
+ ```
106
+
107
+ ### Pluralization
108
+
109
+ Pass a `count` parameter to trigger pluralization. The system appends a suffix to the key using `Intl.PluralRules`:
110
+
111
+ | Suffix | English Example |
112
+ |--------|----------------|
113
+ | `_zero` | count = 0 (some languages) |
114
+ | `_one` | count = 1 |
115
+ | `_two` | count = 2 (Arabic, Welsh, etc.) |
116
+ | `_few` | count = 2-4 (Slavic languages) |
117
+ | `_many` | count = 5-20 (Slavic languages) |
118
+ | `_other` | Everything else |
119
+
120
+ ```js
121
+ const messages = {
122
+ en: {
123
+ items_one: '{count} item',
124
+ items_other: '{count} items'
125
+ }
126
+ };
127
+
128
+ t('items', { count: 0 }); // "0 items" (_other in English)
129
+ t('items', { count: 1 }); // "1 item" (_one)
130
+ t('items', { count: 42 }); // "42 items" (_other)
131
+ ```
132
+
133
+ If the plural-suffixed key is not found, the base key is used:
134
+
135
+ ```js
136
+ const messages = { en: { items: 'Items' } };
137
+
138
+ t('items', { count: 5 }); // "Items" (base key fallback)
139
+ ```
140
+
141
+ Pluralization works with interpolation:
142
+
143
+ ```js
144
+ const messages = {
145
+ en: {
146
+ cart_one: 'Your cart has {count} item ({total})',
147
+ cart_other: 'Your cart has {count} items ({total})'
148
+ }
149
+ };
150
+
151
+ t('cart', { count: 3, total: '$45.00' });
152
+ // "Your cart has 3 items ($45.00)"
153
+ ```
154
+
155
+ ## Fallback Chain
156
+
157
+ When a key is not found in the current locale, the system tries:
158
+
159
+ 1. Current locale messages
160
+ 2. Fallback locale messages (if configured)
161
+ 3. The raw key string
162
+
163
+ ```js
164
+ const { t, setLocale } = createI18n({
165
+ locale: 'de',
166
+ fallbackLocale: 'en',
167
+ messages: {
168
+ en: { save: 'Save', cancel: 'Cancel' },
169
+ de: { save: 'Speichern' }
170
+ }
171
+ });
172
+
173
+ t('save'); // "Speichern" (found in de)
174
+ t('cancel'); // "Cancel" (fallback to en)
175
+ t('missing'); // "missing" (key returned)
176
+ ```
177
+
178
+ ## Reactivity
179
+
180
+ The `t()` function reads the internal locale signal, so it integrates with Decantr's reactive system:
181
+
182
+ ```js
183
+ import { createEffect, createMemo } from 'decantr/state';
184
+
185
+ const { t, setLocale } = createI18n({ ... });
186
+
187
+ // Effect re-runs when locale changes
188
+ createEffect(() => {
189
+ document.title = t('page.title');
190
+ });
191
+
192
+ // Memo derives from t()
193
+ const greeting = createMemo(() => t('welcome', { name: userName() }));
194
+
195
+ // Locale change triggers all reactive consumers
196
+ setLocale('fr');
197
+ ```
198
+
199
+ The `locale()` getter is also a signal:
200
+
201
+ ```js
202
+ createEffect(() => {
203
+ console.log('Locale changed to:', locale());
204
+ });
205
+ ```
206
+
207
+ ## RTL Support
208
+
209
+ Use `setDirection()` to set the `dir` attribute on the `<html>` element:
210
+
211
+ ```js
212
+ const { setLocale, setDirection } = createI18n({ ... });
213
+
214
+ function switchToArabic() {
215
+ setLocale('ar');
216
+ setDirection('rtl');
217
+ }
218
+
219
+ function switchToEnglish() {
220
+ setLocale('en');
221
+ setDirection('ltr');
222
+ }
223
+ ```
224
+
225
+ Combine with Decantr's RTL logical property atoms (`_mis`, `_mie`, `_pis`, `_pie`) for layout adaptation:
226
+
227
+ ```js
228
+ import { css } from 'decantr/css';
229
+
230
+ // These atoms use CSS logical properties, so they automatically
231
+ // flip in RTL mode when dir="rtl" is set on <html>
232
+ div({ class: css('_flex _gap4 _pis4 _pie4') }, ...children);
233
+ ```
234
+
235
+ ## Runtime Message Loading
236
+
237
+ Use `addMessages()` to load translations lazily:
238
+
239
+ ```js
240
+ const { t, setLocale, addMessages } = createI18n({
241
+ locale: 'en',
242
+ fallbackLocale: 'en',
243
+ messages: { en: { loading: 'Loading...' } }
244
+ });
245
+
246
+ // Load locale on demand
247
+ async function loadLocale(locale) {
248
+ const res = await fetch(`/locales/${locale}.json`);
249
+ const messages = await res.json();
250
+ addMessages(locale, messages);
251
+ setLocale(locale);
252
+ }
253
+ ```
254
+
255
+ `addMessages()` deep-merges into existing messages, so you can load partial translations:
256
+
257
+ ```js
258
+ // Initial load
259
+ addMessages('en', { common: { save: 'Save' } });
260
+
261
+ // Later: add page-specific translations
262
+ addMessages('en', { dashboard: { title: 'Dashboard' } });
263
+
264
+ // Both work:
265
+ t('common.save'); // "Save"
266
+ t('dashboard.title'); // "Dashboard"
267
+ ```
268
+
269
+ ## Usage with Components
270
+
271
+ ```js
272
+ import { createI18n } from 'decantr/i18n';
273
+ import { Button, Input } from 'decantr/components';
274
+ import { tags } from 'decantr/tags';
275
+ import { text } from 'decantr/core';
276
+
277
+ const { t } = createI18n({
278
+ locale: 'en',
279
+ messages: {
280
+ en: {
281
+ form: {
282
+ email: 'Email address',
283
+ password: 'Password',
284
+ submit: 'Sign In',
285
+ forgot: 'Forgot password?'
286
+ }
287
+ }
288
+ }
289
+ });
290
+
291
+ const { div, h2, a } = tags;
292
+
293
+ function LoginForm() {
294
+ return div(
295
+ h2({}, text(() => t('form.submit'))),
296
+ Input({ placeholder: t('form.email'), type: 'email' }),
297
+ Input({ placeholder: t('form.password'), type: 'password' }),
298
+ Button({}, text(() => t('form.submit'))),
299
+ a({ href: '#' }, text(() => t('form.forgot')))
300
+ );
301
+ }
302
+ ```
303
+
304
+ ## Message File Organization
305
+
306
+ Recommended structure for larger apps:
307
+
308
+ ```
309
+ src/
310
+ locales/
311
+ en/
312
+ common.json # Shared keys (nav, buttons, errors)
313
+ dashboard.json # Dashboard page
314
+ settings.json # Settings page
315
+ fr/
316
+ common.json
317
+ dashboard.json
318
+ settings.json
319
+ ```
320
+
321
+ Load and merge at startup:
322
+
323
+ ```js
324
+ import common from './locales/en/common.json' with { type: 'json' };
325
+ import dashboard from './locales/en/dashboard.json' with { type: 'json' };
326
+
327
+ const { t } = createI18n({
328
+ locale: 'en',
329
+ messages: {
330
+ en: { common, dashboard }
331
+ }
332
+ });
333
+
334
+ t('common.save'); // "Save"
335
+ t('dashboard.title'); // "Dashboard"
336
+ ```