brut 0.0.20 → 0.0.22

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 (728) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +24 -3
  3. data/.nvim.lua +1 -0
  4. data/Dockerfile.dx +12 -3
  5. data/Gemfile.lock +9 -7
  6. data/README.md +0 -7
  7. data/Rakefile +6 -4
  8. data/bin/dev +20 -0
  9. data/bin/docs +27 -0
  10. data/bin/setup +47 -1
  11. data/brut-css/.nvim.lua +1 -0
  12. data/brut-css/README.md +28 -0
  13. data/brut-css/bin/build +31 -0
  14. data/brut-css/bin/dev +1 -0
  15. data/brut-css/bin/docs +15 -0
  16. data/brut-css/bin/setup +5 -0
  17. data/brut-css/config/media-queries-all.css +15 -0
  18. data/brut-css/config/media-queries-minimal.css +5 -0
  19. data/brut-css/config/postcss.config.cjs +7 -0
  20. data/brut-css/config/pseudo-classes-all.css +9 -0
  21. data/brut-css/dx +1 -0
  22. data/brut-css/package-lock.json +3217 -0
  23. data/brut-css/package.json +36 -0
  24. data/brut-css/src/css/appearance.css +145 -0
  25. data/brut-css/src/css/border.css +522 -0
  26. data/brut-css/src/css/colors.css +3502 -0
  27. data/brut-css/src/css/dimensions.css +548 -0
  28. data/brut-css/src/css/flex.css +179 -0
  29. data/brut-css/src/css/index.css +13 -0
  30. data/brut-css/src/css/layout.css +120 -0
  31. data/brut-css/src/css/list.css +41 -0
  32. data/brut-css/src/css/positioning.css +354 -0
  33. data/brut-css/src/css/properties/colors.css +455 -0
  34. data/brut-css/src/css/properties/index.css +3 -0
  35. data/brut-css/src/css/properties/spacing.css +140 -0
  36. data/brut-css/src/css/properties/typography.css +224 -0
  37. data/brut-css/src/css/reset.css +107 -0
  38. data/brut-css/src/css/spacing.css +585 -0
  39. data/brut-css/src/css/typography.css +519 -0
  40. data/brut-css/src/css/utils.css +104 -0
  41. data/brut-css/src/docs/1_getting-started/1_overview.md +46 -0
  42. data/brut-css/src/docs/1_getting-started/2_installation.md +25 -0
  43. data/brut-css/src/docs/1_getting-started/3_core-concepts.md +75 -0
  44. data/brut-css/src/docs/1_getting-started/4_simple-example.md +132 -0
  45. data/brut-css/src/docs/1_getting-started/page.html.ejs +10 -0
  46. data/brut-css/src/docs/2_properties/page.html.ejs +71 -0
  47. data/brut-css/src/docs/3_classes/color-demo.html.ejs +31 -0
  48. data/brut-css/src/docs/3_classes/page.html.ejs +87 -0
  49. data/brut-css/src/docs/4_customization/1_design-system.md +36 -0
  50. data/brut-css/src/docs/4_customization/2_breakpoints.md +75 -0
  51. data/brut-css/src/docs/4_customization/3_pseudo-classes.md +74 -0
  52. data/brut-css/src/docs/4_customization/4_advanced-configuration.md +40 -0
  53. data/brut-css/src/docs/4_customization/page.html.ejs +10 -0
  54. data/brut-css/src/docs/docs.css +98 -0
  55. data/brut-css/src/docs/includes/body-and-header.html.ejs +30 -0
  56. data/brut-css/src/docs/includes/footer-and-rest.html.ejs +9 -0
  57. data/brut-css/src/docs/includes/head.html.ejs +5 -0
  58. data/brut-css/src/docs/includes/nav.html.ejs +10 -0
  59. data/brut-css/src/docs/index.html.ejs +32 -0
  60. data/brut-css/src/docs/prism-twilight.min.css +1 -0
  61. data/brut-css/src/js/Logger.js +71 -0
  62. data/brut-css/src/js/build.js +111 -0
  63. data/brut-css/src/js/cli/CLIArgError.js +7 -0
  64. data/brut-css/src/js/cli/Debug.js +27 -0
  65. data/brut-css/src/js/cli/DocsDir.js +16 -0
  66. data/brut-css/src/js/cli/DocsTemplateSourceDir.js +16 -0
  67. data/brut-css/src/js/cli/InputFile.js +31 -0
  68. data/brut-css/src/js/cli/MediaQueryConfigFile.js +10 -0
  69. data/brut-css/src/js/cli/OutputFile.js +22 -0
  70. data/brut-css/src/js/cli/ParsedArg.js +17 -0
  71. data/brut-css/src/js/cli/PathToBrutCSSRoot.js +19 -0
  72. data/brut-css/src/js/cli/PseudoClassConfigFile.js +11 -0
  73. data/brut-css/src/js/cli.js +108 -0
  74. data/brut-css/src/js/docGenerator.js +467 -0
  75. data/brut-css/src/js/mediaQueryConfigParser.js +98 -0
  76. data/brut-css/src/js/post-css-plugins/addMediaQueriesPlugin.js +49 -0
  77. data/brut-css/src/js/post-css-plugins/addPseudoClassesPlugin.js +42 -0
  78. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Category.js +9 -0
  79. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/DocState.js +185 -0
  80. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Documentable.js +8 -0
  81. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Group.js +7 -0
  82. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/ParsedComment.js +73 -0
  83. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Property.js +9 -0
  84. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/PropertyCategory.js +4 -0
  85. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/PropertyGroup.js +8 -0
  86. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Rule.js +12 -0
  87. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/RuleCategory.js +4 -0
  88. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/RuleGroup.js +8 -0
  89. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/SeeRef.js +5 -0
  90. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/SeeURL.js +9 -0
  91. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin.js +49 -0
  92. data/brut-css/src/js/post-css-plugins/generateRootCustomPropertiesPlugin.js +45 -0
  93. data/brut-css/src/js/pseudoClassConfigParser.js +145 -0
  94. data/brut-js/.projections.json +10 -0
  95. data/brut-js/README.md +118 -0
  96. data/brut-js/bin/build +10 -0
  97. data/brut-js/bin/ci +5 -0
  98. data/brut-js/bin/setup +5 -0
  99. data/brut-js/docs/README.md +8 -0
  100. data/brut-js/docs/jsdoc-plugins/customElementTag.js +8 -0
  101. data/brut-js/docs/jsdoc-theme/publish.js +692 -0
  102. data/brut-js/docs/jsdoc-theme/static/scripts/linenumber.js +25 -0
  103. data/brut-js/docs/jsdoc-theme/static/scripts/prettify/Apache-License-2.0.txt +202 -0
  104. data/brut-js/docs/jsdoc-theme/static/scripts/prettify/lang-css.js +2 -0
  105. data/brut-js/docs/jsdoc-theme/static/scripts/prettify/prettify.js +28 -0
  106. data/brut-js/docs/jsdoc-theme/static/styles/jsdoc-default.css +327 -0
  107. data/brut-js/docs/jsdoc-theme/static/styles/prettify-jsdoc.css +111 -0
  108. data/brut-js/docs/jsdoc-theme/static/styles/prettify-tomorrow.css +132 -0
  109. data/brut-js/docs/jsdoc-theme/tmpl/augments.tmpl +10 -0
  110. data/brut-js/docs/jsdoc-theme/tmpl/container.tmpl +199 -0
  111. data/brut-js/docs/jsdoc-theme/tmpl/details.tmpl +143 -0
  112. data/brut-js/docs/jsdoc-theme/tmpl/example.tmpl +2 -0
  113. data/brut-js/docs/jsdoc-theme/tmpl/examples.tmpl +13 -0
  114. data/brut-js/docs/jsdoc-theme/tmpl/exceptions.tmpl +32 -0
  115. data/brut-js/docs/jsdoc-theme/tmpl/layout.tmpl +38 -0
  116. data/brut-js/docs/jsdoc-theme/tmpl/mainpage.tmpl +14 -0
  117. data/brut-js/docs/jsdoc-theme/tmpl/members.tmpl +38 -0
  118. data/brut-js/docs/jsdoc-theme/tmpl/method.tmpl +131 -0
  119. data/brut-js/docs/jsdoc-theme/tmpl/modifies.tmpl +14 -0
  120. data/brut-js/docs/jsdoc-theme/tmpl/params.tmpl +131 -0
  121. data/brut-js/docs/jsdoc-theme/tmpl/properties.tmpl +108 -0
  122. data/brut-js/docs/jsdoc-theme/tmpl/returns.tmpl +19 -0
  123. data/brut-js/docs/jsdoc-theme/tmpl/source.tmpl +8 -0
  124. data/brut-js/docs/jsdoc-theme/tmpl/tutorial.tmpl +19 -0
  125. data/brut-js/docs/jsdoc-theme/tmpl/type.tmpl +7 -0
  126. data/brut-js/docs/jsdoc.config.json +23 -0
  127. data/brut-js/docs/package-lock.json +343 -0
  128. data/brut-js/docs/package.json +7 -0
  129. data/brut-js/package-lock.json +2171 -0
  130. data/brut-js/package.json +32 -0
  131. data/brut-js/specs/AjaxSubmit.spec.js +256 -0
  132. data/brut-js/specs/Autosubmit.spec.js +127 -0
  133. data/brut-js/specs/ConfirmSubmit.spec.js +193 -0
  134. data/brut-js/specs/ConstraintViolationMessage.spec.js +33 -0
  135. data/brut-js/specs/ConstraintViolationMessages.spec.js +29 -0
  136. data/brut-js/specs/CopyToClipboard.spec.js +35 -0
  137. data/brut-js/specs/Form.spec.js +181 -0
  138. data/brut-js/specs/I18nTranslation.spec.js +19 -0
  139. data/brut-js/specs/LocaleDetection.spec.js +22 -0
  140. data/brut-js/specs/Message.spec.js +15 -0
  141. data/brut-js/specs/SpecHelper.js +23 -0
  142. data/brut-js/specs/Tabs.spec.js +41 -0
  143. data/brut-js/specs/config/asset_metadata.json +7 -0
  144. data/brut-js/src/AjaxSubmit.js +384 -0
  145. data/brut-js/src/Autosubmit.js +63 -0
  146. data/brut-js/src/BaseCustomElement.js +261 -0
  147. data/brut-js/src/ConfirmSubmit.js +116 -0
  148. data/brut-js/src/ConfirmationDialog.js +143 -0
  149. data/brut-js/src/ConstraintViolationMessage.js +125 -0
  150. data/brut-js/src/ConstraintViolationMessages.js +98 -0
  151. data/brut-js/src/CopyToClipboard.js +96 -0
  152. data/brut-js/src/Form.js +151 -0
  153. data/brut-js/src/I18nTranslation.js +61 -0
  154. data/brut-js/src/LocaleDetection.js +117 -0
  155. data/brut-js/src/Logger.js +90 -0
  156. data/brut-js/src/Message.js +56 -0
  157. data/brut-js/src/RichString.js +113 -0
  158. data/brut-js/src/Tabs.js +168 -0
  159. data/brut-js/src/Tracing.js +247 -0
  160. data/brut-js/src/appForTestingOnly.js +15 -0
  161. data/brut-js/src/index.js +130 -0
  162. data/brut-js/src/testing/AssetMetadata.js +35 -0
  163. data/brut-js/src/testing/AssetMetadataLoader.js +25 -0
  164. data/brut-js/src/testing/CustomElementTest.js +235 -0
  165. data/brut-js/src/testing/DOMCreator.js +45 -0
  166. data/brut-js/src/testing/index.js +48 -0
  167. data/brutrb.com/.vitepress/config.mjs +106 -0
  168. data/brutrb.com/.vitepress/plugins/jsdocLinker.js +34 -0
  169. data/brutrb.com/.vitepress/plugins/rdocLinker.js +18 -0
  170. data/brutrb.com/.vitepress/theme/custom.css +7 -0
  171. data/brutrb.com/.vitepress/theme/index.js +18 -0
  172. data/brutrb.com/.vitepress/theme/style.css +149 -0
  173. data/brutrb.com/ai.md +68 -0
  174. data/brutrb.com/assets.md +138 -0
  175. data/brutrb.com/bin/build +5 -0
  176. data/brutrb.com/bin/deploy +7 -0
  177. data/brutrb.com/bin/dev +5 -0
  178. data/brutrb.com/bin/setup +5 -0
  179. data/brutrb.com/brut-js.md +117 -0
  180. data/brutrb.com/business-logic.md +55 -0
  181. data/brutrb.com/cli.md +278 -0
  182. data/brutrb.com/components.md +243 -0
  183. data/brutrb.com/configuration.md +257 -0
  184. data/brutrb.com/css.md +103 -0
  185. data/brutrb.com/custom-element-tests.md +149 -0
  186. data/brutrb.com/database-access.md +201 -0
  187. data/brutrb.com/database-schema.md +312 -0
  188. data/brutrb.com/deployment.md +66 -0
  189. data/brutrb.com/dev-environment.md +179 -0
  190. data/brutrb.com/doc-conventions.md +39 -0
  191. data/brutrb.com/end-to-end-tests.md +174 -0
  192. data/brutrb.com/flash-and-session.md +224 -0
  193. data/brutrb.com/forms.md +866 -0
  194. data/brutrb.com/getting-started.md +66 -0
  195. data/brutrb.com/handlers.md +153 -0
  196. data/brutrb.com/hooks.md +178 -0
  197. data/brutrb.com/i18n.md +188 -0
  198. data/brutrb.com/images/Makefile +10 -0
  199. data/brutrb.com/images/dev-env-overview.dot +54 -0
  200. data/brutrb.com/images/dev-env-overview.png +0 -0
  201. data/brutrb.com/images/dev-env-protocol.dot +37 -0
  202. data/brutrb.com/images/dev-env-protocol.png +0 -0
  203. data/brutrb.com/images/logo-300.png +0 -0
  204. data/brutrb.com/images/logo.png +0 -0
  205. data/brutrb.com/images/overview.graffle +0 -0
  206. data/brutrb.com/images/overview.png +0 -0
  207. data/brutrb.com/images/spa.dot +19 -0
  208. data/brutrb.com/images/spa.png +0 -0
  209. data/brutrb.com/images/workspace-protocol.dot +44 -0
  210. data/brutrb.com/images/workspace-protocol.png +0 -0
  211. data/brutrb.com/index.md +36 -0
  212. data/brutrb.com/instrumentation.md +183 -0
  213. data/brutrb.com/javascript.md +122 -0
  214. data/brutrb.com/jobs.md +14 -0
  215. data/{doc-src → brutrb.com}/keyword-injection.md +122 -68
  216. data/brutrb.com/markdown-examples.md +85 -0
  217. data/brutrb.com/middleware.md +80 -0
  218. data/brutrb.com/not-released.md +5 -0
  219. data/brutrb.com/overview.md +404 -0
  220. data/brutrb.com/package-lock.json +2404 -0
  221. data/brutrb.com/package.json +11 -0
  222. data/brutrb.com/pages.md +378 -0
  223. data/brutrb.com/public/images/logo-300.png +0 -0
  224. data/brutrb.com/public/images/logo.png +0 -0
  225. data/brutrb.com/routes.md +215 -0
  226. data/brutrb.com/security.md +105 -0
  227. data/brutrb.com/seed-data.md +63 -0
  228. data/brutrb.com/space-time-continuum.md +85 -0
  229. data/brutrb.com/tutorial.md +3 -0
  230. data/brutrb.com/unit-tests.md +148 -0
  231. data/docker-compose.dx.yml +6 -3
  232. data/docs/404.html +21 -0
  233. data/docs/CNAME +1 -0
  234. data/docs/ai.html +24 -0
  235. data/docs/api/Brut/BackEnd/SeedData.html +493 -0
  236. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +214 -0
  237. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +125 -0
  238. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +125 -0
  239. data/docs/api/Brut/BackEnd/Sidekiq.html +125 -0
  240. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +414 -0
  241. data/docs/api/Brut/BackEnd/Validators.html +128 -0
  242. data/docs/api/Brut/BackEnd.html +132 -0
  243. data/docs/api/Brut/CLI/App.html +1576 -0
  244. data/docs/api/Brut/CLI/AppRunner.html +491 -0
  245. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +264 -0
  246. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +306 -0
  247. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +262 -0
  248. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +314 -0
  249. data/docs/api/Brut/CLI/Apps/BuildAssets.html +183 -0
  250. data/docs/api/Brut/CLI/Apps/DB/Create.html +365 -0
  251. data/docs/api/Brut/CLI/Apps/DB/Drop.html +357 -0
  252. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +383 -0
  253. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +335 -0
  254. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +329 -0
  255. data/docs/api/Brut/CLI/Apps/DB/Seed.html +347 -0
  256. data/docs/api/Brut/CLI/Apps/DB/Status.html +383 -0
  257. data/docs/api/Brut/CLI/Apps/DB.html +183 -0
  258. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +303 -0
  259. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +512 -0
  260. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +398 -0
  261. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +374 -0
  262. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +410 -0
  263. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +262 -0
  264. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +303 -0
  265. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +480 -0
  266. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +450 -0
  267. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +380 -0
  268. data/docs/api/Brut/CLI/Apps/Scaffold.html +253 -0
  269. data/docs/api/Brut/CLI/Apps/Test/Audit.html +464 -0
  270. data/docs/api/Brut/CLI/Apps/Test/E2e.html +407 -0
  271. data/docs/api/Brut/CLI/Apps/Test/JS.html +262 -0
  272. data/docs/api/Brut/CLI/Apps/Test/Run.html +578 -0
  273. data/docs/api/Brut/CLI/Apps/Test.html +253 -0
  274. data/docs/api/Brut/CLI/Apps.html +125 -0
  275. data/docs/api/Brut/CLI/Command.html +2342 -0
  276. data/docs/api/Brut/CLI/Error.html +139 -0
  277. data/docs/api/Brut/CLI/ExecutionResults/Result.html +664 -0
  278. data/docs/api/Brut/CLI/ExecutionResults.html +675 -0
  279. data/docs/api/Brut/CLI/Executor.html +430 -0
  280. data/docs/api/Brut/CLI/InvalidOption.html +245 -0
  281. data/docs/api/Brut/CLI/Options.html +753 -0
  282. data/docs/api/Brut/CLI/Output.html +699 -0
  283. data/docs/api/Brut/CLI/SystemExecError.html +451 -0
  284. data/docs/api/Brut/CLI.html +263 -0
  285. data/docs/api/Brut/FactoryBot.html +225 -0
  286. data/docs/api/Brut/Framework/App.html +1097 -0
  287. data/docs/api/Brut/Framework/Config.html +1045 -0
  288. data/docs/api/Brut/Framework/Container.html +1379 -0
  289. data/docs/api/Brut/Framework/Error.html +140 -0
  290. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +144 -0
  291. data/docs/api/Brut/Framework/Errors/Bug.html +234 -0
  292. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +257 -0
  293. data/docs/api/Brut/Framework/Errors/MissingParameter.html +273 -0
  294. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +471 -0
  295. data/docs/api/Brut/Framework/Errors/NotFound.html +308 -0
  296. data/docs/api/Brut/Framework/Errors/NotImplemented.html +234 -0
  297. data/docs/api/Brut/Framework/Errors.html +328 -0
  298. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +392 -0
  299. data/docs/api/Brut/Framework/MCP.html +861 -0
  300. data/docs/api/Brut/Framework/ProjectEnvironment.html +648 -0
  301. data/docs/api/Brut/Framework.html +129 -0
  302. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +317 -0
  303. data/docs/api/Brut/FrontEnd/Component/Helpers.html +326 -0
  304. data/docs/api/Brut/FrontEnd/Component.html +365 -0
  305. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +470 -0
  306. data/docs/api/Brut/FrontEnd/Components/FormTag.html +518 -0
  307. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +317 -0
  308. data/docs/api/Brut/FrontEnd/Components/Input.html +195 -0
  309. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +339 -0
  310. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +660 -0
  311. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +417 -0
  312. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +918 -0
  313. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +651 -0
  314. data/docs/api/Brut/FrontEnd/Components/Inputs.html +125 -0
  315. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +367 -0
  316. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +336 -0
  317. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +655 -0
  318. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +352 -0
  319. data/docs/api/Brut/FrontEnd/Components.html +135 -0
  320. data/docs/api/Brut/FrontEnd/Download.html +467 -0
  321. data/docs/api/Brut/FrontEnd/Flash.html +1150 -0
  322. data/docs/api/Brut/FrontEnd/Form.html +1157 -0
  323. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +634 -0
  324. data/docs/api/Brut/FrontEnd/Forms/Input.html +615 -0
  325. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +547 -0
  326. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1318 -0
  327. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +609 -0
  328. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +587 -0
  329. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +613 -0
  330. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +582 -0
  331. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +609 -0
  332. data/docs/api/Brut/FrontEnd/Forms.html +127 -0
  333. data/docs/api/Brut/FrontEnd/GenericResponse.html +377 -0
  334. data/docs/api/Brut/FrontEnd/Handler.html +442 -0
  335. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +318 -0
  336. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +336 -0
  337. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +399 -0
  338. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +354 -0
  339. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +151 -0
  340. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +315 -0
  341. data/docs/api/Brut/FrontEnd/Handlers.html +125 -0
  342. data/docs/api/Brut/FrontEnd/HandlingResults.html +339 -0
  343. data/docs/api/Brut/FrontEnd/HttpMethod.html +661 -0
  344. data/docs/api/Brut/FrontEnd/HttpStatus.html +496 -0
  345. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +284 -0
  346. data/docs/api/Brut/FrontEnd/Layout.html +318 -0
  347. data/docs/api/Brut/FrontEnd/Middleware.html +135 -0
  348. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +288 -0
  349. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +292 -0
  350. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +324 -0
  351. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +372 -0
  352. data/docs/api/Brut/FrontEnd/Middlewares.html +125 -0
  353. data/docs/api/Brut/FrontEnd/Page.html +773 -0
  354. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +797 -0
  355. data/docs/api/Brut/FrontEnd/Pages.html +125 -0
  356. data/docs/api/Brut/FrontEnd/RequestContext.html +1312 -0
  357. data/docs/api/Brut/FrontEnd/RouteHook.html +424 -0
  358. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +242 -0
  359. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +249 -0
  360. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +264 -0
  361. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +261 -0
  362. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +284 -0
  363. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +252 -0
  364. data/docs/api/Brut/FrontEnd/RouteHooks.html +115 -0
  365. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +227 -0
  366. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +305 -0
  367. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +324 -0
  368. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +319 -0
  369. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +315 -0
  370. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +315 -0
  371. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +327 -0
  372. data/docs/api/Brut/FrontEnd/Routing/Route.html +761 -0
  373. data/docs/api/Brut/FrontEnd/Routing.html +927 -0
  374. data/docs/api/Brut/FrontEnd/Session.html +1195 -0
  375. data/docs/api/Brut/FrontEnd.html +134 -0
  376. data/docs/api/Brut/I18n/BaseMethods.html +931 -0
  377. data/docs/api/Brut/I18n/ForBackEnd.html +302 -0
  378. data/docs/api/Brut/I18n/ForCLI.html +302 -0
  379. data/docs/api/Brut/I18n/ForHTML.html +296 -0
  380. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +316 -0
  381. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +930 -0
  382. data/docs/api/Brut/I18n.html +127 -0
  383. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +435 -0
  384. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +286 -0
  385. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +302 -0
  386. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +864 -0
  387. data/docs/api/Brut/Instrumentation.html +126 -0
  388. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +532 -0
  389. data/docs/api/Brut/SinatraHelpers.html +281 -0
  390. data/docs/api/Brut/SpecSupport/ClockSupport.html +383 -0
  391. data/docs/api/Brut/SpecSupport/ComponentSupport.html +502 -0
  392. data/docs/api/Brut/SpecSupport/E2ETestServer.html +503 -0
  393. data/docs/api/Brut/SpecSupport/E2eSupport.html +142 -0
  394. data/docs/api/Brut/SpecSupport/EnhancedNode.html +403 -0
  395. data/docs/api/Brut/SpecSupport/FlashSupport.html +278 -0
  396. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +401 -0
  397. data/docs/api/Brut/SpecSupport/GeneralSupport.html +195 -0
  398. data/docs/api/Brut/SpecSupport/HandlerSupport.html +160 -0
  399. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +553 -0
  400. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +439 -0
  401. data/docs/api/Brut/SpecSupport/Matchers.html +125 -0
  402. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +335 -0
  403. data/docs/api/Brut/SpecSupport/RSpecSetup.html +602 -0
  404. data/docs/api/Brut/SpecSupport/SessionSupport.html +196 -0
  405. data/docs/api/Brut/SpecSupport.html +129 -0
  406. data/docs/api/Brut.html +225 -0
  407. data/docs/api/Clock.html +603 -0
  408. data/docs/api/RichString.html +968 -0
  409. data/docs/api/SemanticLogger/Appender/Async.html +219 -0
  410. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +115 -0
  411. data/docs/api/Sequel/Extensions/BrutMigrations.html +533 -0
  412. data/docs/api/Sequel/Extensions.html +117 -0
  413. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +105 -0
  414. data/docs/api/Sequel/Plugins/CreatedAt.html +125 -0
  415. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +207 -0
  416. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +186 -0
  417. data/docs/api/Sequel/Plugins/ExternalId.html +218 -0
  418. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +202 -0
  419. data/docs/api/Sequel/Plugins/FindBang.html +125 -0
  420. data/docs/api/Sequel/Plugins.html +117 -0
  421. data/docs/api/Sequel.html +117 -0
  422. data/docs/api/_index.html +1553 -0
  423. data/docs/api/class_list.html +54 -0
  424. data/docs/api/css/common.css +1 -0
  425. data/docs/api/css/full_list.css +58 -0
  426. data/docs/api/css/style.css +503 -0
  427. data/docs/api/file.README.html +127 -0
  428. data/docs/api/file_list.html +59 -0
  429. data/docs/api/frames.html +22 -0
  430. data/docs/api/index.html +127 -0
  431. data/docs/api/js/app.js +344 -0
  432. data/docs/api/js/full_list.js +242 -0
  433. data/docs/api/js/jquery.js +4 -0
  434. data/docs/api/method_list.html +3998 -0
  435. data/docs/api/top-level-namespace.html +112 -0
  436. data/docs/assets/ai.md.tZrjP9im.js +1 -0
  437. data/docs/assets/ai.md.tZrjP9im.lean.js +1 -0
  438. data/docs/assets/app.D_yaTITQ.js +1 -0
  439. data/docs/assets/assets.md.D3wunzLx.js +19 -0
  440. data/docs/assets/assets.md.D3wunzLx.lean.js +1 -0
  441. data/docs/assets/brut-js.md.o2DAO2s2.js +12 -0
  442. data/docs/assets/brut-js.md.o2DAO2s2.lean.js +1 -0
  443. data/docs/assets/business-logic.md.BY4hGy0m.js +1 -0
  444. data/docs/assets/business-logic.md.BY4hGy0m.lean.js +1 -0
  445. data/docs/assets/chunks/@localSearchIndexroot.BsN5i0Fi.js +1 -0
  446. data/docs/assets/chunks/VPLocalSearchBox.B2-ZzyTY.js +8 -0
  447. data/docs/assets/chunks/framework.1L-BeKqY.js +18 -0
  448. data/docs/assets/chunks/theme.CfGFVRvE.js +2 -0
  449. data/docs/assets/cli.md.RmeA2b0i.js +127 -0
  450. data/docs/assets/cli.md.RmeA2b0i.lean.js +1 -0
  451. data/docs/assets/components.md.eCttGlN-.js +104 -0
  452. data/docs/assets/components.md.eCttGlN-.lean.js +1 -0
  453. data/docs/assets/configuration.md.BRriU0cL.js +78 -0
  454. data/docs/assets/configuration.md.BRriU0cL.lean.js +1 -0
  455. data/docs/assets/css.md.DJgj2clw.js +21 -0
  456. data/docs/assets/css.md.DJgj2clw.lean.js +1 -0
  457. data/docs/assets/custom-element-tests.md.BrYJQEl3.js +69 -0
  458. data/docs/assets/custom-element-tests.md.BrYJQEl3.lean.js +1 -0
  459. data/docs/assets/database-access.md.C7l-Vuvb.js +63 -0
  460. data/docs/assets/database-access.md.C7l-Vuvb.lean.js +1 -0
  461. data/docs/assets/database-schema.md.BUjR0VS1.js +63 -0
  462. data/docs/assets/database-schema.md.BUjR0VS1.lean.js +1 -0
  463. data/docs/assets/deployment.md.Dbka4OTr.js +1 -0
  464. data/docs/assets/deployment.md.Dbka4OTr.lean.js +1 -0
  465. data/docs/assets/dev-env-overview.Gj7NWM8-.png +0 -0
  466. data/docs/assets/dev-env-protocol.DysDAtnz.png +0 -0
  467. data/docs/assets/dev-environment.md.BNc8AYiK.js +11 -0
  468. data/docs/assets/dev-environment.md.BNc8AYiK.lean.js +1 -0
  469. data/docs/assets/doc-conventions.md.DCfRXXi-.js +1 -0
  470. data/docs/assets/doc-conventions.md.DCfRXXi-.lean.js +1 -0
  471. data/docs/assets/end-to-end-tests.md.yfQHC0b5.js +26 -0
  472. data/docs/assets/end-to-end-tests.md.yfQHC0b5.lean.js +1 -0
  473. data/docs/assets/flash-and-session.md.BXY8RvT0.js +93 -0
  474. data/docs/assets/flash-and-session.md.BXY8RvT0.lean.js +1 -0
  475. data/docs/assets/forms.md.CBTYQ_Cz.js +379 -0
  476. data/docs/assets/forms.md.CBTYQ_Cz.lean.js +1 -0
  477. data/docs/assets/getting-started.md.Bz2s1Vjb.js +2 -0
  478. data/docs/assets/getting-started.md.Bz2s1Vjb.lean.js +1 -0
  479. data/docs/assets/handlers.md.089DVD3v.js +69 -0
  480. data/docs/assets/handlers.md.089DVD3v.lean.js +1 -0
  481. data/docs/assets/hooks.md.C4-moMny.js +80 -0
  482. data/docs/assets/hooks.md.C4-moMny.lean.js +1 -0
  483. data/docs/assets/i18n.md.Do9i1qWl.js +23 -0
  484. data/docs/assets/i18n.md.Do9i1qWl.lean.js +1 -0
  485. data/docs/assets/index.md.B28EwVpq.js +1 -0
  486. data/docs/assets/index.md.B28EwVpq.lean.js +1 -0
  487. data/docs/assets/instrumentation.md.CL6ax7nT.js +35 -0
  488. data/docs/assets/instrumentation.md.CL6ax7nT.lean.js +1 -0
  489. data/docs/assets/javascript.md.GWbhRS51.js +31 -0
  490. data/docs/assets/javascript.md.GWbhRS51.lean.js +1 -0
  491. data/docs/assets/jobs.md.S-2amAYp.js +1 -0
  492. data/docs/assets/jobs.md.S-2amAYp.lean.js +1 -0
  493. data/docs/assets/keyword-injection.md.Dt2tKREs.js +25 -0
  494. data/docs/assets/keyword-injection.md.Dt2tKREs.lean.js +1 -0
  495. data/docs/assets/markdown-examples.md.CCFEQO44.js +33 -0
  496. data/docs/assets/markdown-examples.md.CCFEQO44.lean.js +1 -0
  497. data/docs/assets/middleware.md.Czz_UlJN.js +20 -0
  498. data/docs/assets/middleware.md.Czz_UlJN.lean.js +1 -0
  499. data/docs/assets/not-released.md.BBy28McC.js +1 -0
  500. data/docs/assets/not-released.md.BBy28McC.lean.js +1 -0
  501. data/docs/assets/overview.Da81cB9R.png +0 -0
  502. data/docs/assets/overview.md.CDalkuxV.js +133 -0
  503. data/docs/assets/overview.md.CDalkuxV.lean.js +1 -0
  504. data/docs/assets/pages.md.BE3kfOc5.js +122 -0
  505. data/docs/assets/pages.md.BE3kfOc5.lean.js +1 -0
  506. data/docs/assets/routes.md.BMM7peut.js +29 -0
  507. data/docs/assets/routes.md.BMM7peut.lean.js +1 -0
  508. data/docs/assets/security.md.C668yXCi.js +1 -0
  509. data/docs/assets/security.md.C668yXCi.lean.js +1 -0
  510. data/docs/assets/seed-data.md.BvFZlqIk.js +14 -0
  511. data/docs/assets/seed-data.md.BvFZlqIk.lean.js +1 -0
  512. data/docs/assets/spa.qejUdp-5.png +0 -0
  513. data/docs/assets/space-time-continuum.md.KPUIKysQ.js +1 -0
  514. data/docs/assets/space-time-continuum.md.KPUIKysQ.lean.js +1 -0
  515. data/docs/assets/style.D73IYGCX.css +1 -0
  516. data/docs/assets/tutorial.md.BnoGjrdK.js +1 -0
  517. data/docs/assets/tutorial.md.BnoGjrdK.lean.js +1 -0
  518. data/docs/assets/unit-tests.md.DUGrnLj5.js +13 -0
  519. data/docs/assets/unit-tests.md.DUGrnLj5.lean.js +1 -0
  520. data/docs/assets/workspace-protocol.C0gXsoDb.png +0 -0
  521. data/docs/assets.html +42 -0
  522. data/docs/brut-css/brut.css +1 -0
  523. data/docs/brut-css/brut.max.css +22372 -0
  524. data/docs/brut-css/classes/appearances.html +783 -0
  525. data/docs/brut-css/classes/background-colors.html +3529 -0
  526. data/docs/brut-css/classes/border-colors.html +3529 -0
  527. data/docs/brut-css/classes/borders.html +2293 -0
  528. data/docs/brut-css/classes/dimensions.html +2581 -0
  529. data/docs/brut-css/classes/flex.html +917 -0
  530. data/docs/brut-css/classes/foreground-colors.html +3261 -0
  531. data/docs/brut-css/classes/junk-drawer.html +431 -0
  532. data/docs/brut-css/classes/layout.html +668 -0
  533. data/docs/brut-css/classes/lists.html +331 -0
  534. data/docs/brut-css/classes/positioning.html +1751 -0
  535. data/docs/brut-css/classes/spacings.html +2633 -0
  536. data/docs/brut-css/classes/typography.html +2206 -0
  537. data/docs/brut-css/customization/advanced-configuration.html +204 -0
  538. data/docs/brut-css/customization/breakpoints.html +227 -0
  539. data/docs/brut-css/customization/design-system.html +197 -0
  540. data/docs/brut-css/customization/pseudo-classes.html +228 -0
  541. data/docs/brut-css/docs.css +98 -0
  542. data/docs/brut-css/getting-started/core-concepts.html +234 -0
  543. data/docs/brut-css/getting-started/installation.html +190 -0
  544. data/docs/brut-css/getting-started/overview.html +210 -0
  545. data/docs/brut-css/getting-started/simple-example.html +285 -0
  546. data/docs/brut-css/index.html +193 -0
  547. data/docs/brut-css/prism-twilight.min.css +1 -0
  548. data/docs/brut-css/properties/colors.html +1548 -0
  549. data/docs/brut-css/properties/spacings.html +614 -0
  550. data/docs/brut-css/properties/typography.html +777 -0
  551. data/docs/brut-js/api/AjaxSubmit.html +374 -0
  552. data/docs/brut-js/api/AjaxSubmit.js.html +435 -0
  553. data/docs/brut-js/api/Autosubmit.html +192 -0
  554. data/docs/brut-js/api/Autosubmit.js.html +114 -0
  555. data/docs/brut-js/api/BaseCustomElement.html +1091 -0
  556. data/docs/brut-js/api/BaseCustomElement.js.html +312 -0
  557. data/docs/brut-js/api/BrutCustomElements.html +172 -0
  558. data/docs/brut-js/api/BufferedLogger.html +173 -0
  559. data/docs/brut-js/api/ConfirmSubmit.html +278 -0
  560. data/docs/brut-js/api/ConfirmSubmit.js.html +167 -0
  561. data/docs/brut-js/api/ConfirmationDialog.html +425 -0
  562. data/docs/brut-js/api/ConfirmationDialog.js.html +194 -0
  563. data/docs/brut-js/api/ConstraintViolationMessage.html +448 -0
  564. data/docs/brut-js/api/ConstraintViolationMessage.js.html +176 -0
  565. data/docs/brut-js/api/ConstraintViolationMessages.html +590 -0
  566. data/docs/brut-js/api/ConstraintViolationMessages.js.html +149 -0
  567. data/docs/brut-js/api/CopyToClipboard.html +345 -0
  568. data/docs/brut-js/api/CopyToClipboard.js.html +147 -0
  569. data/docs/brut-js/api/Form.html +294 -0
  570. data/docs/brut-js/api/Form.js.html +202 -0
  571. data/docs/brut-js/api/I18nTranslation.html +409 -0
  572. data/docs/brut-js/api/I18nTranslation.js.html +112 -0
  573. data/docs/brut-js/api/LocaleDetection.html +312 -0
  574. data/docs/brut-js/api/LocaleDetection.js.html +168 -0
  575. data/docs/brut-js/api/Logger.html +702 -0
  576. data/docs/brut-js/api/Logger.js.html +141 -0
  577. data/docs/brut-js/api/Message.html +238 -0
  578. data/docs/brut-js/api/Message.js.html +107 -0
  579. data/docs/brut-js/api/PrefixedLogger.html +369 -0
  580. data/docs/brut-js/api/RichString.html +1049 -0
  581. data/docs/brut-js/api/RichString.js.html +164 -0
  582. data/docs/brut-js/api/Tabs.html +295 -0
  583. data/docs/brut-js/api/Tabs.js.html +219 -0
  584. data/docs/brut-js/api/Tracing.html +277 -0
  585. data/docs/brut-js/api/Tracing.js.html +298 -0
  586. data/docs/brut-js/api/external-CustomElementRegistry.html +140 -0
  587. data/docs/brut-js/api/external-Performance.html +138 -0
  588. data/docs/brut-js/api/external-Promise.html +138 -0
  589. data/docs/brut-js/api/external-ValidityState.html +138 -0
  590. data/docs/brut-js/api/external-Window.html +233 -0
  591. data/docs/brut-js/api/external-fetch.html +138 -0
  592. data/docs/brut-js/api/global.html +400 -0
  593. data/docs/brut-js/api/index.html +168 -0
  594. data/docs/brut-js/api/index.js.html +181 -0
  595. data/docs/brut-js/api/module-testing.html +383 -0
  596. data/docs/brut-js/api/scripts/linenumber.js +25 -0
  597. data/docs/brut-js/api/scripts/prettify/Apache-License-2.0.txt +202 -0
  598. data/docs/brut-js/api/scripts/prettify/lang-css.js +2 -0
  599. data/docs/brut-js/api/scripts/prettify/prettify.js +28 -0
  600. data/docs/brut-js/api/styles/jsdoc-default.css +327 -0
  601. data/docs/brut-js/api/styles/prettify-jsdoc.css +111 -0
  602. data/docs/brut-js/api/styles/prettify-tomorrow.css +132 -0
  603. data/docs/brut-js/api/testing.AssetMetadata.html +172 -0
  604. data/docs/brut-js/api/testing.AssetMetadataLoader.html +171 -0
  605. data/docs/brut-js/api/testing.CustomElementTest.html +679 -0
  606. data/docs/brut-js/api/testing.DOMCreator.html +171 -0
  607. data/docs/brut-js/api/testing_AssetMetadata.js.html +86 -0
  608. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +76 -0
  609. data/docs/brut-js/api/testing_CustomElementTest.js.html +286 -0
  610. data/docs/brut-js/api/testing_DOMCreator.js.html +96 -0
  611. data/docs/brut-js/api/testing_index.js.html +99 -0
  612. data/docs/brut-js.html +35 -0
  613. data/docs/business-logic.html +24 -0
  614. data/docs/cli.html +150 -0
  615. data/docs/components.html +127 -0
  616. data/docs/configuration.html +101 -0
  617. data/docs/css.html +44 -0
  618. data/docs/custom-element-tests.html +92 -0
  619. data/docs/database-access.html +86 -0
  620. data/docs/database-schema.html +86 -0
  621. data/docs/deployment.html +24 -0
  622. data/docs/dev-environment.html +34 -0
  623. data/docs/doc-conventions.html +24 -0
  624. data/docs/end-to-end-tests.html +49 -0
  625. data/docs/flash-and-session.html +116 -0
  626. data/docs/forms.html +402 -0
  627. data/docs/getting-started.html +25 -0
  628. data/docs/handlers.html +92 -0
  629. data/docs/hashmap.json +1 -0
  630. data/docs/hooks.html +103 -0
  631. data/docs/i18n.html +46 -0
  632. data/docs/images/logo-300.png +0 -0
  633. data/docs/images/logo.png +0 -0
  634. data/docs/index.html +24 -0
  635. data/docs/instrumentation.html +58 -0
  636. data/docs/javascript.html +54 -0
  637. data/docs/jobs.html +24 -0
  638. data/docs/keyword-injection.html +48 -0
  639. data/docs/markdown-examples.html +56 -0
  640. data/docs/middleware.html +43 -0
  641. data/docs/not-released.html +24 -0
  642. data/docs/overview.html +156 -0
  643. data/docs/pages.html +145 -0
  644. data/docs/routes.html +52 -0
  645. data/docs/security.html +24 -0
  646. data/docs/seed-data.html +37 -0
  647. data/docs/space-time-continuum.html +24 -0
  648. data/docs/tutorial.html +24 -0
  649. data/docs/unit-tests.html +36 -0
  650. data/docs/vp-icons.css +1 -0
  651. data/lib/brut/back_end/seed_data.rb +19 -2
  652. data/lib/brut/back_end/sidekiq/middlewares/server.rb +2 -1
  653. data/lib/brut/back_end/sidekiq/middlewares.rb +2 -1
  654. data/lib/brut/back_end/sidekiq.rb +2 -1
  655. data/lib/brut/back_end/validator.rb +5 -1
  656. data/lib/brut/back_end.rb +4 -2
  657. data/lib/brut/cli/app_runner.rb +1 -1
  658. data/lib/brut/cli/apps/test.rb +5 -0
  659. data/lib/brut/cli.rb +4 -3
  660. data/lib/brut/factory_bot.rb +0 -5
  661. data/lib/brut/framework/app.rb +70 -5
  662. data/lib/brut/framework/config.rb +5 -3
  663. data/lib/brut/framework/container.rb +3 -2
  664. data/lib/brut/framework/errors.rb +12 -4
  665. data/lib/brut/framework/mcp.rb +58 -1
  666. data/lib/brut/framework/project_environment.rb +6 -2
  667. data/lib/brut/framework.rb +1 -1
  668. data/lib/brut/front_end/component.rb +69 -71
  669. data/lib/brut/front_end/components/constraint_violations.rb +1 -4
  670. data/lib/brut/front_end/components/form_tag.rb +1 -1
  671. data/lib/brut/front_end/components/input.rb +3 -3
  672. data/lib/brut/front_end/components/inputs/csrf_token.rb +1 -1
  673. data/lib/brut/front_end/components/inputs/{text_field.rb → input_tag.rb} +7 -9
  674. data/lib/brut/front_end/components/inputs/radio_button.rb +1 -1
  675. data/lib/brut/front_end/components/inputs/select_tag_with_options.rb +187 -0
  676. data/lib/brut/front_end/components/inputs/{textarea.rb → textarea_tag.rb} +2 -2
  677. data/lib/brut/front_end/components/time_tag.rb +2 -1
  678. data/lib/brut/front_end/form.rb +4 -4
  679. data/lib/brut/front_end/forms/input.rb +2 -1
  680. data/lib/brut/front_end/forms/input_definition.rb +5 -2
  681. data/lib/brut/front_end/forms/radio_button_group_input.rb +2 -1
  682. data/lib/brut/front_end/forms/radio_button_group_input_definition.rb +2 -2
  683. data/lib/brut/front_end/forms/select_input.rb +2 -4
  684. data/lib/brut/front_end/forms/select_input_definition.rb +2 -2
  685. data/lib/brut/front_end/handler.rb +28 -26
  686. data/lib/brut/front_end/handlers/csp_reporting_handler.rb +5 -2
  687. data/lib/brut/front_end/handlers/instrumentation_handler.rb +8 -4
  688. data/lib/brut/front_end/handlers/locale_detection_handler.rb +9 -5
  689. data/lib/brut/front_end/handlers/missing_handler.rb +5 -2
  690. data/lib/brut/front_end/layout.rb +16 -0
  691. data/lib/brut/front_end/page.rb +52 -29
  692. data/lib/brut/front_end/request_context.rb +3 -2
  693. data/lib/brut/front_end/routing.rb +5 -1
  694. data/lib/brut/front_end.rb +4 -13
  695. data/lib/brut/i18n/base_methods.rb +167 -79
  696. data/lib/brut/i18n/for_back_end.rb +4 -0
  697. data/lib/brut/i18n/for_cli.rb +4 -0
  698. data/lib/brut/i18n/for_html.rb +32 -4
  699. data/lib/brut/i18n/http_accept_language.rb +47 -0
  700. data/lib/brut/instrumentation/open_telemetry.rb +36 -1
  701. data/lib/brut/instrumentation.rb +3 -5
  702. data/lib/brut/sinatra_helpers.rb +11 -3
  703. data/lib/brut/spec_support/component_support.rb +30 -16
  704. data/lib/brut/spec_support/e2e_support.rb +1 -1
  705. data/lib/brut/spec_support/e2e_test_server.rb +3 -0
  706. data/lib/brut/spec_support/general_support.rb +3 -0
  707. data/lib/brut/spec_support/handler_support.rb +6 -1
  708. data/lib/brut/spec_support/matcher.rb +1 -0
  709. data/lib/brut/spec_support/matchers/be_page_for.rb +1 -0
  710. data/lib/brut/spec_support/matchers/have_html_attribute.rb +1 -0
  711. data/lib/brut/spec_support/matchers/have_i18n_string.rb +2 -5
  712. data/lib/brut/spec_support/matchers/have_link_to.rb +1 -0
  713. data/lib/brut/spec_support/matchers/have_redirected_to.rb +1 -0
  714. data/lib/brut/spec_support/matchers/have_rendered.rb +1 -0
  715. data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +44 -0
  716. data/lib/brut/spec_support.rb +1 -1
  717. data/lib/brut/version.rb +1 -1
  718. data/lib/brut.rb +5 -4
  719. data/lib/sequel/extensions/brut_migrations.rb +1 -1
  720. metadata +648 -13
  721. data/doc-src/architecture.md +0 -102
  722. data/doc-src/assets.md +0 -98
  723. data/doc-src/forms.md +0 -214
  724. data/doc-src/handlers.md +0 -83
  725. data/doc-src/javascript.md +0 -265
  726. data/doc-src/pages.md +0 -210
  727. data/doc-src/route-hooks.md +0 -59
  728. data/lib/brut/front_end/components/inputs/select.rb +0 -117
@@ -0,0 +1,866 @@
1
+ # Forms
2
+
3
+ The most common way for a web site visitor to submit data to the server is to submit a form. The Web Platform's forms API is much like an uncle you may have: old and rich.
4
+
5
+ Brut's forms module solves three problems:
6
+
7
+ * Descrbing the data being collected and submitted
8
+ * Providing access to the submitted form data on the server
9
+ * Support for creating a good client-side experience regarding constraint violations, both client-side and
10
+ server-side.
11
+
12
+ ## Overview
13
+
14
+ The forms module has a lot of moving parts, but the general process of using forms is:
15
+
16
+ 1. Create a *form class* to describe the data in your form.
17
+ 2. Use an instance of that form class to generate HTML for the elements of the form.
18
+ 3. Implement a *handler class* that will receive an instance of your form class, populated with the data provided when the form was submitted. (**Note**: forms generally cannot contain data submitted by the browser that was not described by the form class, thus obviating the need for something like Rails' strong parameters).
19
+ 4. Add server-side constraint violations to the form and re-render your HTML or use the form data as input to a
20
+ back-end process.
21
+
22
+ ### Forms Are Submitted to Routes
23
+
24
+ When you have a form to process, create a `form` route (remember, all routes must follow the [rules of routing](/routes)):
25
+
26
+ ```ruby{6}
27
+ class App < Brut::Framework::App
28
+
29
+ # ...
30
+
31
+ routes do
32
+ form "/login"
33
+
34
+ # ...
35
+ end
36
+ end
37
+ ```
38
+
39
+ Because a form is not a url, the `form` method will set up two expectations of your code:
40
+
41
+ * There should be a form class, in this example named `LoginForm`
42
+ * There should be a handler class, in this example named `LoginHandler`.
43
+
44
+ When a browser issues an HTTP `POST` to `/login`, the forms contents will populate an instance of
45
+ `LoginForm`, which will be given to an instance of a `LoginHandler` to process the form submission.
46
+
47
+ First, let's go through the bare minimum of form processing.
48
+
49
+ ### Simplest Case of Form Processing
50
+
51
+ #### Creating a Form Class
52
+
53
+ All form classes must be subclasses of `Brut::FrontEnd::Form`, though practically speaking, yours will subclass `AppForm`, which subclasses `Brut::FrontEnd::Form`. The form class allows you to use various class methods to declare your form's inputs.
54
+
55
+ Let's take a simple example of a login form that has an email and a password. We'll use `input` to declare two
56
+ `<input>` elements. `input`, like `<input>` accepts a type. Brut recognizes the same list of types as modern
57
+ browers. In this case, we want `type="email"` and a `type="password"` inputs:
58
+
59
+ ```ruby
60
+ # app/src/front_end/forms/login_form.rb
61
+ class LoginForm < AppForm
62
+ input :email, type: :email
63
+ input :password, type: :password
64
+ end
65
+ ```
66
+
67
+ #### Generating HTML with a Form Object
68
+
69
+ Let's suppose we have a `LoginPage` that will include this form. Below is a sketch of the implementation. Note
70
+ that `form_tag` is a Brut-provided method that will create an HTML `<form>` but also include a hidden field used
71
+ for CSRF protection.
72
+
73
+ ```ruby
74
+ # app/src/front_end/pages/login_page.rb
75
+ class LoginPage < AppPage
76
+ def initialize
77
+ @form = LoginForm.new
78
+ end
79
+
80
+ def page_template
81
+ form_tag(method: :post,
82
+ action: LoginHandler.routing) do
83
+ # ...
84
+ button { "Login" }
85
+ end
86
+ end
87
+ end
88
+ ```
89
+
90
+ Brut can generate the HTML for the needed inputs via `Brut::FrontEnd::Components::Inputs::TextField.for_form_input`, which is a very long name. Hold that thought for now. This method will generate an `<input>` element for you, based on how you've set up the field in your form class. The HTML element will have a value set based on the form, if there is a value.
91
+
92
+ ```ruby {11,12}
93
+ # app/src/front_end/pages/login_page.rb
94
+ class LoginPage < AppPage
95
+ def initialize
96
+ @form = LoginForm.new
97
+ end
98
+
99
+ def page_template
100
+ form_tag(method: :post,
101
+ action: LoginHandler.routing) do
102
+ # We promise you don't have to type this every time!
103
+ Brut::FrontEnd::Components::Inputs::TextField.for_form_input(form: @form, input_name: :email)
104
+ Brut::FrontEnd::Components::Inputs::TextField.for_form_input(form: @form, input_name: :password)
105
+ button { "Login" }
106
+ end
107
+ end
108
+ end
109
+ ```
110
+
111
+ This produces the following HTML (formatted here for clarity):
112
+
113
+ ```html
114
+ <form method="post" action="/login">
115
+ <input type="email" name="email" required>
116
+ <input type="password" name="password" required>
117
+ <button>Login</button>
118
+ </form>
119
+ ```
120
+
121
+ Note that each fields type and name match what was used in `LoginForm`. Also note that both fields have the `required` attribute. We'll discuss why in a moment.
122
+
123
+ #### Expedient Access to Brut Components
124
+
125
+ `Brut::FrontEnd::Components` is the root namespace of, among other things, [components](/components) provided by Brut. Components are Phlex components that generate HTML and can be accessed as a Phlex *kit*, by including the namespace in your class:
126
+
127
+
128
+ ```ruby {2}
129
+ # app/src/front_end/pages/login_page.rb
130
+ class LoginPage < AppPage
131
+ include Brut::FrontEnd::Components
132
+
133
+ # ...
134
+ end
135
+ ```
136
+
137
+ This allows you to call `Inputs::TextField.for_form_input`:
138
+
139
+ ```ruby {12,13}
140
+ # app/src/front_end/pages/login_page.rb
141
+ class LoginPage < AppPage
142
+ include Brut::FrontEnd::Components
143
+
144
+ def initialize
145
+ @form = LoginForm.new
146
+ end
147
+
148
+ def page_template
149
+ form_tag(method: :post,
150
+ action: LoginHandler.routing) do
151
+ Inputs::TextField.for_form_input(form: @form, input_name: :email)
152
+ Inputs::TextField.for_form_input(form: @form, input_name: :password)
153
+ button { "Login" }
154
+ end
155
+ end
156
+ end
157
+ ```
158
+
159
+ Brut prefers this style, instead of a bag of random helpers, to make it more clear where the logic being
160
+ called is actually coming from. In practice, you would create your own re-usable components for input
161
+ fields that use `Brut::FrontEnd::Components::Inputs::TextField` and friends, so even if you find the kit
162
+ version too long, it's not something you should be typing a lot.
163
+
164
+ #### Receive the Submission
165
+
166
+ When the website visitor clicks the "Login" button, the form's data is submitted to `/login` via an HTTP Post.
167
+ Brut expects the class `LoginHandler` to exist and will call its `handle!` method. A *handler* must extend
168
+ `Brut::FrontEnd::Handler`, though practically speaking it will extend your app's `AppHandler`, which extends `Brut::FrontEnd::Handler`.
169
+
170
+ The handler's initializer's signature indicates what data should be passed-in by Brut. Since this is processing a
171
+ form submission, the `form:` parameter should be included. If it is, an instance of `LoginForm`, populated with the data provided by the website visitor, will be passed to the initializer. It can accept other parameters as well, but we'll discuss that later.
172
+
173
+ Note that *you* must implement `handle`, which `handle!` calls:
174
+
175
+ ```ruby
176
+ # app/src/front_end/handlers/login_handler.rb
177
+ class LoginHandler < AppHandler
178
+ def initializer(form:)
179
+ @form = form
180
+ end
181
+ def handle
182
+ # ...
183
+ end
184
+ end
185
+ ```
186
+
187
+ Typically, `handle` will implement a common pattern: checking the validity of the form submission and, if it's
188
+ not valid, re-render the previous page with errors, whereas if it is valid, execute some back-end logic.
189
+
190
+ If you'll remember, both email and password were set as required in the HTML. We'll talk about how to control
191
+ that behavior later, but it does mean that the browser would not submit form data without those values provided.
192
+ That said, JavaScript could be circumvented, so our handler could be called without either of those fields.
193
+
194
+ Because `LoginForm` describes the inputs *and* we used an instance of it to generate HTML, that instance can re-evaulate the client-side constraints at any time. The handler does this by calling `#constraint_violations?`.
195
+
196
+ ```ruby {7}
197
+ # app/src/front_end/handlers/login_handler.rb
198
+ class LoginHandler < AppHandler
199
+ def initialize(form:)
200
+ @form = form
201
+ end
202
+ def handle
203
+ if @form.constraint_violations?
204
+ # ...
205
+ else
206
+ # ...
207
+ end
208
+ end
209
+ end
210
+ ```
211
+
212
+ Of course, some constraints can't be validated
213
+ client-side and require some back-end logic. In this case, we want to check that there is an authorized user
214
+ with that email and password. Let's assume the existence of the class `AuthorizedUser` that has a class method
215
+ `login` that returns `nil` if there is no user with that email/password combination.
216
+
217
+ If that returns `nil`, we want to re-render the `LoginPage`, exposing some sort of constraint violation message
218
+ so it can be rendered. We also want the form fields to be pre-filled with the values the visitor provided.
219
+
220
+ `for_form_input` can handle this, so we need to pass our form object into `LoginPage` instead of allowing `LoginPage` to create an empty one. We can do that by adding a `form:` keyword argument that defaults to `nil`:
221
+
222
+ ```ruby {3,4}
223
+ # app/src/front_end/pages/login_page.rb
224
+ class LoginPage < AppPage
225
+ def initialize(form: nil)
226
+ @form = form || LoginForm.new
227
+ end
228
+
229
+ def page_template
230
+ form_tag(method: :post,
231
+ action: LoginHandler.routing) do
232
+ Inputs::TextField.for_form_input(form: @form, input_name: :email)
233
+ Inputs::TextField.for_form_input(form: @form, input_name: :password)
234
+ button { "Login" }
235
+ end
236
+ end
237
+ end
238
+ ```
239
+
240
+ To trigger this behavior, the handler will:
241
+
242
+ * Call `server_side_constraint_violation` on the form instance.
243
+ * Pass it to `LoginPage.new`, which it will return, thus re-rendering the page (when a handler's `handle!` method returns an instance of a page, that page's HTML is generated as the response).
244
+
245
+ ```ruby {10-13,17}
246
+ # app/src/front_end/handlers/login_handler.rb
247
+ class LoginHandler < AppHandler
248
+ def handle(form:)
249
+ if !form.constraint_violations?
250
+ authorized_user = AuthorizedUser.login(
251
+ email: form.email,
252
+ password: form.password
253
+ )
254
+ if authorized_user.nil?
255
+ form.server_side_constraint_violation(
256
+ input_name: :email,
257
+ key: :login_not_found
258
+ )
259
+ end
260
+ end
261
+ if form.constraint_violations?
262
+ LoginPage.new(form: @form)
263
+ else
264
+ # ...
265
+ end
266
+ end
267
+ end
268
+ ```
269
+
270
+ When `LoginPage` generates HTML, different HTML is generated, since the form being passed to
271
+ `for_form_input` contains constraint violations.
272
+
273
+ #### Showing Constraint Violations in HTML
274
+
275
+ When `Inputs::TextField.for_form_input` is called with an existing form that has constraint violations, different HTML is generated. This is what would be produced by our existing `LoginPage` (again, formatted her for clarity):
276
+
277
+ ```html {3}
278
+ <form method="post" action="/login">
279
+ <input type="email" name="email" required
280
+ data-invalid data-login_not_found>
281
+ <input type="password" name="password" required>
282
+ <button>Login</button>
283
+ </form>
284
+ ```
285
+
286
+ These `data-` attributes allow you to target these fields with CSS.
287
+
288
+ Actual error messages aren't shown since we didn't put in any HTML that might hold them. The form object
289
+ is capable of exposing the constraint violations as keys, intended to be used by the [I18n system](/i18n).
290
+
291
+ In general, you don't want to do this directy, but the API looks like so:
292
+
293
+ ```ruby
294
+ form.input(:email).validity_state.each do |constraint|
295
+ # use constraint.key to construct a message
296
+ end
297
+ ```
298
+
299
+ The reason to avoid this is that a) Brut provides a built-in component to generate HTML and b) if you use
300
+ Brut's component, you can achieve parity between client-side constraint violations detected by the browser
301
+ and server-side violations identified by your app.
302
+
303
+ ### Forms and Constraint Violations
304
+
305
+ There are two common issues around constraint violations in HTML forms:
306
+
307
+ * Handling the case where JavaScript is circumvented and invalid data is submitted to the server.
308
+ * Unifying how client- and server-side constraint violations are shown the user.
309
+
310
+ We saw that the use of form classes handles the first issue: a form created with submitted data can self-validate its configured client-side constraints. In this section, we'll see how to unify the violations from both client- and server-side, which will include actually showing error messages.
311
+
312
+ Above, we mentioned that each constrait violation is represented by a key to be used with the [I18n
313
+ system](/i18n). For client-side violations, these keys are limited to those that are part of the web platform's [`ValidityState`](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState) class. For example, `patternMismatch` is the key used when an input field's value doesn't match the regular expression set in the `pattern` attribute.
314
+
315
+ Brut provides default translations for these in `app/config/i18n/en/1_defaults.rb` under the prefix
316
+ `cv.fe` (`cv` being short of "constraint violation" and `fe` being short for "front end"). Note that these
317
+ keys match `ValidityState` so are in camel-case, not Ruby's idiomatic snake-case.
318
+
319
+ Back-end constraint violations are expected to have keys under `cv.be` (`be` for "back-end"), and these
320
+ keys *should* conform to Ruby's idioms.
321
+
322
+ Let's look at showing server-side constraints first, since those are more like what you may be familiar
323
+ with coming from Rails.
324
+
325
+ #### Showing Server-Side Violations
326
+
327
+ Brut provides the component `Brut::FrontEnd::Components::ConstraintViolations`, which will render all the markup you need for both server- and client-side violations. When there are server-side violations, this component will handle generating the actual error messages.
328
+
329
+ Because we've included `Brut::FrontEnd::Components`, the Phlex kit allows
330
+ `ConstraintViolations` to be called directly, like so:
331
+
332
+ ```ruby {7,10}
333
+ # Inside app/src/front_end/pages/login_page.rb
334
+ def page_template
335
+ form_tag(method: :post,
336
+ action: LoginHandler.routing) do
337
+
338
+ Inputs::TextField.for_form_input(form: @form, input_name: :email)
339
+ ConstraintViolations(form: @form, input_name: :email)
340
+
341
+ Inputs::TextField.for_form_input(form: @form, input_name: :password)
342
+ ConstraintViolations(form: @form, input_name: :password)
343
+
344
+ button { "Login" }
345
+ end
346
+ end
347
+ ```
348
+
349
+ In the case where we've set the server-side constraint violation for the email field, and assuming that the i18n
350
+ key "cv.be.login\_not\_found" maps to the string "No login with that email/password", here is the HTML that
351
+ will be rendered:
352
+
353
+ ```html {4-8,11-12}
354
+ <form method="post" action="/login">
355
+ <input type="email" name="email" required
356
+ data-invalid data-login_not_found>
357
+ <brut-cv-messages input-name="email">
358
+ <brut-cv server-side>
359
+ No login with that email/password.
360
+ </brut-cv>
361
+ </brut-cv-messages>
362
+
363
+ <input type="password" name="password" required>
364
+ <brut-cv-messages input-name="password">
365
+ </brut-cv-messages>
366
+
367
+ <button>Login</button>
368
+ </form>
369
+ ```
370
+
371
+ `brut-cv-messages` and `brut-cv` are [autonomous custom
372
+ elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements). If you aren't
373
+ familiar with this part of the web platform, there are two things to know:
374
+
375
+ * These elements can be targeted with CSS without any JavaScript executing at all (they are treated and rendered by the browser as if they are `display: inline` elements).
376
+ * It's possible to attach behavior to these with JavaScript to add progressively-enhanced behavior.
377
+
378
+ Without any JavaScript, you now have the basis for styling your error messages *and* the server-side messages are
379
+ now rendered using internationalization. As a very basic demonstration, you could place this in
380
+ `app/src/front_end/css/index.css`:
381
+
382
+ ```css
383
+ input[data-invalid] {
384
+ color: red;
385
+ background-color: mistyrose; /* yes, that's a CSS color :) */
386
+ }
387
+ brut-cv-messages {
388
+ color: red;
389
+ display: block;
390
+
391
+ brut-cv {
392
+ display: block;
393
+ }
394
+ }
395
+ ```
396
+
397
+ #### Dynamically Showing Client-Side Violations
398
+
399
+ It would be nice if, when the browser detects client-side violations before the user submits the form, the
400
+ same UI could be used to show *those* error messages. Brut achieves this via the aforementioned
401
+ autonomous custom elements.
402
+
403
+ You'll note that even though the password field had no constraint violations, `<brut-cv-messages input-name="password">` was still generated for it. This element, working in conjuction with a few other elements, will provide localized messaging for client-side constraint violations using the same markup and CSS as your server-side constraint violations.
404
+
405
+ * `<brut-form>` will manage the `<form>` it contains to listen for any violations
406
+ * `<brut-cv-messages>` identifies where error messages should go, per form element.
407
+ * `<brut-cv>` contains a specific message or key.
408
+ * `<brut-i18n-translation>` maps keys from `<brut-cv>` elements to actual translated strings.
409
+
410
+ Together, these elements will show the visitor localized error messages exactly the same way as
411
+ server-side error messages are shown.
412
+
413
+ First, we need to wrap our form with `brut-form`:
414
+
415
+ ```ruby {3,15}
416
+ # Inside app/src/front_end/pages/login_page.rb
417
+ def page_template
418
+ brut_form do
419
+ form_tag(method: :post,
420
+ action: LoginHandler.routing) do
421
+
422
+ Inputs::TextField.for_form_input(form: @form, input_name: :email)
423
+ ConstraintViolations(form: @form, input_name: :email)
424
+
425
+ Inputs::TextField.for_form_input(form: @form, input_name: :password)
426
+ ConstraintViolations(form: @form, input_name: :password)
427
+
428
+ button { "Login" }
429
+ end
430
+ end
431
+ end
432
+ ```
433
+
434
+ Second, we need `<brut-i18-translation>` elements on the page somewhere. These *should* be in your
435
+ default layout and look like so:
436
+
437
+ ```ruby {7,8}
438
+ # app/src/front_end/layouts/default_layout.rb
439
+ def view_template
440
+ doctype
441
+ html(lang: "en") do
442
+ head do
443
+ # ...
444
+ I18nTranslations("cv.fe")
445
+ I18nTranslations("cv.this_field")
446
+ # ...
447
+ end
448
+ body do
449
+ yield
450
+ end
451
+ end
452
+ end
453
+ ```
454
+
455
+ `I18nTranslations` is a shortcut to `Brut::FrontEnd::Components::I18nTranslations`, which is a component to
456
+ render one `<brut-i18n-translation>` element per transalation found under the given prefix. Thus, it
457
+ would generate HTML like so:
458
+
459
+ ```html
460
+ <brut-i18n-translation
461
+ key="cv.fe.badInput"
462
+ value="%{field} is the wrong type of data">
463
+ </brut-i18n-translation>
464
+ <brut-i18n-translation
465
+ key="cv.fe.patternMismatch"
466
+ value="%{field} isn't in the right format">
467
+ </brut-i18n-translation>
468
+ <!-- etc. -->
469
+ ```
470
+
471
+ With this in place, here is how this works:
472
+
473
+ 1. The `<brut-form>` listens for constraint violations on the `<form>` elements.
474
+ 2. When one is detected, it then locates the `<brut-cv-messages>` element for that element's name (based on the `input-name` attribute).
475
+ 3. `<brut-form>` will insert one `<brut-cv>` for each constraint that element's value violates, based on `ValidityState`. The value from `ValidityState` is used to create an I18n key, for example `<brut-cv key="cv.fe.patternMismatch"></brut-cv>`.
476
+ 4. `brut-cv` itself is a custom element that will use its `key` attribute to locate the actual message to show. That message is expected to be in a `<brut-i18n-translation>` element with a matching key, somewhere on the page.
477
+
478
+ This may seem convoluted, however it separates concerns reasonably well and allows localization of the messaging.
479
+
480
+ If your visitor's locale is not `en`, the layout would render different values for each `<brut-i18n-transation>` elements, thus allowing client-side constraint violations to be shown in the visitor's language.
481
+
482
+ You can now style these client-side messages with a slight change to your CSS:
483
+
484
+ ```css {2}
485
+ input[data-invalid],
486
+ input:invalid {
487
+ color: red;
488
+ background-color: mistyrose;
489
+ }
490
+ brut-cv-messages {
491
+ color: red;
492
+ display: block;
493
+
494
+ brut-cv {
495
+ display: block;
496
+ }
497
+ }
498
+ ```
499
+
500
+ Note that a) this didn't require a lot of code on your part, b) the server is still re-evaluating the
501
+ client-side constraints, so the visitor will see them, even if JavaScript is off or fails, and c) it
502
+ sticks as closely to the web platform as possible.
503
+
504
+ That all said, this implementation falls vicitim to an annoyance of client-side constraint violations, which is prematurely showing error messages.
505
+
506
+ #### Managing Errors Shown Before Submission
507
+
508
+ I'm sure we've all experienced over-zealous forms where typing a single character into an email field reveals a blaring red message that our email is not valid. Brut's custom elements can help.
509
+
510
+ You can certainly use [`user-invalid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:user-invalid) to help address this problem, but it doesn't always work how you'd think, and is only recently available in
511
+ [Baseline](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility).
512
+
513
+ To help, `brut-form` will set the attribute `submitted-invalid` on itself if the user has attempted to submit the form with data that violates the client-side constraints. A slight change to CSS will cause your error messages to only show up when submission has been attempted:
514
+
515
+ ```css
516
+ /* First, hide client-side messaging by default.
517
+ Server-side messages will always appear */
518
+ form {
519
+ brut-cv {
520
+ display:none;
521
+ }
522
+ brut-cv[server-side] {
523
+ display:block;
524
+ }
525
+ }
526
+
527
+ /* Now, show constraint violations only if
528
+ submitted-invalid was set */
529
+ brut-form[submitted-invalid] {
530
+ brut-cv {
531
+ display:block;
532
+ }
533
+ }
534
+
535
+ /* Always show elements with data-invalid since that
536
+ is server-generated, but only style the elements
537
+ as invalid if the form has submitted-invalid on it */
538
+ input[data-invalid],
539
+ brut-form[submitted-invalid] input:invalid {
540
+ color: red;
541
+ background-color: mistyrose;
542
+ }
543
+
544
+ brut-cv-messages {
545
+ color: red;
546
+ display: block;
547
+ }
548
+ ```
549
+
550
+ Now, client-side constraint violations will only be shown to the user when they attempt to submit the form. Note that you have complete control, since this is all impelmented using standard CSS. Brut and its custom elements give you the tools and hooks to style as you see fit.
551
+
552
+ ### Checkboxes
553
+
554
+ Checkboxes are implemented in HTML by `<input type="checkbox">`, so in your form, you would use `type:
555
+ :checkbox`:
556
+
557
+ ```ruby {5,6}
558
+ # app/src/front_end/forms/login_form.rb
559
+ class LoginForm < AppForm
560
+ input :email, type: :email
561
+ input :password, type: :password
562
+ input :remember, type: :checkbox
563
+ input :not_robot, type: :checkbox
564
+ end
565
+ ```
566
+
567
+ Checkboxes can be rendered by `Inputs::TextField.for_form_input`, and their `value` attribute would always be the string `"true"`. If the form's value for the input is the string `"true"`, the checkbox would have the `checked` attribute:
568
+
569
+ ```html
570
+ <!-- Form.new(params: { remember: "true" }) -->
571
+ <input type="checkbox" name="remember" value="true" checked>
572
+ <input type="checkbox" name="not_robot" value="true">
573
+ ```
574
+
575
+ ### Radio Buttons
576
+
577
+ Radio buttons are implemented in HTML by `<input type="radio">`, with an expectation of more than one such input
578
+ having the same value for the `name` attribute, but different values for the `value` attributes, one of which may
579
+ be `checked`.
580
+
581
+ Brut implements this via `Brut::FrontEnd::Components::Inputs::RadioButton`, which has the class method
582
+ `for_form_input`. To create radio buttons in a form, use `radio_button_group`:
583
+
584
+ ```ruby {5}
585
+ # app/src/front_end/forms/login_form.rb
586
+ class LoginForm < AppForm
587
+ input :email, type: :email
588
+ input :password, type: :password
589
+ radio_button_group :remember
590
+ end
591
+ ```
592
+
593
+ The form would not need to be configured with the possible values - that will happen when you generate HTML:
594
+
595
+ ```ruby
596
+ def view_template
597
+ form do
598
+ [ :never, :one_week, :one_month ].each do |remember|
599
+ label do
600
+ render(
601
+ Inputs::RadioButton.for_form_input(
602
+ form:,
603
+ input_name: :remember,
604
+ value: remember
605
+ )
606
+ )
607
+ plain { remember.to_s }
608
+ end
609
+ end
610
+ end
611
+ end
612
+ ```
613
+
614
+ When generating HTML, Brut will examine the value of `form.remember` to know which radio button to check. To set
615
+ a default, set that value when creating the form:
616
+
617
+ ```ruby {4-6}
618
+ # app/src/front_end/pages/login_page.rb
619
+ class LoginPage < AppPage
620
+ def initialize(form: nil)
621
+ @form = form || LoginForm.new(params: {
622
+ remember: "never"
623
+ })
624
+ end
625
+ ```
626
+
627
+ ### Selects
628
+
629
+ Selects are implemented in HTML by a `<select>` that has a `name` attribute, and contains several `<option>` elements, each having a `value` attribute. They work like radio buttons in Brut, in that you would not specify the possible values in the form class.
630
+
631
+ > [!WARNING]
632
+ > Brut does not support multi-selects, yet.
633
+
634
+
635
+ You can set up a select via `select`
636
+
637
+ ```ruby {5}
638
+ # app/src/front_end/forms/login_form.rb
639
+ class LoginForm < AppForm
640
+ input :email, type: :email
641
+ input :password, type: :password
642
+ select :remember
643
+ end
644
+ ```
645
+
646
+ Creating the HTML can be done with `Brut::FrontEnd::Components::Inputs::Select`. It's `for_form_input` is more complex, since it provides a way to show visitor-friendly values instead of the innate `value` for each option,
647
+ as well as to allow for a "blank" entry.
648
+
649
+ Let's suppose we have a class named `LoginRememberOption`. It's a simple wrapper around a value we might store in the database and use to lookup an I18n key.
650
+
651
+ ```ruby
652
+ class LoginRememberOption
653
+ include Brut::I18n::ForBackend
654
+ def initialize(value)
655
+ @value = value
656
+ end
657
+
658
+ def to_s = @value
659
+
660
+ def name
661
+ t("login.remember_options.#{@value}")
662
+ end
663
+
664
+ def self.all
665
+ [
666
+ LoginRememberOption.new("never"),
667
+ LoginRememberOption.new("one_week"),
668
+ LoginRememberOption.new("one_month"),
669
+ ]
670
+ end
671
+ end
672
+ ```
673
+
674
+ To show these options in a `<select>`, we might do this:
675
+
676
+ ```ruby
677
+ def view_template
678
+ form do
679
+ render(
680
+ Inputs::Select.for_form_input(
681
+ form:,
682
+ input_name: :remember,
683
+ options: LoginRememberOption.all,
684
+ value_attribute: :to_s,
685
+ option_text_attribute: :name,
686
+ include_blank: {
687
+ value: :blank,
688
+ text_content: "-- Choose --",
689
+ }
690
+ )
691
+ )
692
+ end
693
+ end
694
+ ```
695
+
696
+ This will create this HTML (making some assumptions about the translations):
697
+
698
+ ```html
699
+ <select name="remember">
700
+ <option value="blank">-- Choose --</option>
701
+ <option value="never">Never</option>
702
+ <option value="one_week">One Week</option>
703
+ <option value="one_month">One Month</option>
704
+ </select>
705
+ ```
706
+
707
+ ### Arrays of Values
708
+
709
+ Some complex forms involve a potentially arbitrary number of inputs for a given field. For example, you might allow the visitor to edit widgets in bulk, 10 at a time.
710
+
711
+ Brut can handle this, with help from Rack. First, you'll use `array: true` when declaring an input:
712
+
713
+ ```ruby
714
+ class BulkWidgetForm < AppForm
715
+ input :name, array: true, required: false
716
+ end
717
+ ```
718
+
719
+ In this case, we need `required: false` or every single field we generate will be required.
720
+
721
+ To generate the HTML, use the optional `index:` parameter to `for_form_input` as well as for
722
+ `ConstraintViolations`:
723
+
724
+ ```ruby {11,16}
725
+ # Inside e.g. app/src/front_end/pages/create_bulk_widget_page.rb
726
+ def page_template
727
+ brut_form do
728
+ form_tag(method: :post,
729
+ action: BulkWidgetForm.routing) do
730
+
731
+ 10.times do |i|
732
+ Inputs::TextField.for_form_input(
733
+ form: @form,
734
+ input_name: :name,
735
+ index: i
736
+ )
737
+ ConstraintViolations(
738
+ form: @form,
739
+ input_name: :email,
740
+ index: i
741
+ )
742
+ end
743
+
744
+ button { "Save" }
745
+ end
746
+ end
747
+ end
748
+ ```
749
+
750
+ This will generate HTML like so:
751
+
752
+ ```html
753
+ <!-- ... -->
754
+ <input name="name[]">
755
+ <!-- ... -->
756
+ <input name="name[]">
757
+ <!-- ... -->
758
+ ```
759
+
760
+ The `[]` is a Rack-specific format that will provide the values to the server as an array. While this is not supported nor required of the web platform, Rack does not provide all values for a given input name, unless that name has the `[]` suffix.
761
+
762
+ Also note that you do not have to specify a max length of the array. You can use as many as you like, just be sure that the index values are monotonically increasing with no gaps.
763
+
764
+ In the handler, values can be accessed by index:
765
+
766
+ ```ruby {4}
767
+ # app/src/front_end/handlers/bulk_widget_handler.rb
768
+ class BulkWidgetHandler < AppHandler
769
+ def handle
770
+ @form.name(2) # name with index 2 i.e. the 3rd value
771
+ end
772
+ end
773
+ ```
774
+
775
+ To set the values, you must provide an array to `params:`:
776
+
777
+ ```ruby {3}
778
+ widgets = DB::Widget.order(:created_at).limit(10)
779
+ BulkWidgetForm.new(params: {
780
+ name: widgets.to_a.map(&:name),
781
+ })
782
+
783
+ # OR
784
+ BulkWidgetForm.new(params: {
785
+ name: [
786
+ "",
787
+ "",
788
+ "Third Widget",
789
+ "",
790
+ ""
791
+ ],
792
+ })
793
+ ```
794
+
795
+ To set server-side constraint violations, `index:` can be used:
796
+
797
+ ```ruby {4}
798
+ form.server_side_constraint_violation(
799
+ input_name: :name,
800
+ key: :must_be_two_words,
801
+ index: 3
802
+ )
803
+ ```
804
+
805
+ This can be quite complicated when editing sparse data. Brut should do a decent job raising an error if you try to treat a non-array value as an array or vice-versa.
806
+
807
+ ## Testing
808
+
809
+ Form classes don't need any logic on them, but they can be given helper methods or other logic if it makes sense.
810
+ To test them, test them like any other class - instantiate an object and examine the behavior of its methods.
811
+
812
+ Note that Brut provides the constructor for all form classes, and it expects a single keyword parameter named `params:`
813
+ that is a hash mapping strings to strings representing the submitted form data. The keys can be symbols and Brut will
814
+ map them to strings.
815
+
816
+ Testing handlers is covered in [Handlers](/handlers)
817
+
818
+ When testing the UX around constraint violations, you should use an end-to-end test, as this will allow you to
819
+ assert behavior around client-side constraint violations. This is discussed in [End-to-end
820
+ Tests](/end-to-end-tests).
821
+
822
+ ## Recommended Practices
823
+
824
+ ### Make Use of Components
825
+
826
+ The example we saw above creates only minimal markup, yet required a fair bit of code. You are encouraged to
827
+ create your own components that generate the markup you need for your app's inputs. For example, you are likely
828
+ going to want `app/src/front_end/components/text_field_component.rb` to generate whatever markup is needed fo
829
+ your text fields to look how they are supposed to, with and without constraint violation messages.
830
+
831
+ ### Functional or Utility CSS Is Difficult Here
832
+
833
+ The way constraint violations are implemented leverages the web platform, which naturally includes conventional use of CSS. This creates an "impedance mismatch" with functional or utility CSS like Tailwind.
834
+
835
+ While it may be possible to write a bunch of single-purpose classes to target the markup and attributes Brut generates, it may be easier to write conventional CSS for constraint violations.
836
+
837
+ To avoid duplication, you should leverage the custom properties of your CSS framework. For example:
838
+
839
+ ```css
840
+ input[data-invalid] {
841
+ color: var(--color-red);
842
+ }
843
+ ```
844
+
845
+ In theory, your design for form inputs will be done once and be relatively stable. But, this is the downside of using CSS frameworks that eschew using CSS directly. You will need to manage this.
846
+
847
+ ## Technical Notes
848
+
849
+ > [!IMPORTANT]
850
+ > Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's
851
+ > internals, the source code is always more correct.
852
+
853
+ _Last Updated May 13, 2025_
854
+
855
+ Form internals try to coerce types to strings, since the web and HTTP is all strings all the time. Empty strings
856
+ are coerced to `nil`. If the form's `params:` value contains any type Brut cannot deal with, you'll get an
857
+ exception during tests and a notice/event in production.
858
+
859
+ For HTML generation, there are few classes that work together:
860
+
861
+ * *input definitions* define an input and tend to provide an API similar to HTML's. See `Brut::FrontEnd::Forms::InputDefinition`.
862
+ * *inputs* represent the runtime state of an input from the browser. Whereas an input definition has no state, the input does. It delegates much of its behavior to the underlying input definition. It's `value=` method performs client-side constraint validations by creating a `ValidityState` internally. See `Brut::FrontEnd::Forms::Input`.
863
+ * `Brut::FrontEnd::Forms::InputDeclarations` is a module that allows creating input definitions inside your form
864
+ class. It implements the class methods like `input`.
865
+ * `Brut::FrontEnd::Components::Inputs` contains components used to generate `<input>` fields. These classes will
866
+ coerce the value of the `input` they are given to generate the correct HTML.