brut 0.9.1 → 0.10.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 (353) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/Gemfile.lock +1 -1
  4. data/bin/new-version +8 -0
  5. data/brut-css/package-lock.json +2 -2
  6. data/brut-css/package.json +1 -1
  7. data/brut-js/package-lock.json +2 -2
  8. data/brut-js/package.json +1 -1
  9. data/brut-js/specs/ConstraintViolationMessages.spec.js +4 -1
  10. data/brut-js/specs/Form.spec.js +2 -46
  11. data/brut-js/src/ConstraintViolationMessage.js +17 -2
  12. data/brut-js/src/Form.js +19 -23
  13. data/brutrb.com/.vitepress/config.mjs +1 -0
  14. data/brutrb.com/form-constraints.md +6 -6
  15. data/brutrb.com/recipes/form-errors.md +148 -0
  16. data/docs/404.html +2 -2
  17. data/docs/adrs.html +3 -3
  18. data/docs/ai.html +3 -3
  19. data/docs/api/Brut/BackEnd/SeedData.html +1 -1
  20. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +1 -1
  21. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +1 -1
  22. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +1 -1
  23. data/docs/api/Brut/BackEnd/Sidekiq.html +1 -1
  24. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +1 -1
  25. data/docs/api/Brut/BackEnd/Validators.html +1 -1
  26. data/docs/api/Brut/BackEnd.html +1 -1
  27. data/docs/api/Brut/CLI/App.html +1 -1
  28. data/docs/api/Brut/CLI/AppRunner.html +1 -1
  29. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +1 -1
  30. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +1 -1
  31. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +1 -1
  32. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +1 -1
  33. data/docs/api/Brut/CLI/Apps/BuildAssets.html +1 -1
  34. data/docs/api/Brut/CLI/Apps/DB/Create.html +1 -1
  35. data/docs/api/Brut/CLI/Apps/DB/Drop.html +1 -1
  36. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +1 -1
  37. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +1 -1
  38. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +1 -1
  39. data/docs/api/Brut/CLI/Apps/DB/Seed.html +1 -1
  40. data/docs/api/Brut/CLI/Apps/DB/Status.html +1 -1
  41. data/docs/api/Brut/CLI/Apps/DB.html +1 -1
  42. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +1 -1
  43. data/docs/api/Brut/CLI/Apps/DeployBase.html +1 -1
  44. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +1 -1
  45. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +1 -1
  46. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
  47. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +1 -1
  48. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +1 -1
  49. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +1 -1
  50. data/docs/api/Brut/CLI/Apps/Scaffold/DbModel.html +1 -1
  51. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +1 -1
  52. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +1 -1
  53. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
  54. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +1 -1
  55. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +1 -1
  56. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +1 -1
  57. data/docs/api/Brut/CLI/Apps/Scaffold.html +1 -1
  58. data/docs/api/Brut/CLI/Apps/Test/Audit.html +1 -1
  59. data/docs/api/Brut/CLI/Apps/Test/E2e.html +1 -1
  60. data/docs/api/Brut/CLI/Apps/Test/JS.html +1 -1
  61. data/docs/api/Brut/CLI/Apps/Test/Run.html +1 -1
  62. data/docs/api/Brut/CLI/Apps/Test.html +1 -1
  63. data/docs/api/Brut/CLI/Apps.html +1 -1
  64. data/docs/api/Brut/CLI/Command.html +1 -1
  65. data/docs/api/Brut/CLI/Error.html +1 -1
  66. data/docs/api/Brut/CLI/ExecutionResults/Result.html +1 -1
  67. data/docs/api/Brut/CLI/ExecutionResults.html +1 -1
  68. data/docs/api/Brut/CLI/Executor.html +1 -1
  69. data/docs/api/Brut/CLI/InvalidOption.html +1 -1
  70. data/docs/api/Brut/CLI/Options.html +1 -1
  71. data/docs/api/Brut/CLI/Output.html +1 -1
  72. data/docs/api/Brut/CLI/SystemExecError.html +1 -1
  73. data/docs/api/Brut/CLI.html +1 -1
  74. data/docs/api/Brut/FactoryBot.html +1 -1
  75. data/docs/api/Brut/Framework/App.html +1 -1
  76. data/docs/api/Brut/Framework/Config.html +1 -1
  77. data/docs/api/Brut/Framework/Container.html +1 -1
  78. data/docs/api/Brut/Framework/Error.html +1 -1
  79. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +1 -1
  80. data/docs/api/Brut/Framework/Errors/Bug.html +1 -1
  81. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +1 -1
  82. data/docs/api/Brut/Framework/Errors/MissingParameter.html +1 -1
  83. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +1 -1
  84. data/docs/api/Brut/Framework/Errors/NotFound.html +1 -1
  85. data/docs/api/Brut/Framework/Errors/NotImplemented.html +1 -1
  86. data/docs/api/Brut/Framework/Errors.html +1 -1
  87. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +1 -1
  88. data/docs/api/Brut/Framework/MCP.html +1 -1
  89. data/docs/api/Brut/Framework/ProjectEnvironment.html +1 -1
  90. data/docs/api/Brut/Framework.html +1 -1
  91. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +1 -1
  92. data/docs/api/Brut/FrontEnd/Component/Helpers.html +1 -1
  93. data/docs/api/Brut/FrontEnd/Component.html +1 -1
  94. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +1 -1
  95. data/docs/api/Brut/FrontEnd/Components/FormTag.html +1 -1
  96. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +1 -1
  97. data/docs/api/Brut/FrontEnd/Components/Input.html +1 -1
  98. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +1 -1
  99. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +1 -1
  100. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +1 -1
  101. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +1 -1
  102. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +1 -1
  103. data/docs/api/Brut/FrontEnd/Components/Inputs.html +1 -1
  104. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +1 -1
  105. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +1 -1
  106. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +1 -1
  107. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +1 -1
  108. data/docs/api/Brut/FrontEnd/Components.html +1 -1
  109. data/docs/api/Brut/FrontEnd/CsrfProtector.html +1 -1
  110. data/docs/api/Brut/FrontEnd/Download.html +1 -1
  111. data/docs/api/Brut/FrontEnd/Flash.html +1 -1
  112. data/docs/api/Brut/FrontEnd/Form.html +1 -1
  113. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +1 -1
  114. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +1 -1
  115. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +1 -1
  116. data/docs/api/Brut/FrontEnd/Forms/Input.html +1 -1
  117. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +1 -1
  118. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1 -1
  119. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +1 -1
  120. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
  121. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +1 -1
  122. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
  123. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +1 -1
  124. data/docs/api/Brut/FrontEnd/Forms.html +1 -1
  125. data/docs/api/Brut/FrontEnd/GenericResponse.html +1 -1
  126. data/docs/api/Brut/FrontEnd/Handler.html +1 -1
  127. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +1 -1
  128. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +1 -1
  129. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +1 -1
  130. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +1 -1
  131. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +1 -1
  132. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +1 -1
  133. data/docs/api/Brut/FrontEnd/Handlers.html +1 -1
  134. data/docs/api/Brut/FrontEnd/HandlingResults.html +1 -1
  135. data/docs/api/Brut/FrontEnd/HttpMethod.html +1 -1
  136. data/docs/api/Brut/FrontEnd/HttpStatus.html +1 -1
  137. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +1 -1
  138. data/docs/api/Brut/FrontEnd/Layout.html +1 -1
  139. data/docs/api/Brut/FrontEnd/Middleware.html +1 -1
  140. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +1 -1
  141. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +1 -1
  142. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +1 -1
  143. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +1 -1
  144. data/docs/api/Brut/FrontEnd/Middlewares.html +1 -1
  145. data/docs/api/Brut/FrontEnd/Page.html +1 -1
  146. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +1 -1
  147. data/docs/api/Brut/FrontEnd/Pages.html +1 -1
  148. data/docs/api/Brut/FrontEnd/RequestContext.html +1 -1
  149. data/docs/api/Brut/FrontEnd/RouteHook.html +1 -1
  150. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +1 -1
  151. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +1 -1
  152. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +1 -1
  153. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +1 -1
  154. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +1 -1
  155. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +1 -1
  156. data/docs/api/Brut/FrontEnd/RouteHooks.html +1 -1
  157. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +1 -1
  158. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +1 -1
  159. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +1 -1
  160. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +1 -1
  161. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +1 -1
  162. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +1 -1
  163. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +1 -1
  164. data/docs/api/Brut/FrontEnd/Routing/Route.html +1 -1
  165. data/docs/api/Brut/FrontEnd/Routing.html +1 -1
  166. data/docs/api/Brut/FrontEnd/Session.html +1 -1
  167. data/docs/api/Brut/FrontEnd.html +1 -1
  168. data/docs/api/Brut/I18n/BaseMethods.html +1 -1
  169. data/docs/api/Brut/I18n/ForBackEnd.html +1 -1
  170. data/docs/api/Brut/I18n/ForCLI.html +1 -1
  171. data/docs/api/Brut/I18n/ForHTML.html +1 -1
  172. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +1 -1
  173. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +1 -1
  174. data/docs/api/Brut/I18n.html +1 -1
  175. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +1 -1
  176. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +1 -1
  177. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +1 -1
  178. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +1 -1
  179. data/docs/api/Brut/Instrumentation.html +1 -1
  180. data/docs/api/Brut/RubocopConfig.html +1 -1
  181. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +1 -1
  182. data/docs/api/Brut/SinatraHelpers.html +1 -1
  183. data/docs/api/Brut/SpecSupport/ClockSupport.html +1 -1
  184. data/docs/api/Brut/SpecSupport/ComponentSupport.html +1 -1
  185. data/docs/api/Brut/SpecSupport/E2ETestServer.html +1 -1
  186. data/docs/api/Brut/SpecSupport/E2eSupport.html +1 -1
  187. data/docs/api/Brut/SpecSupport/EnhancedNode.html +1 -1
  188. data/docs/api/Brut/SpecSupport/FlashSupport.html +1 -1
  189. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +1 -1
  190. data/docs/api/Brut/SpecSupport/GeneralSupport.html +1 -1
  191. data/docs/api/Brut/SpecSupport/HandlerSupport.html +1 -1
  192. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +1 -1
  193. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +1 -1
  194. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +1 -1
  195. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +1 -1
  196. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +1 -1
  197. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +1 -1
  198. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +1 -1
  199. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +1 -1
  200. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +1 -1
  201. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +1 -1
  202. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +1 -1
  203. data/docs/api/Brut/SpecSupport/Matchers.html +1 -1
  204. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +1 -1
  205. data/docs/api/Brut/SpecSupport/RSpecSetup.html +1 -1
  206. data/docs/api/Brut/SpecSupport/SessionSupport.html +1 -1
  207. data/docs/api/Brut/SpecSupport.html +1 -1
  208. data/docs/api/Brut.html +1 -1
  209. data/docs/api/Clock.html +1 -1
  210. data/docs/api/ModuleName.html +1 -1
  211. data/docs/api/RichString.html +1 -1
  212. data/docs/api/SemanticLogger/Appender/Async.html +1 -1
  213. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +1 -1
  214. data/docs/api/Sequel/Extensions/BrutMigrations.html +1 -1
  215. data/docs/api/Sequel/Extensions.html +1 -1
  216. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +1 -1
  217. data/docs/api/Sequel/Plugins/CreatedAt.html +1 -1
  218. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +1 -1
  219. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +1 -1
  220. data/docs/api/Sequel/Plugins/ExternalId.html +1 -1
  221. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +1 -1
  222. data/docs/api/Sequel/Plugins/FindBang.html +1 -1
  223. data/docs/api/Sequel/Plugins.html +1 -1
  224. data/docs/api/Sequel.html +1 -1
  225. data/docs/api/_index.html +1 -1
  226. data/docs/api/file.README.html +1 -1
  227. data/docs/api/index.html +1 -1
  228. data/docs/api/top-level-namespace.html +1 -1
  229. data/docs/assets/{app.BuBdZoUg.js → app.vjGWMSnJ.js} +1 -1
  230. data/docs/assets/chunks/@localSearchIndexroot.Dn1xGMv_.js +1 -0
  231. data/docs/assets/chunks/{VPLocalSearchBox.UtnyvkX-.js → VPLocalSearchBox.C-ymMW2k.js} +1 -1
  232. data/docs/assets/chunks/{theme.DqwvuBEK.js → theme.pJUosGlI.js} +2 -2
  233. data/docs/assets/{components.md.Bu80E2Nr.js → components.md.B543a3Lm.js} +3 -3
  234. data/docs/assets/{configuration.md.CuIxVsSf.js → configuration.md.-foE_iVv.js} +1 -1
  235. data/docs/assets/{forms.md.DnLbzVDa.js → forms.md.D5-2rgHh.js} +1 -1
  236. data/docs/assets/{getting-started.md.DdQLmU3C.js → getting-started.md.Cd4XSZb_.js} +3 -3
  237. data/docs/assets/{getting-started.md.DdQLmU3C.lean.js → getting-started.md.Cd4XSZb_.lean.js} +1 -1
  238. data/docs/assets.html +3 -3
  239. data/docs/brut-js/api/AjaxSubmit.html +1 -1
  240. data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
  241. data/docs/brut-js/api/Autosubmit.html +1 -1
  242. data/docs/brut-js/api/Autosubmit.js.html +1 -1
  243. data/docs/brut-js/api/BaseCustomElement.html +1 -1
  244. data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
  245. data/docs/brut-js/api/BrutCustomElements.html +1 -1
  246. data/docs/brut-js/api/BufferedLogger.html +1 -1
  247. data/docs/brut-js/api/ConfirmSubmit.html +1 -1
  248. data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
  249. data/docs/brut-js/api/ConfirmationDialog.html +1 -1
  250. data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
  251. data/docs/brut-js/api/ConstraintViolationMessage.html +1 -1
  252. data/docs/brut-js/api/ConstraintViolationMessage.js.html +1 -1
  253. data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
  254. data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
  255. data/docs/brut-js/api/CopyToClipboard.html +1 -1
  256. data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
  257. data/docs/brut-js/api/Form.html +1 -1
  258. data/docs/brut-js/api/Form.js.html +1 -1
  259. data/docs/brut-js/api/I18nTranslation.html +1 -1
  260. data/docs/brut-js/api/I18nTranslation.js.html +1 -1
  261. data/docs/brut-js/api/LocaleDetection.html +1 -1
  262. data/docs/brut-js/api/LocaleDetection.js.html +1 -1
  263. data/docs/brut-js/api/Logger.html +1 -1
  264. data/docs/brut-js/api/Logger.js.html +1 -1
  265. data/docs/brut-js/api/Message.html +1 -1
  266. data/docs/brut-js/api/Message.js.html +1 -1
  267. data/docs/brut-js/api/PrefixedLogger.html +1 -1
  268. data/docs/brut-js/api/RichString.html +1 -1
  269. data/docs/brut-js/api/RichString.js.html +1 -1
  270. data/docs/brut-js/api/Tabs.html +1 -1
  271. data/docs/brut-js/api/Tabs.js.html +1 -1
  272. data/docs/brut-js/api/Tracing.html +1 -1
  273. data/docs/brut-js/api/Tracing.js.html +1 -1
  274. data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
  275. data/docs/brut-js/api/external-Performance.html +1 -1
  276. data/docs/brut-js/api/external-Promise.html +1 -1
  277. data/docs/brut-js/api/external-ValidityState.html +1 -1
  278. data/docs/brut-js/api/external-Window.html +1 -1
  279. data/docs/brut-js/api/external-fetch.html +1 -1
  280. data/docs/brut-js/api/global.html +1 -1
  281. data/docs/brut-js/api/index.html +1 -1
  282. data/docs/brut-js/api/index.js.html +1 -1
  283. data/docs/brut-js/api/module-testing.html +1 -1
  284. data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
  285. data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
  286. data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
  287. data/docs/brut-js/api/testing.DOMCreator.html +1 -1
  288. data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
  289. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
  290. data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
  291. data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
  292. data/docs/brut-js/api/testing_index.js.html +1 -1
  293. data/docs/brut-js.html +3 -3
  294. data/docs/business-logic.html +3 -3
  295. data/docs/cli.html +3 -3
  296. data/docs/components.html +7 -7
  297. data/docs/configuration.html +5 -5
  298. data/docs/css.html +3 -3
  299. data/docs/custom-element-tests.html +3 -3
  300. data/docs/database-access.html +3 -3
  301. data/docs/database-schema.html +3 -3
  302. data/docs/deployment.html +3 -3
  303. data/docs/dev-environment.html +3 -3
  304. data/docs/dir-structure.html +3 -3
  305. data/docs/doc-conventions.html +3 -3
  306. data/docs/end-to-end-tests.html +3 -3
  307. data/docs/features.html +3 -3
  308. data/docs/flash-and-session.html +3 -3
  309. data/docs/form-constraints.html +3 -3
  310. data/docs/forms.html +5 -5
  311. data/docs/getting-started.html +6 -6
  312. data/docs/handlers.html +3 -3
  313. data/docs/hashmap.json +1 -1
  314. data/docs/hooks.html +3 -3
  315. data/docs/i18n.html +3 -3
  316. data/docs/index.html +3 -3
  317. data/docs/instrumentation.html +3 -3
  318. data/docs/javascript.html +3 -3
  319. data/docs/jobs.html +3 -3
  320. data/docs/keyword-injection.html +3 -3
  321. data/docs/layouts.html +3 -3
  322. data/docs/lsp.html +3 -3
  323. data/docs/markdown-examples.html +3 -3
  324. data/docs/middleware.html +3 -3
  325. data/docs/overview.html +3 -3
  326. data/docs/pages.html +3 -3
  327. data/docs/recipes/alternate-layouts.html +3 -3
  328. data/docs/recipes/authentication.html +3 -3
  329. data/docs/recipes/blank-layouts.html +3 -3
  330. data/docs/recipes/custom-flash.html +3 -3
  331. data/docs/recipes/indexed-forms.html +3 -3
  332. data/docs/recipes/migrations.html +3 -3
  333. data/docs/recipes/text-field-component.html +3 -3
  334. data/docs/roadmap.html +3 -3
  335. data/docs/routes.html +3 -3
  336. data/docs/security.html +3 -3
  337. data/docs/seed-data.html +3 -3
  338. data/docs/space-time-continuum.html +3 -3
  339. data/docs/tutorial.html +3 -3
  340. data/docs/unit-tests.html +3 -3
  341. data/docs/why.html +3 -3
  342. data/lib/brut/front_end/components/constraint_violations.rb +23 -10
  343. data/lib/brut/version.rb +1 -1
  344. data/mkbrut/Gemfile.lock +1 -1
  345. data/mkbrut/lib/mkbrut/version.rb +1 -1
  346. data/mkbrut/templates/Base/Dockerfile.dx +2 -2
  347. data/mkbrut/templates/segments/Heroku/deploy/Dockerfile +4 -5
  348. metadata +15 -14
  349. data/.nvim.lua +0 -1
  350. data/docs/assets/chunks/@localSearchIndexroot.BZ_a3X0T.js +0 -1
  351. /data/docs/assets/{components.md.Bu80E2Nr.lean.js → components.md.B543a3Lm.lean.js} +0 -0
  352. /data/docs/assets/{configuration.md.CuIxVsSf.lean.js → configuration.md.-foE_iVv.lean.js} +0 -0
  353. /data/docs/assets/{forms.md.DnLbzVDa.lean.js → forms.md.D5-2rgHh.lean.js} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b580e8b69cc722fb2d84c090f7848b9c45623de2faaa996a14914d52773c48a
4
- data.tar.gz: 37b84eb37ac974c48c054be0305086622fab712345d7ef581b7bd58f3ec7a79e
3
+ metadata.gz: bb034356c94bcb184b5ded4c4d6d07d46d85e28371cbe7fd17ea999c78e1932e
4
+ data.tar.gz: 90339e125f3b8b9bde9bda634f4934d0c0a09955141cda41969c9347b7b86877
5
5
  SHA512:
6
- metadata.gz: 43f5d19e8bb88c0e0956757b1826224f18f68eed46ef34d5a7e8df73b7a8638fb674f499c5e335edae84bd42fb2266fbb19ec347c7e14cbb18027c832e66f33b
7
- data.tar.gz: 7b12806b7ce2164e6d1dd2eb85dc3d01902113fadb6762babcd265d8cca3659dc3bf2913db160c518c6ade57624d2e4558d779a62151807a6c0a9f7e4f2c72e9
6
+ metadata.gz: 1ad76fb17f0247373ded5eb4df807eab9f32db39633082afb647b6b268d6d74f1529b433f3a3a06c5cd101e79d841f3533bef589940de3d629af3c7c400096d0
7
+ data.tar.gz: 50acf68486d2bbcb6600fb2cf0e21f62ec879ba1a3f0767557d29f9e2260229cc4bdad4b869d800cfdc7101723eca7afb4498577f44b22095a24526ec9526a45
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Brut CHANGELOG
2
2
 
3
+ ## v0.10.0 - Aug 15, 2025
4
+
5
+ * Changed `ConstraintViolations` HTML to indicate if a `<brut-cv>` is server-generated, even if it's generating client-side constraint. See #56.
6
+ * `<brut-form>` now requires that all `<brut-cv-messages>` have an `input-name`.
7
+
8
+
9
+ ## v0.9.2 - Aug 14, 2025
10
+
11
+ * Updated Docker instructions for installing PostgreSQL client, due to ruby:3.4 image upgrading to Debian Trixie which removed something. See https://github.com/thirdtank/brut/issues/55
12
+
3
13
  ## v0.9.1 - Aug 12, 2025
4
14
 
5
15
  * Changed `<brut-confirm-submit>` to not ask for confirmation if the form is invalid
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brut (0.9.1)
4
+ brut (0.10.0)
5
5
  concurrent-ruby
6
6
  i18n
7
7
  irb
data/bin/new-version ADDED
@@ -0,0 +1,8 @@
1
+ set -e
2
+
3
+ if [ -f /.dockerenv ]; then
4
+ echo "Do not run this inside Docker"
5
+ exit 1
6
+ fi
7
+
8
+ nvim -o lib/brut/version.rb mkbrut/lib/mkbrut/version.rb brut-js/package.json brut-css/package.json CHANGELOG.md
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "brut-css",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "brut-css",
9
- "version": "0.9.1",
9
+ "version": "0.10.0",
10
10
  "license": "Hippocratic-2.1",
11
11
  "devDependencies": {
12
12
  "comment-parser": "^1.4.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brut-css",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Utility CSS Library for Full Stack Developers",
5
5
  "keywords": [
6
6
  "css"
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "brut-js",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "brut-js",
9
- "version": "0.9.1",
9
+ "version": "0.10.0",
10
10
  "license": "Hippocratic-2.1",
11
11
  "devDependencies": {
12
12
  "esbuild": "^0.24.0",
data/brut-js/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brut-js",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "keywords": [ "WebComponents", "Custom Elements" ],
6
6
  "bugs": {
@@ -18,9 +18,12 @@ describe("<brut-cv-messages>", () => {
18
18
 
19
19
  element.createMessages({validityState,inputName})
20
20
 
21
- assert.equal(element.querySelectorAll("brut-cv").length,2)
21
+ const brutCvElements = element.querySelectorAll("brut-cv")
22
+ assert.equal(brutCvElements.length, 2)
22
23
  assert.match(element.textContent,new RegExp("This field does not match the pattern","m"))
23
24
  assert.match(element.textContent,new RegExp("This field is above the range","m"))
25
+ assert.equal(brutCvElements[0].getAttribute("client-side"),"")
26
+ assert.equal(brutCvElements[1].getAttribute("client-side"),"")
24
27
 
25
28
  element.clearClientSideMessages()
26
29
  assert.equal(element.textContent,"")
@@ -6,12 +6,12 @@ describe("<brut-form>", () => {
6
6
  <form>
7
7
  <label>
8
8
  <input required type="text" name="text">
9
- <brut-cv-messages>
9
+ <brut-cv-messages input-name="text">
10
10
  </brut-cv-messages>
11
11
  </label>
12
12
  <label>
13
13
  <input required type="number" name="number">
14
- <brut-cv-messages>
14
+ <brut-cv-messages input-name="number">
15
15
  </brut-cv-messages>
16
16
  </label>
17
17
  <input type="submit">Save</input>
@@ -134,48 +134,4 @@ describe("<brut-form>", () => {
134
134
  assert(gotValid)
135
135
  assert(!gotInvalid)
136
136
  })
137
- withHTML(`
138
- <brut-form>
139
- <form>
140
- <label>
141
- <input required type="text" name="text">
142
- </label>
143
- <brut-cv-messages input-name='text'>
144
- </brut-cv-messages>
145
- <input type="submit">Save</input>
146
- </form>
147
- </brut-form>
148
- `).test("locates the messages for errors based on name", ({window,document,assert}) => {
149
-
150
- const brutForm = document.querySelector("brut-form")
151
- const form = brutForm.querySelector("form")
152
- const button = form.querySelector("input[type=submit]")
153
- const textFieldLabel = form.querySelector("label:has(input[type=text])")
154
-
155
- let submitted = false
156
- let gotInvalid = false
157
- let gotValid = false
158
-
159
- form.addEventListener("submit", (event) => {
160
- event.preventDefault()
161
- submitted = true
162
- })
163
- brutForm.addEventListener("brut:valid", () => {
164
- gotValid = true
165
- })
166
- brutForm.addEventListener("brut:invalid", () => {
167
- gotInvalid = true
168
- })
169
-
170
- button.click()
171
-
172
- assert(!submitted)
173
- assert(!gotValid)
174
- assert(gotInvalid)
175
- assert.equal(brutForm.getAttribute("submitted-invalid"),"")
176
-
177
- let error = brutForm.querySelector("brut-cv[input-name='text'][key='cv.cs.valueMissing']")
178
- assert(error)
179
-
180
- })
181
137
  })
@@ -15,9 +15,15 @@ import I18nTranslation from "./I18nTranslation"
15
15
  * @property {string} key - the i18n translation key to use. It must map to the `key` of a `<brut-i18n-translation>` on the page or
16
16
  * the element will not render any text.
17
17
  * @property {string} input-name - the name of the input, used to insert into the message, e.g. "Title is required".
18
+ * @property {boolean} server-generated if true, this indicates the element's HTML was generated on the server.
19
+ * This means that your CSS can target it for display in all cases. If this is not present,
20
+ * you may want to avoid showing this element if the form has not been submitted yet.
21
+ * Does not affect behavior.
18
22
  * @property {boolean} server-side if true, this indicates the element contains constraint violation messages
19
- * from the server. Currently doesn't affect this element's behavior, however
20
- * AjaxSubmit will use it to locate where it should insert server-side errors.
23
+ * from the server. Does not affect behavior.
24
+ * @property {boolean} client-side if true, this indicates the element contains constraint violation messages
25
+ * from the client, however they may have been generated from the server, since the server may
26
+ * re-evaluate the client-side constraints. Does not affect behavior of this tag.
21
27
  *
22
28
  * @see I18nTranslation
23
29
  * @see ConstraintViolationMessages
@@ -33,12 +39,15 @@ class ConstraintViolationMessage extends BaseCustomElement {
33
39
  "key",
34
40
  "input-name",
35
41
  "server-side",
42
+ "client-side",
43
+ "server-generated",
36
44
  ]
37
45
 
38
46
  static createElement(document,attributes) {
39
47
  const element = document.createElement(ConstraintViolationMessage.tagName)
40
48
  element.setAttribute("key",this.i18nKey("cs", attributes.key))
41
49
  element.setAttribute("input-name",attributes["input-name"])
50
+ element.setAttribute("client-side","")
42
51
  if (Object.hasOwn(attributes,"show-warnings")) {
43
52
  element.setAttribute("show-warnings",attributes["show-warnings"])
44
53
  }
@@ -85,6 +94,12 @@ class ConstraintViolationMessage extends BaseCustomElement {
85
94
  serverSideChangedCallback({newValueAsBoolean}) {
86
95
  // attribute listed for documentation purposes only
87
96
  }
97
+ clientSideChangedCallback({newValueAsBoolean}) {
98
+ // attribute listed for documentation purposes only
99
+ }
100
+ serverGeneratedChangedCallback({newValueAsBoolean}) {
101
+ // attribute listed for documentation purposes only
102
+ }
88
103
 
89
104
  update() {
90
105
  if (!this.#key) {
data/brut-js/src/Form.js CHANGED
@@ -15,10 +15,7 @@ import ConstraintViolationMessages from "./ConstraintViolationMessages"
15
15
  * set `submitted-invalid` on itself when that happens, thus allowing you to target invalid
16
16
  * fields only after a submission attempt.
17
17
  * * You may wish to control the messaging of client-side constraint violations
18
- * beyond what the browser gives you. Assuming your `INPUT` tags are inside a container
19
- * like `LABEL`, a `brut-cv` tag found in that container
20
- * (i.e. a sibling of your `INPUT`) will be modified to contain error messages specific
21
- * to the {@link external:ValidityState} of the control.
18
+ * beyond what the browser gives you. Assuming you have generated a `<brut-cv-messages input-name="«input name»"></brut-cv-messasges>`, it will be populated with `<brut-cv>` elements for each client-side constraint violation, based on the {@link external:ValidityState} of the control.
22
19
  *
23
20
  * @fires brut:invalid Fired when any element is found to be invalid
24
21
  * @fires brut:valid Fired when no element is found to be invalid. This should be reliable to know
@@ -29,12 +26,12 @@ import ConstraintViolationMessages from "./ConstraintViolationMessages"
29
26
  * <form ...>
30
27
  * <label>
31
28
  * <input type="text" required name="username">
32
- * <brut-cv-messages>
29
+ * <brut-cv-messages input-name="username">
33
30
  * </brut-cv-messages>
34
31
  * </label>
35
32
  * <div> <!-- container need not be a label -->
36
33
  * <input type="text" required minlength="4" name="alias">
37
- * <brut-cv-messages>
34
+ * <brut-cv-messages input-name="alias">
38
35
  * </brut-cv-messages>
39
36
  * </div>
40
37
  * <button>Submit</button>
@@ -45,13 +42,13 @@ import ConstraintViolationMessages from "./ConstraintViolationMessages"
45
42
  * <form ...>
46
43
  * <label>
47
44
  * <input type="text" required name="username">
48
- * <brut-cv-messages>
45
+ * <brut-cv-messages input-name="username">
49
46
  * <brut-cv>This field is required</brut-cv>
50
47
  * </brut-cv-messages>
51
48
  * </label>
52
49
  * <div> <!-- container need not be a label -->
53
50
  * <input type="text" required minlength="4" name="alias">
54
- * <brut-cv-messages>
51
+ * <brut-cv-messages input-name="alias">
55
52
  * <brut-cv>This field is required</brut-cv>
56
53
  * </brut-cv-messages>
57
54
  * </div>
@@ -107,28 +104,27 @@ class Form extends BaseCustomElement {
107
104
 
108
105
  #updateErrorMessages(event) {
109
106
  const element = event.target
110
- const selector = ConstraintViolationMessages.tagName
111
- let errorLabels = element.parentNode.querySelectorAll(selector)
112
- if (errorLabels.length == 0) {
113
- if (element.name && element.form) {
114
- const moreGeneralSelector = `${ConstraintViolationMessages.tagName}[input-name='${element.name}']`
115
- errorLabels = element.form.querySelectorAll(moreGeneralSelector)
116
- if (errorLabels.length == 0) {
117
- this.logger.warn(`Did not find any elements matching ${selector} or ${moreGeneralSelector}, so no error messages will be shown`)
118
- }
107
+ let constraintViolationMessages = []
108
+ if (element.name && element.form) {
109
+ const selector = `${ConstraintViolationMessages.tagName}[input-name='${element.name}']`
110
+ constraintViolationMessages = element.form.querySelectorAll(selector)
111
+ if (constraintViolationMessages.length == 0) {
112
+ this.logger.warn(`Did not find any elements matching ${selector}, so no error messages will be shown`)
113
+ }
114
+ }
115
+ else {
116
+ if (element.name) {
117
+ this.logger.warn("Element has a name (%s), but is not associated with any form.", element.name)
119
118
  }
120
119
  else {
121
- this.logger.warn("Did not find any elements matching %s and the form element has %s %s",
122
- selector,
123
- element.name ? "no name" : "a name, but",
124
- element.form ? "no form" : "though has a form")
120
+ this.logger.warn("Element has a form, but has no name, which means we cannot locate %s by input-name", ConstraintViolationMessages.tagName)
125
121
  }
126
122
  }
127
- if (errorLabels.length == 0) {
123
+ if (constraintViolationMessages.length == 0) {
128
124
  return
129
125
  }
130
126
  let anyErrors = false
131
- errorLabels.forEach( (errorLabel) => {
127
+ constraintViolationMessages.forEach( (errorLabel) => {
132
128
  if (element.validity.valid) {
133
129
  errorLabel.clearClientSideMessages()
134
130
  }
@@ -130,6 +130,7 @@ export default defineConfig({
130
130
  collapsed: true,
131
131
  items: [
132
132
  { text: "Migration Basics", link: "/recipes/migrations" },
133
+ { text: "Styling Form Errors", link: "/recipes/form-errors" },
133
134
  { text: "Authentication", link: "/recipes/authentication" },
134
135
  { text: "Alternate Layouts", link: "/recipes/alternate-layouts" },
135
136
  { text: "Blank Layouts", link: "/recipes/blank-layouts" },
@@ -120,7 +120,7 @@ inputs `ValidityState`. That may look like so:
120
120
  ```html {3}
121
121
  <input type="text" name="name" required minlength="3">
122
122
  <brut-cv-messages input-name="name">
123
- <brut-cv input-name="name" key="rangeUnderflow"></brut-cv>
123
+ <brut-cv input-name="name" client-side key="rangeUnderflow"></brut-cv>
124
124
  </brut-cv-messages>
125
125
  ```
126
126
 
@@ -139,7 +139,7 @@ this HTML:
139
139
  ```html {4}
140
140
  <input type="text" name="name" required minlength="3">
141
141
  <brut-cv-messages input-name="name">
142
- <brut-cv input-name="name" key="rangeUnderflow">
142
+ <brut-cv input-name="name" client-side key="rangeUnderflow">
143
143
  This field is too short
144
144
  </brut-cv>
145
145
  </brut-cv-messages>
@@ -154,13 +154,13 @@ violation, the same general markup is generated:
154
154
  ```html {3,4}
155
155
  <input type="text" name="name" required minlength="3">
156
156
  <brut-cv-messages input-name="name">
157
- <brut-cv server-side>
157
+ <brut-cv server-generated server-side>
158
158
  This name has already been taken.
159
159
  </brut-cv>
160
160
  </brut-cv-messages>
161
161
  ```
162
162
 
163
- The `server-side` attribute is set, which can help with CSS targeting.
163
+ The `server-genereted` and `server-side` attributes is set, which can help with CSS targeting.
164
164
 
165
165
  The *last* piece of this puzzle is a solution for the issue where forms that have
166
166
  not yet been submitted are considered to have invalid values by the browser.
@@ -177,7 +177,7 @@ This might lead to HTML like so:
177
177
 
178
178
  <input type="text" name="name" required minlength="3">
179
179
  <brut-cv-messages input-name="name">
180
- <brut-cv input-name="name" key="rangeUnderflow">
180
+ <brut-cv input-name="name" client-side key="rangeUnderflow">
181
181
  This field is too short
182
182
  </brut-cv>
183
183
  </brut-cv-messages>
@@ -199,7 +199,7 @@ brut-cv {
199
199
  /* brut-cv inside a submitted-invalid
200
200
  OR brut-cv from the server ARE shown */
201
201
  brut-form[submitted-invalid] brut-cv,
202
- brut-cv[server-side] {
202
+ brut-cv[server-generated] {
203
203
  display: block;
204
204
  color: red; /* e.g. */
205
205
  }
@@ -0,0 +1,148 @@
1
+ # Styling Form Errors
2
+
3
+ Brut makes it as easy as possible to unify client-side and server-side constraint violation handling, including
4
+ how you style those messages.
5
+
6
+ ## Requirements
7
+
8
+ What you want:
9
+
10
+ * When a form is rendered for the first time, there should be errors shown.
11
+ * When a visitor interacts with a form before submissions, no errors are shown.
12
+ * When the form is submitted, client-side constraint violations should be shown.
13
+ * When JavaScript is circumvented and the form is submitted with client-side constraint violations, the form should be re-generated, showing those violations the same is if JavaScripts was *not* circumvented
14
+ * When there are no client-side constraint violations, but there *are* server-side violations, the form should be re-generated, showing those violations the same is if JavaScripts was *not* circumvented
15
+
16
+ This can be achieved through CSS.
17
+
18
+ ## Recipe
19
+
20
+ ### Create Pages and HTML
21
+
22
+ First, create a form and handler:
23
+
24
+ ```
25
+ bin/scaffold form /new_widget
26
+ ```
27
+
28
+ Edit `app/src/front_end/forms/new_widget_form.rb`
29
+
30
+ ```ruby
31
+ class NewWidgetForm < AppForm
32
+ input :name, minlength: 3
33
+ input :description
34
+ end
35
+ ```
36
+
37
+ Now, implement the handler in `app/src/front_end/handlers/new_widget_handler.rb` to check for client-side violations *and* require that the description have at
38
+ least 5 words in it.
39
+
40
+ ```ruby
41
+ class NewWidgetHandler < AppHandler
42
+ def initialize(form:)
43
+ @form = form
44
+ end
45
+
46
+ def handle
47
+ if @form.valid?
48
+ if @form.description.split(/\s+/).length < 5
49
+ @form.server_side_constraint_violation(
50
+ input_name: :description,
51
+ key: :not_enough_words
52
+ )
53
+ end
54
+
55
+ if @form.constraint_violations?
56
+ NewWidgetPage.new(form: @form)
57
+ else
58
+ redirect_to(HomePage)
59
+ end
60
+ end
61
+ end
62
+ ```
63
+
64
+ Add the new error message to `app/config/i18n/en/2_app.rb`
65
+
66
+ ```ruby {6}
67
+ {
68
+ en: {
69
+ nevermind: "Nevermind",
70
+ cv: {
71
+ ss: {
72
+ not_enough_words: "%{field} must have at least %{minwords} words",
73
+ },
74
+ # ...
75
+ ```
76
+
77
+ Now, build a minimal `NewWidgetPage`:
78
+
79
+ ```
80
+ bin/scaffold page /new_widget`
81
+ ```
82
+
83
+ We'll create the bare minimum in `app/src/front_end/pages/new_widget_page.rb`
84
+
85
+ ```ruby
86
+ class NewWidgetPage < AppPage
87
+ def initialize(form: nil)
88
+ @form = form || NewWidgetForm.new
89
+ end
90
+
91
+ def page_template
92
+ brut_form do
93
+ FormTag(for: @form) do
94
+ label do
95
+ Inputs::InputTag(form: @form, input_name: :name)
96
+ ConstraintViolations(form: @form, input_name: :name)
97
+ span { "Name" }
98
+ end
99
+ label do
100
+ Inputs::TextareaTag(form: @form, input_name: :name)
101
+ ConstraintViolations(form: @form, input_name: :name)
102
+ span { "Description" }
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ ```
109
+
110
+ ### Create CSS
111
+
112
+ The most minimal CSS would be as followed, which you can place in `app/src/front_end/css/index.css`
113
+
114
+ ```css
115
+ brut-cv {
116
+ display: none;
117
+ }
118
+
119
+ brut-form[submitted-invalid] brut-cv,
120
+ brut-cv[server-generated] {
121
+ display: block;
122
+ color: red;
123
+ }
124
+ ```
125
+
126
+ This will show the messages in `<brut-cv>` *only* if:
127
+
128
+ * Form submission was attempted (`submitted-invalid` would be set on `<brut-form>`)
129
+ * The server generated via `ConstraintViolations` (only if the handler was triggered)
130
+
131
+ Because more than one `<brut-cv>` could be generated or inserted, you may want to style the
132
+ `<brut-cv-messages>` that contains them, but you only want it to show up if it has contents.
133
+
134
+ You can't just do `brut-cv-messages:has(brut-cv)`, because your container would show up before the form
135
+ submission was attempted.
136
+
137
+ Here is what you want instead. This will put the error messages in a red box:
138
+
139
+ ```css
140
+ brut-form[submitted-invalid] brut-cv-messages:has(brut-cv),
141
+ brut-cv-messages:has(brut-cv[server-generated]) {
142
+ color: red;
143
+ background-color: pink;
144
+ border: solid thin red;
145
+ border-radius: 1rem;
146
+ }
147
+ ```
148
+
data/docs/404.html CHANGED
@@ -9,7 +9,7 @@
9
9
  <link rel="preload stylesheet" href="/assets/style.B1z60PPQ.css" as="style">
10
10
  <link rel="preload stylesheet" href="/vp-icons.css" as="style">
11
11
 
12
- <script type="module" src="/assets/app.BuBdZoUg.js"></script>
12
+ <script type="module" src="/assets/app.vjGWMSnJ.js"></script>
13
13
  <link rel="icon" href="/favicon.ico">
14
14
  <meta property="og:title" content="BrutRB Documentation">
15
15
  <meta property="og:type" content="website">
@@ -20,7 +20,7 @@
20
20
  </head>
21
21
  <body>
22
22
  <div id="app"></div>
23
- <script>window.__VP_HASH_MAP__=JSON.parse("{\"adrs.md\":\"BxjHi9-8\",\"ai.md\":\"Cy9GWnER\",\"assets.md\":\"7C3HWkga\",\"brut-js.md\":\"B4GYxQVw\",\"business-logic.md\":\"BY4hGy0m\",\"cli.md\":\"CjsktgFz\",\"components.md\":\"Bu80E2Nr\",\"configuration.md\":\"CuIxVsSf\",\"css.md\":\"CltvJqAa\",\"custom-element-tests.md\":\"B_rbta32\",\"database-access.md\":\"gnluu54N\",\"database-schema.md\":\"LpmBPVEU\",\"deployment.md\":\"BLseERGV\",\"dev-environment.md\":\"DRH2D2-O\",\"dir-structure.md\":\"CWir1pic\",\"doc-conventions.md\":\"DOkAuXlt\",\"end-to-end-tests.md\":\"DzqRpZ43\",\"features.md\":\"DPFXsy0z\",\"flash-and-session.md\":\"nPvUpnUx\",\"form-constraints.md\":\"DK5adCgM\",\"forms.md\":\"DnLbzVDa\",\"getting-started.md\":\"DdQLmU3C\",\"handlers.md\":\"h84MMB1R\",\"hooks.md\":\"Jmb5VOLA\",\"i18n.md\":\"BAm9t9JJ\",\"index.md\":\"Bn9e0sRJ\",\"instrumentation.md\":\"BgcaGVYH\",\"javascript.md\":\"DzrMxUmI\",\"jobs.md\":\"S-2amAYp\",\"keyword-injection.md\":\"95Zgh2eN\",\"layouts.md\":\"CVGl9xIO\",\"lsp.md\":\"Dn1rIiW0\",\"markdown-examples.md\":\"CCFEQO44\",\"middleware.md\":\"Czz_UlJN\",\"overview.md\":\"DlKiRRG_\",\"pages.md\":\"B7Hc-i6H\",\"recipes_alternate-layouts.md\":\"BwEytl59\",\"recipes_authentication.md\":\"Dzvi_g69\",\"recipes_blank-layouts.md\":\"fyAUJyJR\",\"recipes_custom-flash.md\":\"CrQbI5eH\",\"recipes_indexed-forms.md\":\"CstYyOSo\",\"recipes_migrations.md\":\"CTcnWDJF\",\"recipes_text-field-component.md\":\"H4wLAK0Z\",\"roadmap.md\":\"C6PRi0DX\",\"routes.md\":\"BD6y2i-f\",\"security.md\":\"C0G_AZR-\",\"seed-data.md\":\"BvFZlqIk\",\"space-time-continuum.md\":\"xl44xDos\",\"tutorial.md\":\"C4zR5XPG\",\"unit-tests.md\":\"DUGrnLj5\",\"why.md\":\"C-hk5xgJ\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"Brut RB\",\"description\":\"Documentation for the Brut.RB web framework.\",\"base\":\"/\",\"head\":[],\"router\":{\"prefetchLinks\":true},\"appearance\":true,\"themeConfig\":{\"search\":{\"provider\":\"local\"},\"nav\":[{\"text\":\"Home\",\"link\":\"/\"},{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Overview\",\"link\":\"/overview\"},{\"text\":\"Brut API\",\"link\":\"/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutCSS\",\"link\":\"/brut-css/index.html\",\"target\":\"_self\"}],\"outline\":[2,3],\"sidebar\":[{\"text\":\"Overview\",\"collapsed\":false,\"items\":[{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Concepts\",\"link\":\"/overview\"},{\"text\":\"Features\",\"link\":\"/features\"},{\"text\":\"Directory Structure\",\"link\":\"/dir-structure\"},{\"text\":\"Dev Environment\",\"link\":\"/dev-environment\"},{\"text\":\"Tutorial\",\"link\":\"/tutorial\"},{\"text\":\"Documentation Conventions\",\"link\":\"/doc-conventions\"}]},{\"text\":\"Front-End\",\"collapsed\":false,\"items\":[{\"text\":\"Routes\",\"link\":\"/routes\"},{\"text\":\"Pages\",\"link\":\"/pages\"},{\"text\":\"Layouts\",\"link\":\"/layouts\"},{\"text\":\"Forms\",\"link\":\"/forms\"},{\"text\":\"Form Constraints\",\"link\":\"/form-constraints\"},{\"text\":\"Handlers and Actions\",\"link\":\"/handlers\"},{\"text\":\"Components\",\"link\":\"/components\"},{\"text\":\"Flash and Session\",\"link\":\"/flash-and-session\"},{\"text\":\"Space/Time Continuum\",\"link\":\"/space-time-continuum\"},{\"text\":\"JavaScript\",\"link\":\"/javascript\"},{\"text\":\"CSS\",\"link\":\"/css\"},{\"text\":\"Assets\",\"link\":\"/assets\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js\"}]},{\"text\":\"Back-End\",\"collapsed\":false,\"items\":[{\"text\":\"Database Schema\",\"link\":\"/database-schema\"},{\"text\":\"Database Access\",\"link\":\"/database-access\"},{\"text\":\"Seed Data\",\"link\":\"/seed-data\"},{\"text\":\"Jobs\",\"link\":\"/jobs\"},{\"text\":\"Business Logic\",\"link\":\"/business-logic\"}]},{\"text\":\"Framework\",\"collapsed\":false,\"items\":[{\"text\":\"Configuration\",\"link\":\"/configuration\"},{\"text\":\"Keyword Injection\",\"link\":\"/keyword-injection\"},{\"text\":\"I18n\",\"link\":\"/i18n\"},{\"text\":\"CLI / Tasks\",\"link\":\"/cli\"},{\"text\":\"Deployment\",\"link\":\"/deployment\"}]},{\"text\":\"Testing\",\"collapsed\":false,\"items\":[{\"text\":\"Unit Tests\",\"link\":\"/unit-tests\"},{\"text\":\"End-to-End Tests\",\"link\":\"/end-to-end-tests\"},{\"text\":\"Testing Custom Elements\",\"link\":\"/custom-element-tests\"}]},{\"text\":\"Advanced Topics\",\"collapsed\":true,\"items\":[{\"text\":\"Route Hooks\",\"link\":\"/hooks\"},{\"text\":\"Middleware\",\"link\":\"/middleware\"},{\"text\":\"Instrumentation\",\"link\":\"/instrumentation\"},{\"text\":\"Security\",\"link\":\"/security\"},{\"text\":\"LSP Support\",\"link\":\"/lsp\"}]},{\"text\":\"Recipes\",\"collapsed\":true,\"items\":[{\"text\":\"Migration Basics\",\"link\":\"/recipes/migrations\"},{\"text\":\"Authentication\",\"link\":\"/recipes/authentication\"},{\"text\":\"Alternate Layouts\",\"link\":\"/recipes/alternate-layouts\"},{\"text\":\"Blank Layouts\",\"link\":\"/recipes/blank-layouts\"},{\"text\":\"Custom Flash Class\",\"link\":\"/recipes/custom-flash\"},{\"text\":\"Indexed Form Elements\",\"link\":\"/recipes/indexed-forms\"},{\"text\":\"Text Field Component\",\"link\":\"/recipes/text-field-component\"}]},{\"text\":\"Meta\",\"collapsed\":false,\"items\":[{\"text\":\"Why?!\",\"link\":\"/why\"},{\"text\":\"ADRs\",\"link\":\"/adrs\"},{\"text\":\"Roadmap to 1.0\",\"link\":\"/roadmap\"},{\"text\":\"AI Declaration\",\"link\":\"/ai\"}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/thirdtank/brut\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":false}");</script>
23
+ <script>window.__VP_HASH_MAP__=JSON.parse("{\"adrs.md\":\"BxjHi9-8\",\"ai.md\":\"Cy9GWnER\",\"assets.md\":\"7C3HWkga\",\"brut-js.md\":\"B4GYxQVw\",\"business-logic.md\":\"BY4hGy0m\",\"cli.md\":\"CjsktgFz\",\"components.md\":\"B543a3Lm\",\"configuration.md\":\"-foE_iVv\",\"css.md\":\"CltvJqAa\",\"custom-element-tests.md\":\"B_rbta32\",\"database-access.md\":\"gnluu54N\",\"database-schema.md\":\"LpmBPVEU\",\"deployment.md\":\"BLseERGV\",\"dev-environment.md\":\"DRH2D2-O\",\"dir-structure.md\":\"CWir1pic\",\"doc-conventions.md\":\"DOkAuXlt\",\"end-to-end-tests.md\":\"DzqRpZ43\",\"features.md\":\"DPFXsy0z\",\"flash-and-session.md\":\"nPvUpnUx\",\"form-constraints.md\":\"DK5adCgM\",\"forms.md\":\"D5-2rgHh\",\"getting-started.md\":\"Cd4XSZb_\",\"handlers.md\":\"h84MMB1R\",\"hooks.md\":\"Jmb5VOLA\",\"i18n.md\":\"BAm9t9JJ\",\"index.md\":\"Bn9e0sRJ\",\"instrumentation.md\":\"BgcaGVYH\",\"javascript.md\":\"DzrMxUmI\",\"jobs.md\":\"S-2amAYp\",\"keyword-injection.md\":\"95Zgh2eN\",\"layouts.md\":\"CVGl9xIO\",\"lsp.md\":\"Dn1rIiW0\",\"markdown-examples.md\":\"CCFEQO44\",\"middleware.md\":\"Czz_UlJN\",\"overview.md\":\"DlKiRRG_\",\"pages.md\":\"B7Hc-i6H\",\"recipes_alternate-layouts.md\":\"BwEytl59\",\"recipes_authentication.md\":\"Dzvi_g69\",\"recipes_blank-layouts.md\":\"fyAUJyJR\",\"recipes_custom-flash.md\":\"CrQbI5eH\",\"recipes_indexed-forms.md\":\"CstYyOSo\",\"recipes_migrations.md\":\"CTcnWDJF\",\"recipes_text-field-component.md\":\"H4wLAK0Z\",\"roadmap.md\":\"C6PRi0DX\",\"routes.md\":\"BD6y2i-f\",\"security.md\":\"C0G_AZR-\",\"seed-data.md\":\"BvFZlqIk\",\"space-time-continuum.md\":\"xl44xDos\",\"tutorial.md\":\"C4zR5XPG\",\"unit-tests.md\":\"DUGrnLj5\",\"why.md\":\"C-hk5xgJ\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"Brut RB\",\"description\":\"Documentation for the Brut.RB web framework.\",\"base\":\"/\",\"head\":[],\"router\":{\"prefetchLinks\":true},\"appearance\":true,\"themeConfig\":{\"search\":{\"provider\":\"local\"},\"nav\":[{\"text\":\"Home\",\"link\":\"/\"},{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Overview\",\"link\":\"/overview\"},{\"text\":\"Brut API\",\"link\":\"/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutCSS\",\"link\":\"/brut-css/index.html\",\"target\":\"_self\"}],\"outline\":[2,3],\"sidebar\":[{\"text\":\"Overview\",\"collapsed\":false,\"items\":[{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Concepts\",\"link\":\"/overview\"},{\"text\":\"Features\",\"link\":\"/features\"},{\"text\":\"Directory Structure\",\"link\":\"/dir-structure\"},{\"text\":\"Dev Environment\",\"link\":\"/dev-environment\"},{\"text\":\"Tutorial\",\"link\":\"/tutorial\"},{\"text\":\"Documentation Conventions\",\"link\":\"/doc-conventions\"}]},{\"text\":\"Front-End\",\"collapsed\":false,\"items\":[{\"text\":\"Routes\",\"link\":\"/routes\"},{\"text\":\"Pages\",\"link\":\"/pages\"},{\"text\":\"Layouts\",\"link\":\"/layouts\"},{\"text\":\"Forms\",\"link\":\"/forms\"},{\"text\":\"Form Constraints\",\"link\":\"/form-constraints\"},{\"text\":\"Handlers and Actions\",\"link\":\"/handlers\"},{\"text\":\"Components\",\"link\":\"/components\"},{\"text\":\"Flash and Session\",\"link\":\"/flash-and-session\"},{\"text\":\"Space/Time Continuum\",\"link\":\"/space-time-continuum\"},{\"text\":\"JavaScript\",\"link\":\"/javascript\"},{\"text\":\"CSS\",\"link\":\"/css\"},{\"text\":\"Assets\",\"link\":\"/assets\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js\"}]},{\"text\":\"Back-End\",\"collapsed\":false,\"items\":[{\"text\":\"Database Schema\",\"link\":\"/database-schema\"},{\"text\":\"Database Access\",\"link\":\"/database-access\"},{\"text\":\"Seed Data\",\"link\":\"/seed-data\"},{\"text\":\"Jobs\",\"link\":\"/jobs\"},{\"text\":\"Business Logic\",\"link\":\"/business-logic\"}]},{\"text\":\"Framework\",\"collapsed\":false,\"items\":[{\"text\":\"Configuration\",\"link\":\"/configuration\"},{\"text\":\"Keyword Injection\",\"link\":\"/keyword-injection\"},{\"text\":\"I18n\",\"link\":\"/i18n\"},{\"text\":\"CLI / Tasks\",\"link\":\"/cli\"},{\"text\":\"Deployment\",\"link\":\"/deployment\"}]},{\"text\":\"Testing\",\"collapsed\":false,\"items\":[{\"text\":\"Unit Tests\",\"link\":\"/unit-tests\"},{\"text\":\"End-to-End Tests\",\"link\":\"/end-to-end-tests\"},{\"text\":\"Testing Custom Elements\",\"link\":\"/custom-element-tests\"}]},{\"text\":\"Advanced Topics\",\"collapsed\":true,\"items\":[{\"text\":\"Route Hooks\",\"link\":\"/hooks\"},{\"text\":\"Middleware\",\"link\":\"/middleware\"},{\"text\":\"Instrumentation\",\"link\":\"/instrumentation\"},{\"text\":\"Security\",\"link\":\"/security\"},{\"text\":\"LSP Support\",\"link\":\"/lsp\"}]},{\"text\":\"Recipes\",\"collapsed\":true,\"items\":[{\"text\":\"Migration Basics\",\"link\":\"/recipes/migrations\"},{\"text\":\"Authentication\",\"link\":\"/recipes/authentication\"},{\"text\":\"Alternate Layouts\",\"link\":\"/recipes/alternate-layouts\"},{\"text\":\"Blank Layouts\",\"link\":\"/recipes/blank-layouts\"},{\"text\":\"Custom Flash Class\",\"link\":\"/recipes/custom-flash\"},{\"text\":\"Indexed Form Elements\",\"link\":\"/recipes/indexed-forms\"},{\"text\":\"Text Field Component\",\"link\":\"/recipes/text-field-component\"}]},{\"text\":\"Meta\",\"collapsed\":false,\"items\":[{\"text\":\"Why?!\",\"link\":\"/why\"},{\"text\":\"ADRs\",\"link\":\"/adrs\"},{\"text\":\"Roadmap to 1.0\",\"link\":\"/roadmap\"},{\"text\":\"AI Declaration\",\"link\":\"/ai\"}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/thirdtank/brut\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":false}");</script>
24
24
 
25
25
  </body>
26
26
  </html>