brut 0.10.0 → 0.11.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 (376) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/Gemfile.lock +15 -15
  4. data/assets/YouTubeThumb.pxd +0 -0
  5. data/bin/new-version +3 -3
  6. data/brut-css/package-lock.json +2 -2
  7. data/brut-css/package.json +1 -1
  8. data/brut-js/package-lock.json +2 -2
  9. data/brut-js/package.json +1 -1
  10. data/brutrb.com/getting-started.md +3 -0
  11. data/brutrb.com/images/tutorial/02-confirmation-dialog-browser-element-styled.png +0 -0
  12. data/brutrb.com/images/tutorial/02-confirmation-dialog-browser-element.png +0 -0
  13. data/brutrb.com/images/tutorial/02-confirmation-dialog-browser.png +0 -0
  14. data/brutrb.com/images/tutorial/02-confirmation-flow.graffle +0 -0
  15. data/brutrb.com/images/tutorial/02-confirmation-flow.png +0 -0
  16. data/brutrb.com/instrumentation.md +142 -3
  17. data/brutrb.com/tutorial.md +29 -1627
  18. data/brutrb.com/tutorials/01-intro.md +1630 -0
  19. data/brutrb.com/tutorials/02-dialog.md +569 -0
  20. data/docs/404.html +2 -2
  21. data/docs/adrs.html +4 -4
  22. data/docs/ai.html +4 -4
  23. data/docs/api/Brut/BackEnd/SeedData.html +1 -1
  24. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +1 -1
  25. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +1 -1
  26. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +1 -1
  27. data/docs/api/Brut/BackEnd/Sidekiq.html +1 -1
  28. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +1 -1
  29. data/docs/api/Brut/BackEnd/Validators.html +1 -1
  30. data/docs/api/Brut/BackEnd.html +1 -1
  31. data/docs/api/Brut/CLI/App.html +1 -1
  32. data/docs/api/Brut/CLI/AppRunner.html +1 -1
  33. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +1 -1
  34. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +1 -1
  35. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +1 -1
  36. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +1 -1
  37. data/docs/api/Brut/CLI/Apps/BuildAssets.html +1 -1
  38. data/docs/api/Brut/CLI/Apps/DB/Create.html +1 -1
  39. data/docs/api/Brut/CLI/Apps/DB/Drop.html +1 -1
  40. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +1 -1
  41. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +1 -1
  42. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +1 -1
  43. data/docs/api/Brut/CLI/Apps/DB/Seed.html +1 -1
  44. data/docs/api/Brut/CLI/Apps/DB/Status.html +1 -1
  45. data/docs/api/Brut/CLI/Apps/DB.html +1 -1
  46. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +1 -1
  47. data/docs/api/Brut/CLI/Apps/DeployBase.html +1 -1
  48. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +1 -1
  49. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +1 -1
  50. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
  51. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +1 -1
  52. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +1 -1
  53. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +1 -1
  54. data/docs/api/Brut/CLI/Apps/Scaffold/DbModel.html +1 -1
  55. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +1 -1
  56. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +1 -1
  57. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
  58. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +1 -1
  59. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +1 -1
  60. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +1 -1
  61. data/docs/api/Brut/CLI/Apps/Scaffold.html +1 -1
  62. data/docs/api/Brut/CLI/Apps/Test/Audit.html +1 -1
  63. data/docs/api/Brut/CLI/Apps/Test/E2e.html +1 -1
  64. data/docs/api/Brut/CLI/Apps/Test/JS.html +1 -1
  65. data/docs/api/Brut/CLI/Apps/Test/Run.html +1 -1
  66. data/docs/api/Brut/CLI/Apps/Test.html +1 -1
  67. data/docs/api/Brut/CLI/Apps.html +1 -1
  68. data/docs/api/Brut/CLI/Command.html +1 -1
  69. data/docs/api/Brut/CLI/Error.html +1 -1
  70. data/docs/api/Brut/CLI/ExecutionResults/Result.html +1 -1
  71. data/docs/api/Brut/CLI/ExecutionResults.html +1 -1
  72. data/docs/api/Brut/CLI/Executor.html +1 -1
  73. data/docs/api/Brut/CLI/InvalidOption.html +1 -1
  74. data/docs/api/Brut/CLI/Options.html +1 -1
  75. data/docs/api/Brut/CLI/Output.html +1 -1
  76. data/docs/api/Brut/CLI/SystemExecError.html +1 -1
  77. data/docs/api/Brut/CLI.html +1 -1
  78. data/docs/api/Brut/FactoryBot.html +1 -1
  79. data/docs/api/Brut/Framework/App.html +1 -1
  80. data/docs/api/Brut/Framework/Config.html +1 -1
  81. data/docs/api/Brut/Framework/Container.html +1 -1
  82. data/docs/api/Brut/Framework/Error.html +1 -1
  83. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +1 -1
  84. data/docs/api/Brut/Framework/Errors/Bug.html +1 -1
  85. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +1 -1
  86. data/docs/api/Brut/Framework/Errors/MissingParameter.html +1 -1
  87. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +1 -1
  88. data/docs/api/Brut/Framework/Errors/NotFound.html +1 -1
  89. data/docs/api/Brut/Framework/Errors/NotImplemented.html +1 -1
  90. data/docs/api/Brut/Framework/Errors.html +1 -1
  91. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +1 -1
  92. data/docs/api/Brut/Framework/MCP.html +1 -1
  93. data/docs/api/Brut/Framework/ProjectEnvironment.html +1 -1
  94. data/docs/api/Brut/Framework.html +1 -1
  95. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +1 -1
  96. data/docs/api/Brut/FrontEnd/Component/Helpers.html +1 -1
  97. data/docs/api/Brut/FrontEnd/Component.html +1 -1
  98. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +48 -27
  99. data/docs/api/Brut/FrontEnd/Components/FormTag.html +1 -1
  100. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +1 -1
  101. data/docs/api/Brut/FrontEnd/Components/Input.html +1 -1
  102. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +1 -1
  103. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +1 -1
  104. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +1 -1
  105. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +1 -1
  106. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +1 -1
  107. data/docs/api/Brut/FrontEnd/Components/Inputs.html +1 -1
  108. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +1 -1
  109. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +1 -1
  110. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +1 -1
  111. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +1 -1
  112. data/docs/api/Brut/FrontEnd/Components.html +1 -1
  113. data/docs/api/Brut/FrontEnd/CsrfProtector.html +1 -1
  114. data/docs/api/Brut/FrontEnd/Download.html +1 -1
  115. data/docs/api/Brut/FrontEnd/Flash.html +1 -1
  116. data/docs/api/Brut/FrontEnd/Form.html +1 -1
  117. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +1 -1
  118. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +1 -1
  119. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +1 -1
  120. data/docs/api/Brut/FrontEnd/Forms/Input.html +1 -1
  121. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +1 -1
  122. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1 -1
  123. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +1 -1
  124. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
  125. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +1 -1
  126. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
  127. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +1 -1
  128. data/docs/api/Brut/FrontEnd/Forms.html +1 -1
  129. data/docs/api/Brut/FrontEnd/GenericResponse.html +1 -1
  130. data/docs/api/Brut/FrontEnd/Handler.html +1 -1
  131. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +1 -1
  132. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +1 -1
  133. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +1 -1
  134. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +1 -1
  135. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +1 -1
  136. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +1 -1
  137. data/docs/api/Brut/FrontEnd/Handlers.html +1 -1
  138. data/docs/api/Brut/FrontEnd/HandlingResults.html +1 -1
  139. data/docs/api/Brut/FrontEnd/HttpMethod.html +1 -1
  140. data/docs/api/Brut/FrontEnd/HttpStatus.html +1 -1
  141. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +1 -1
  142. data/docs/api/Brut/FrontEnd/Layout.html +1 -1
  143. data/docs/api/Brut/FrontEnd/Middleware.html +1 -1
  144. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +1 -1
  145. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +1 -1
  146. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +1 -1
  147. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +1 -1
  148. data/docs/api/Brut/FrontEnd/Middlewares.html +1 -1
  149. data/docs/api/Brut/FrontEnd/Page.html +1 -1
  150. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +1 -1
  151. data/docs/api/Brut/FrontEnd/Pages.html +1 -1
  152. data/docs/api/Brut/FrontEnd/RequestContext.html +1 -1
  153. data/docs/api/Brut/FrontEnd/RouteHook.html +1 -1
  154. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +1 -1
  155. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +1 -1
  156. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +1 -1
  157. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +1 -1
  158. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +1 -1
  159. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +1 -1
  160. data/docs/api/Brut/FrontEnd/RouteHooks.html +1 -1
  161. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +1 -1
  162. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +1 -1
  163. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +1 -1
  164. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +1 -1
  165. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +1 -1
  166. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +1 -1
  167. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +1 -1
  168. data/docs/api/Brut/FrontEnd/Routing/Route.html +1 -1
  169. data/docs/api/Brut/FrontEnd/Routing.html +1 -1
  170. data/docs/api/Brut/FrontEnd/Session.html +1 -1
  171. data/docs/api/Brut/FrontEnd.html +1 -1
  172. data/docs/api/Brut/I18n/BaseMethods.html +1 -1
  173. data/docs/api/Brut/I18n/ForBackEnd.html +1 -1
  174. data/docs/api/Brut/I18n/ForCLI.html +1 -1
  175. data/docs/api/Brut/I18n/ForHTML.html +1 -1
  176. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +1 -1
  177. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +1 -1
  178. data/docs/api/Brut/I18n.html +1 -1
  179. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +1 -1
  180. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +1 -1
  181. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +1 -1
  182. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +1 -1
  183. data/docs/api/Brut/Instrumentation.html +1 -1
  184. data/docs/api/Brut/RubocopConfig.html +1 -1
  185. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +1 -1
  186. data/docs/api/Brut/SinatraHelpers.html +1 -1
  187. data/docs/api/Brut/SpecSupport/ClockSupport.html +1 -1
  188. data/docs/api/Brut/SpecSupport/ComponentSupport.html +1 -1
  189. data/docs/api/Brut/SpecSupport/E2ETestServer.html +1 -1
  190. data/docs/api/Brut/SpecSupport/E2eSupport.html +1 -1
  191. data/docs/api/Brut/SpecSupport/EnhancedNode.html +1 -1
  192. data/docs/api/Brut/SpecSupport/FlashSupport.html +1 -1
  193. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +1 -1
  194. data/docs/api/Brut/SpecSupport/GeneralSupport.html +1 -1
  195. data/docs/api/Brut/SpecSupport/HandlerSupport.html +1 -1
  196. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +1 -1
  197. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +1 -1
  198. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +1 -1
  199. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +1 -1
  200. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +1 -1
  201. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +1 -1
  202. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +1 -1
  203. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +1 -1
  204. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +1 -1
  205. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +1 -1
  206. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +1 -1
  207. data/docs/api/Brut/SpecSupport/Matchers.html +1 -1
  208. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +1 -1
  209. data/docs/api/Brut/SpecSupport/RSpecSetup.html +1 -1
  210. data/docs/api/Brut/SpecSupport/SessionSupport.html +1 -1
  211. data/docs/api/Brut/SpecSupport.html +1 -1
  212. data/docs/api/Brut.html +1 -1
  213. data/docs/api/Clock.html +1 -1
  214. data/docs/api/ModuleName.html +1 -1
  215. data/docs/api/RichString.html +1 -1
  216. data/docs/api/SemanticLogger/Appender/Async.html +1 -1
  217. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +1 -1
  218. data/docs/api/Sequel/Extensions/BrutMigrations.html +1 -1
  219. data/docs/api/Sequel/Extensions.html +1 -1
  220. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +1 -1
  221. data/docs/api/Sequel/Plugins/CreatedAt.html +1 -1
  222. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +1 -1
  223. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +1 -1
  224. data/docs/api/Sequel/Plugins/ExternalId.html +1 -1
  225. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +1 -1
  226. data/docs/api/Sequel/Plugins/FindBang.html +1 -1
  227. data/docs/api/Sequel/Plugins.html +1 -1
  228. data/docs/api/Sequel.html +1 -1
  229. data/docs/api/_index.html +1 -1
  230. data/docs/api/file.README.html +1 -1
  231. data/docs/api/index.html +1 -1
  232. data/docs/api/top-level-namespace.html +1 -1
  233. data/docs/assets/02-confirmation-dialog-browser-element-styled.3NEGM20-.png +0 -0
  234. data/docs/assets/02-confirmation-dialog-browser-element.DPsf0xUW.png +0 -0
  235. data/docs/assets/02-confirmation-dialog-browser.DH8ALFO4.png +0 -0
  236. data/docs/assets/02-confirmation-flow.D9gZ0S5U.png +0 -0
  237. data/docs/assets/{app.vjGWMSnJ.js → app.0-aKXKdt.js} +1 -1
  238. data/docs/assets/chunks/@localSearchIndexroot.DPhqaz1b.js +1 -0
  239. data/docs/assets/chunks/{VPLocalSearchBox.C-ymMW2k.js → VPLocalSearchBox.CW-UBkNA.js} +1 -1
  240. data/docs/assets/chunks/{theme.pJUosGlI.js → theme.a6feKWJO.js} +2 -2
  241. data/docs/assets/{components.md.B543a3Lm.js → components.md.BzVRwegp.js} +3 -3
  242. data/docs/assets/{configuration.md.-foE_iVv.js → configuration.md.eM5wFVi5.js} +1 -1
  243. data/docs/assets/{form-constraints.md.DK5adCgM.js → form-constraints.md.KTv5cdR4.js} +6 -6
  244. data/docs/assets/{forms.md.D5-2rgHh.js → forms.md.B3BHvCV3.js} +1 -1
  245. data/docs/assets/{getting-started.md.Cd4XSZb_.js → getting-started.md.BgR0ZHsl.js} +6 -3
  246. data/docs/assets/{getting-started.md.Cd4XSZb_.lean.js → getting-started.md.BgR0ZHsl.lean.js} +1 -1
  247. data/docs/assets/recipes_form-errors.md.Bv5RCKqH.js +66 -0
  248. data/docs/assets/recipes_form-errors.md.Bv5RCKqH.lean.js +1 -0
  249. data/docs/assets/tutorial.md.BM40jnoq.js +27 -0
  250. data/docs/assets/tutorial.md.BM40jnoq.lean.js +1 -0
  251. data/docs/assets/{tutorial.md.C4zR5XPG.js → tutorials_01-intro.md.BXvYWcO9.js} +5 -25
  252. data/docs/assets/tutorials_01-intro.md.BXvYWcO9.lean.js +1 -0
  253. data/docs/assets/tutorials_02-dialog.md.CIeg8R--.js +274 -0
  254. data/docs/assets/tutorials_02-dialog.md.CIeg8R--.lean.js +1 -0
  255. data/docs/assets.html +4 -4
  256. data/docs/brut-js/api/AjaxSubmit.html +1 -1
  257. data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
  258. data/docs/brut-js/api/Autosubmit.html +1 -1
  259. data/docs/brut-js/api/Autosubmit.js.html +1 -1
  260. data/docs/brut-js/api/BaseCustomElement.html +1 -1
  261. data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
  262. data/docs/brut-js/api/BrutCustomElements.html +1 -1
  263. data/docs/brut-js/api/BufferedLogger.html +1 -1
  264. data/docs/brut-js/api/ConfirmSubmit.html +1 -1
  265. data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
  266. data/docs/brut-js/api/ConfirmationDialog.html +1 -1
  267. data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
  268. data/docs/brut-js/api/ConstraintViolationMessage.html +55 -5
  269. data/docs/brut-js/api/ConstraintViolationMessage.js.html +18 -3
  270. data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
  271. data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
  272. data/docs/brut-js/api/CopyToClipboard.html +1 -1
  273. data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
  274. data/docs/brut-js/api/Form.html +7 -10
  275. data/docs/brut-js/api/Form.js.html +20 -24
  276. data/docs/brut-js/api/I18nTranslation.html +1 -1
  277. data/docs/brut-js/api/I18nTranslation.js.html +1 -1
  278. data/docs/brut-js/api/LocaleDetection.html +1 -1
  279. data/docs/brut-js/api/LocaleDetection.js.html +1 -1
  280. data/docs/brut-js/api/Logger.html +1 -1
  281. data/docs/brut-js/api/Logger.js.html +1 -1
  282. data/docs/brut-js/api/Message.html +1 -1
  283. data/docs/brut-js/api/Message.js.html +1 -1
  284. data/docs/brut-js/api/PrefixedLogger.html +1 -1
  285. data/docs/brut-js/api/RichString.html +1 -1
  286. data/docs/brut-js/api/RichString.js.html +1 -1
  287. data/docs/brut-js/api/Tabs.html +1 -1
  288. data/docs/brut-js/api/Tabs.js.html +1 -1
  289. data/docs/brut-js/api/Tracing.html +1 -1
  290. data/docs/brut-js/api/Tracing.js.html +1 -1
  291. data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
  292. data/docs/brut-js/api/external-Performance.html +1 -1
  293. data/docs/brut-js/api/external-Promise.html +1 -1
  294. data/docs/brut-js/api/external-ValidityState.html +1 -1
  295. data/docs/brut-js/api/external-Window.html +1 -1
  296. data/docs/brut-js/api/external-fetch.html +1 -1
  297. data/docs/brut-js/api/global.html +1 -1
  298. data/docs/brut-js/api/index.html +1 -1
  299. data/docs/brut-js/api/index.js.html +1 -1
  300. data/docs/brut-js/api/module-testing.html +1 -1
  301. data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
  302. data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
  303. data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
  304. data/docs/brut-js/api/testing.DOMCreator.html +1 -1
  305. data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
  306. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
  307. data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
  308. data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
  309. data/docs/brut-js/api/testing_index.js.html +1 -1
  310. data/docs/brut-js.html +4 -4
  311. data/docs/business-logic.html +4 -4
  312. data/docs/cli.html +4 -4
  313. data/docs/components.html +8 -8
  314. data/docs/configuration.html +5 -5
  315. data/docs/css.html +4 -4
  316. data/docs/custom-element-tests.html +4 -4
  317. data/docs/database-access.html +4 -4
  318. data/docs/database-schema.html +4 -4
  319. data/docs/deployment.html +4 -4
  320. data/docs/dev-environment.html +4 -4
  321. data/docs/dir-structure.html +4 -4
  322. data/docs/doc-conventions.html +4 -4
  323. data/docs/end-to-end-tests.html +4 -4
  324. data/docs/features.html +4 -4
  325. data/docs/flash-and-session.html +4 -4
  326. data/docs/form-constraints.html +11 -11
  327. data/docs/forms.html +6 -6
  328. data/docs/getting-started.html +10 -7
  329. data/docs/handlers.html +4 -4
  330. data/docs/hashmap.json +1 -1
  331. data/docs/hooks.html +4 -4
  332. data/docs/i18n.html +4 -4
  333. data/docs/index.html +3 -3
  334. data/docs/instrumentation.html +4 -4
  335. data/docs/javascript.html +4 -4
  336. data/docs/jobs.html +4 -4
  337. data/docs/keyword-injection.html +4 -4
  338. data/docs/layouts.html +4 -4
  339. data/docs/lsp.html +4 -4
  340. data/docs/markdown-examples.html +4 -4
  341. data/docs/middleware.html +4 -4
  342. data/docs/overview.html +4 -4
  343. data/docs/pages.html +4 -4
  344. data/docs/recipes/alternate-layouts.html +4 -4
  345. data/docs/recipes/authentication.html +5 -5
  346. data/docs/recipes/blank-layouts.html +4 -4
  347. data/docs/recipes/custom-flash.html +4 -4
  348. data/docs/recipes/form-errors.html +94 -0
  349. data/docs/recipes/indexed-forms.html +4 -4
  350. data/docs/recipes/migrations.html +5 -5
  351. data/docs/recipes/text-field-component.html +4 -4
  352. data/docs/roadmap.html +4 -4
  353. data/docs/routes.html +4 -4
  354. data/docs/security.html +4 -4
  355. data/docs/seed-data.html +4 -4
  356. data/docs/space-time-continuum.html +4 -4
  357. data/docs/tutorial.html +12 -713
  358. data/docs/tutorials/01-intro.html +736 -0
  359. data/docs/tutorials/02-dialog.html +302 -0
  360. data/docs/unit-tests.html +4 -4
  361. data/docs/why.html +4 -4
  362. data/lib/brut/instrumentation/methods.rb +153 -0
  363. data/lib/brut/instrumentation/open_telemetry.rb +1 -0
  364. data/lib/brut/instrumentation.rb +1 -0
  365. data/lib/brut/version.rb +1 -1
  366. data/mkbrut/Gemfile.lock +1 -1
  367. data/mkbrut/bin/publish +1 -1
  368. data/mkbrut/lib/mkbrut/version.rb +1 -1
  369. data/specs/brut/instrumentation/methods.spec.rb +399 -0
  370. metadata +39 -17
  371. data/docs/assets/chunks/@localSearchIndexroot.Dn1xGMv_.js +0 -1
  372. data/docs/assets/tutorial.md.C4zR5XPG.lean.js +0 -1
  373. /data/docs/assets/{components.md.B543a3Lm.lean.js → components.md.BzVRwegp.lean.js} +0 -0
  374. /data/docs/assets/{configuration.md.-foE_iVv.lean.js → configuration.md.eM5wFVi5.lean.js} +0 -0
  375. /data/docs/assets/{form-constraints.md.DK5adCgM.lean.js → form-constraints.md.KTv5cdR4.lean.js} +0 -0
  376. /data/docs/assets/{forms.md.D5-2rgHh.lean.js → forms.md.B3BHvCV3.lean.js} +0 -0
@@ -0,0 +1,569 @@
1
+ # Tutorial: Styled Confirmation Dialog
2
+
3
+ For actions that can't be undone, it's customary to confirm with the visitor that they are sure they
4
+ want to take that action. Brut provides support for this. You can use `window.confirm` or create your
5
+ own styled `<dialog>` that Brut will use. Both approaches don't require writing any JavaScript
6
+ yourself.
7
+
8
+ [You can watching this as a screencast instead](https://video.hardlimit.com/w/4y8Pjd8VVPDK372mozCUdj).
9
+
10
+ ## Set Up
11
+
12
+ If you haven't followed the [initial tutorial](/tutorials/01-intro), you'll need to pull down the blog
13
+ app so you have a place to work.
14
+
15
+ 1. [Install Docker](https://docker.com)
16
+
17
+ > [!TIP]
18
+ > If you are on Windows, we *highly* recommend you use the
19
+ > Windows Subystem for Linux (WSL2), as this makes Brut, web developement,
20
+ > and, honestly, your entire life as you know it, far easier than trying to
21
+ > get things working natively in Windows.
22
+
23
+ 2. Clone the `blog-demo` repo (**don't use Codespaces as it is not supported**):
24
+
25
+ ::: code-group
26
+ ```bash [Terminal]
27
+ git clone git@github.com:thirdtank/blog-demo.git
28
+ ```
29
+
30
+ ```bash [GitHub CLI]
31
+ gh repo clone thirdtank/blog-demo
32
+ ```
33
+ :::
34
+
35
+ 3. `cd` to what you just cloned.
36
+
37
+ ```bash
38
+ cd blog-demo
39
+ ```
40
+
41
+ 4. Create a branch named `confirmation-dialog` off of the `02-confirmation-dialog/start` branch:
42
+
43
+ ```bash
44
+ git checkout -b confirmation-dialog 02-confirmation-dialog/start
45
+ ```
46
+ 5. Build your development image.
47
+
48
+ ```bash
49
+ dx/build
50
+ ```
51
+
52
+ 6. Start the environment, which will pull down Postgres and otel-desktop-viewer
53
+
54
+ ```bash
55
+ dx/start
56
+ ```
57
+
58
+ 7. In another terminal window, "log in" to your dev environment (note that you can use your editor on your computer to edit code)
59
+
60
+ ```bash
61
+ dx/exec bash
62
+ ```
63
+
64
+ 8. Set up and run tests to make sure things are working before you start making changes. Note, this is
65
+ **inside the container**, not directly on your computer.
66
+
67
+ ```bash
68
+ bin/setup
69
+ bin/ci
70
+ ```
71
+
72
+ ## What We're Doing
73
+
74
+ When writing a blog post, if the title and content satisfy all constraints, the post is saved and shown
75
+ on the home page. Because this can't currently be undone, we want the user to confirm the posting, just
76
+ to avoid any accidents.
77
+
78
+ Initially, we will use `window.confirm` to do this. After that, we'll create a nicely styled dialog to
79
+ do the confirmation. While this will require that the browser execute JavaScript, we won't be writing any. We'll use Brut-provided Web
80
+ Components to do this.
81
+
82
+ ![Diagram showing the flow, with a screenshot of the blog post editor on the left, and a pink arrow from
83
+ the 'Post it' button going to the text 'Are You Sure?'. From there, a pink line labeled 'No' goes back
84
+ to the editor, while a pink line labeled 'Yes' goes to a screenshot of the home page showing the blog
85
+ post.](/images/tutorial/02-confirmation-flow.png)
86
+
87
+ ## Initial Version Using `window.confirm`
88
+
89
+ Brut includes an [autonomous custom
90
+ element](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements) named
91
+ `<brut-confirm-submit>`. This element wraps an existing submit button and intercepts its form
92
+ submission to ask for confirmation. If confirmation is granted, the form is submitted. If not, it's not.
93
+
94
+ It is used on a per-button basis, which gives you flexibility in handling what buttons do what within
95
+ the form. It *only* works on `<button>` and `<input type="submit">` elements.
96
+
97
+ ```html
98
+ <form ...>
99
+ <input ...>
100
+ <brut-confirm-submit message="You sure?">
101
+ <button>Submit</button> <!-- if clicked, confirmation is requested -->
102
+ </brut-confirm-submit>
103
+ <button>Also Submit</button> <!-- if clicked, form is submitted -->
104
+ </form>
105
+ ```
106
+
107
+ ### Adding Confirmation to Blog Posting
108
+
109
+ We can use it on `BlogPostEditorPage`. Open up `app/src/front_end/pages/blog_post_editor_page.rb` and
110
+ make this change toward the end of `page_template`
111
+
112
+ ```ruby:line-numbers=21 {1-3,5}
113
+ brut_confirm_submit(
114
+ message: "This will post immediately to the home page"
115
+ ) do
116
+ button { t([:form,:post]) }
117
+ end
118
+ ```
119
+
120
+ The method `brut_confirm_submit` is provided by Phlex due to a call to
121
+ [`register_element`](https://www.phlex.fun/sgml/html-elements.html#custom-elements) in
122
+ `Brut::FrontEnd::Component`.
123
+
124
+ Now, start up your server using `bin/dev`:
125
+
126
+ ```bash
127
+ bin/dev
128
+ ```
129
+
130
+ ```txt
131
+ # OUTPUT
132
+ « LOTS OF OUTPUT »
133
+ 15:50:10 startup_message.1 | Your app is now running at
134
+ 15:50:10 startup_message.1 |
135
+ 15:50:10 startup_message.1 | http://localhost:6502
136
+ 15:50:10 startup_message.1 |
137
+ ```
138
+
139
+ Open `http://localhost:6502` in your browser, then click "Write New Blog Post", write a valid post and click "Post It". You
140
+ should see the browser's `window.confirm` show up with the value for `message:` as the message.
141
+
142
+ ![Screenshot showing the browser's builtin confirmation dialog](/images/tutorial/02-confirmation-dialog-browser.png)
143
+
144
+ Click "Cancel" and the dialog goes away and nothing is posted. Click "Post It" again, then click "OK", and the post goes through as normal.
145
+
146
+ Even though we are going to build our own dialog, let's keep our end-to-end test working.
147
+
148
+ ### Interacting with `window.confirm` in End-to-End Tests
149
+
150
+ Let's start by seeing how the test fails:
151
+
152
+ ```bash
153
+ bin/test e2e
154
+ ```
155
+
156
+ ```txt {20-21,33}
157
+ # OUTPUT
158
+ > bin/test e2e
159
+ [ bin/test ] Rebuilding test database schema
160
+ [ bin/test ] Executing ["bin/db rebuild --env=test"]
161
+ [ bin/db ] Database exists. Dropping...
162
+ [ bin/db ] blog_test does not exit. Creating...
163
+ [ bin/db ] Migrations applied
164
+ [ bin/test ] ["bin/db rebuild --env=test"] succeeded
165
+ [ bin/test ] Running all tests
166
+ [ bin/test ] Executing ["bin/rspec -I /Users/davec/Projects/ThirdTank/blog-demo/specs -I /Users/davec/Projects/ThirdTank/blog-demo/app/src -I lib/ --tag e2e -P \"**/*.spec.rb\" /Users/davec/Projects/ThirdTank/blog-demo/specs/"]
167
+
168
+ «TONS OF OUTPUT»
169
+
170
+ Failures:
171
+
172
+ 1) We can post a new blog post allows posting a post
173
+ Failure/Error: expect(content_error_message).to have_text("This field does not have enough words")
174
+
175
+ /Users/davec/Projects/ThirdTank/blog-demo/local-gems/gem-home/gems/playwright-ruby-client-1.52.0/lib/playwright/locator_assertions_impl.rb:53:in 'Playwright::LocatorAssertionsImpl#expect_impl': (Playwright::AssertionError)
176
+ Locator expected to have text 'This field does not have enough words'
177
+ Actual value <element(s) not found>
178
+ Call log:
179
+ - locator#Playwright::Locator#expect with timeout 5000ms
180
+ - waiting for locator("brut-cv-messages[input-name='content'] brut-cv")
181
+ from /Users/davec/Projects/ThirdTank/blog-demo/local-gems/gem-home/gems/playwright-ruby-client-1.52.0/lib/playwright/locator_assertions_impl.rb:397:in 'Playwright::LocatorAssertionsImpl#to_have_text'
182
+
183
+ «MASSIVE STACK TRACE»
184
+
185
+ from /Users/davec/Projects/ThirdTank/blog-demo/local-gems/gem-home/gems/rspec-core-3.13.5/lib/rspec/core/runner.rb:45:in 'RSpec::Core::Runner.invoke'
186
+ from /Users/davec/Projects/ThirdTank/blog-demo/local-gems/gem-home/gems/rspec-core-3.13.5/exe/rspec:4:in '<top (required)>'
187
+ from bin/rspec:16:in 'Kernel#load'
188
+ from bin/rspec:16:in '<main>'
189
+ # ./specs/e2e/home_page.spec.rb:34:in 'block (2 levels) in <top (required)>'
190
+
191
+ «MASSIVE STACK TRACE»
192
+
193
+ # ./local-gems/gem-home/gems/brut-0.5.0/lib/brut/spec_support/rspec_setup.rb:129:in 'block in Brut::SpecSupport::RSpecSetup#setup!'
194
+
195
+ Finished in 7.6 seconds (files took 0.7169 seconds to load)
196
+ 1 example, 1 failure
197
+
198
+ Failed examples:
199
+
200
+ bin/test run ./specs/e2e/home_page.spec.rb:4 # We can post a new blog post allows posting a post
201
+
202
+ Randomized with seed 25427
203
+
204
+ [ bin/test ] error: ["bin/rspec -I /Users/davec/Projects/ThirdTank/blog-demo/specs -I /Users/davec/Projects/ThirdTank/blog-demo/app/src -I lib/ --tag e2e -P \"**/*.spec.rb\" /Users/davec/Projects/ThirdTank/blog-demo/specs/"] failed - exited 1
205
+ ```
206
+
207
+ I've highlighted the relevant parts. Playwright loves stack traces and obtuse errors.
208
+
209
+ Let's look at line 34 of `specs/e2e/home_page.spec.rb`:
210
+
211
+ ```ruby {11}
212
+ expect(title_error_message).to have_text("This field is too short")
213
+ expect(content_error_message).to have_text("This field is required")
214
+
215
+ title_field.fill("New blog post")
216
+ content_field.fill("Too short")
217
+
218
+ submit_button.click
219
+
220
+ expect(page).to be_page_for(BlogPostEditorPage)
221
+
222
+ expect(content_error_message).to have_text("This field does not have enough words")
223
+
224
+ content_field.fill("This is a longer post, so we should be OK")
225
+
226
+ submit_button.click
227
+ expect(page).to be_page_for(HomePage)
228
+ ```
229
+
230
+ The test was expecting to hit the server and re-generate the page with a server-side error message.
231
+ Although `<brut-confirm-submit>` did not pop up when there were client-side constraint violations, it
232
+ doesn't know there are server-side ones, so it is waiting for us to confirm the submission.
233
+
234
+ Playwright will [automatically dismiss any browser-based dialogs](https://playwright.dev/docs/dialogs).
235
+ To handle them, our test will need to register a handler. To do this with Ruby, we'll call `page.on`
236
+ and given it an event name and a block to handle the event.
237
+
238
+ The event name is "dialog" and a Playwright `Dialog` will be passed. We can call `accept` on that.
239
+
240
+ Here's the change. Note the line numbers for reference in the file. You want to set this up before
241
+ `submit_button.click` is called.
242
+
243
+ ```ruby:line-numbers=28 {3-6}
244
+ content_field.fill("Too short")
245
+
246
+ accept_dialog = ->(dialog) {
247
+ dialog.accept
248
+ }
249
+ page.on("dialog",accept_dialog)
250
+
251
+ submit_button.click
252
+ ```
253
+
254
+ Note that this configuration will stay in effect for the rest of the test. That means when we later save
255
+ the blog post, it will accept the dialog.
256
+
257
+ Now, `bin/test e2e` should pass:
258
+
259
+ ```bash
260
+ bin/test e2e
261
+ ```
262
+
263
+ ```txt {14}
264
+ #OUTPUT
265
+ [ bin/test ] Rebuilding test database schema
266
+ [ bin/test ] Executing ["bin/db rebuild --env=test"]
267
+ [ bin/db ] Database exists. Dropping...
268
+ [ bin/db ] blog_test does not exit. Creating...
269
+ [ bin/db ] Migrations applied
270
+ [ bin/test ] ["bin/db rebuild --env=test"] succeeded
271
+ [ bin/test ] Running all tests
272
+ [ bin/test ] Executing ["bin/rspec -I /Users/davec/Projects/ThirdTank/blog-demo/specs -I /Users/davec/Projects/ThirdTank/blog-demo/app/src -I lib/ --tag e2e -P \"**/*.spec.rb\" /Users/davec/Projects/ThirdTank/blog-demo/specs/"]
273
+
274
+ «TONS OF OUTPUT»
275
+
276
+ Finished in 3.57 seconds (files took 0.7341 seconds to load)
277
+ 1 example, 0 failures
278
+
279
+ Randomized with seed 1445
280
+
281
+ [ bin/test ] ["bin/rspec -I /Users/davec/Projects/ThirdTank/blog-demo/specs -I /Users/davec/Projects/ThirdTank/blog-demo/app/src -I lib/ --tag e2e -P \"**/*.spec.rb\" /Users/davec/Projects/ThirdTank/blog-demo/specs/"] succeeded
282
+ [ bin/test ] Re-Rebuilding test database schema
283
+ [ bin/test ] Executing ["bin/db rebuild --env=test"]
284
+ [ bin/db ] Database exists. Dropping...
285
+ [ bin/db ] blog_test does not exit. Creating...
286
+ [ bin/db ] Migrations applied
287
+ [ bin/test ] ["bin/db rebuild --env=test"] succeeded
288
+ ```
289
+
290
+ `window.confirm` is great in a pinch, but we'd like to use our own styled dialog if possible.
291
+
292
+ ## Using a Styled Dialog
293
+
294
+ The [`<dialog>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog) element
295
+ has been available since 2022 and provides some of what we'll need to confirm a blog post. Brut
296
+ can enhance a `<dialog>` to act as a confirmation dialog by using the `<brut-confirmation-dialog>`
297
+ custom element.
298
+
299
+ Like `<brut-confirm-submit>`, it wraps an element and enhances it. To work, the `<dialog>` must include
300
+ certain elements to represent the message, a button for consent, and a button for denial.
301
+
302
+ Let's see it in action.
303
+
304
+ ### Creating a Styled Dialog
305
+
306
+ Edit `app/src/front_end/pages/blog_post_editor_page.rb` and add the dialog at the end of `page_template`:
307
+
308
+ ```ruby:line-numbers=21 {8-16}
309
+ brut_confirm_submit(
310
+ message: "This will post immediately to the home page"
311
+ ) do
312
+ button { t([:form,:post]) }
313
+ end
314
+ end
315
+ end
316
+ brut_confirmation_dialog do
317
+ dialog do
318
+ h1
319
+ div do
320
+ button(value:"ok")
321
+ button(value:"cancel") { "Don't Publish" }
322
+ end
323
+ end
324
+ end
325
+ ```
326
+
327
+ Your browser should provide a default visual style for the dialog (that is terrible), but you can see
328
+ that `<brut-confirm-submit>` will now use it when you click "Post It":
329
+
330
+ ![Screenshot showing the browser's styling of a dialog element](/images/tutorial/02-confirmation-dialog-browser-element.png)
331
+
332
+ `<brut-confirm-submit>` and `<brut-confirmation-dialog>` work together to allow you to style these
333
+ dialog how you'd like. It expects an `h1` element inside where the message will go. It expects a
334
+ `<button value="ok">` that, when clicked, indicates the visitor is accepting the dialog. A `<button
335
+ value="cancel">` should also be present that, when clicked, indicates the visitor wants to abort and not
336
+ submit the form.
337
+
338
+ If you've never worked with a `<dialog>` before, it can be handy to set `open` on the element so it
339
+ shows up without having to click something to open it. It doesn't show exactly as it would when we use
340
+ JavaScript to show it, but it's good enough to get your styling work done:
341
+
342
+ ```ruby
343
+ dialog(open: true) do
344
+ # ...
345
+ end
346
+ ```
347
+
348
+ Here's the CSS I chose. Add this to `app/src/front_end/css/index.css`, inside the `.BlogPostEditorPage` block:
349
+
350
+ ```css:line-numbers=59 {7-44}
351
+ cursor: pointer;
352
+ &:hover {
353
+ background-color: #ACFFAC;
354
+ }
355
+ }
356
+ }
357
+ brut-confirmation-dialog dialog {
358
+ border-radius: 1rem;
359
+ border-width: 0;
360
+ box-shadow: rgb(200, 200, 200) 1px 1px 12.72px 3.46892px;
361
+ background-color: white;
362
+ padding: 1rem;
363
+ h1 {
364
+ color: black;
365
+ font-size: 2rem;
366
+ }
367
+ div {
368
+ width: 100%;
369
+ display: flex;
370
+ gap: 0.25rem;
371
+ align-items: center;
372
+ justify-content: space-between;
373
+ button {
374
+ padding-left: 2rem;
375
+ padding-right: 2rem;
376
+ padding-top: 1rem;
377
+ padding-bottom: 1rem;
378
+ border-radius: 1rem;
379
+ font-size: 150%;
380
+ align-self: end;
381
+ cursor: pointer;
382
+ &[value="ok"] {
383
+ background-color: #E5FFE5;
384
+ border: solid thin #006300;
385
+ color: #006300;
386
+ }
387
+ &[value="cancel"] {
388
+ background-color: #FFE5E5;
389
+ border: solid thin #630000;
390
+ color: #630000;
391
+ }
392
+ }
393
+ }
394
+ }
395
+ }
396
+ brut-cv {
397
+ display: none;
398
+ color: #A60053;
399
+ ```
400
+
401
+ Now, reload the page and click "Post It". You should see a somewhat nicer dialog:
402
+
403
+
404
+ ![Screenshot showing the our styling of a dialog element](/images/tutorial/02-confirmation-dialog-browser-element-styled.png)
405
+
406
+ And, sure enough if you click "Don't Publish", the dialog clears and nothing happens. If you click
407
+ "Post It!", it submits the form.
408
+
409
+ A few notes on how this works:
410
+
411
+ * The contents of the `<h1>` come from the `message` attribute of the **`<brut-confirm-submit>`**. This
412
+ allows you to re-use the confirmation dialog for other purposes.
413
+ * The content of the `<button value="ok" ...>` is the same as the button wrapped by
414
+ `<brut-confirm-submit>`.
415
+
416
+ Also note how the use of semantic and standard HTML allows us to style the elements without classes or
417
+ `data-` tags.
418
+
419
+ Let's look back at our tests.
420
+
421
+ ### Interacting with Our Dialog in Tests
422
+
423
+ Run our end-to-end test:
424
+
425
+ ```bash
426
+ bin/test e2e
427
+ ```
428
+
429
+ It should fail:
430
+
431
+ ```txt {18,19,33}
432
+ #OUTPUT
433
+ > bin/test e2e
434
+ [ bin/test ] Rebuilding test database schema
435
+ [ bin/test ] Executing ["bin/db rebuild --env=test"]
436
+ [ bin/db ] Database exists. Dropping...
437
+ [ bin/db ] blog_test does not exit. Creating...
438
+ [ bin/db ] Migrations applied
439
+ [ bin/test ] ["bin/db rebuild --env=test"] succeeded
440
+
441
+ «TONS OF OUTPUT»
442
+
443
+ Failures:
444
+
445
+ 1) We can post a new blog post allows posting a post
446
+ Failure/Error: expect(content_error_message).to have_text("This field does not have enough words")
447
+
448
+ /Users/davec/Projects/ThirdTank/blog-demo/local-gems/gem-home/gems/playwright-ruby-client-1.52.0/lib/playwright/locator_assertions_impl.rb:53:in 'Playwright::LocatorAssertionsImpl#expect_impl': (Playwright::AssertionError)
449
+ Locator expected to have text 'This field does not have enough words'
450
+ Actual value <element(s) not found>
451
+ Call log:
452
+ - locator#Playwright::Locator#expect with timeout 5000ms
453
+ - waiting for locator("brut-cv-messages[input-name='content'] brut-cv")
454
+ from /Users/davec/Projects/ThirdTank/blog-demo/local-gems/gem-home/gems/playwright-ruby-client-1.52.0/lib/playwright/locator_assertions_impl.rb:397:in 'Playwright::LocatorAssertionsImpl#to_have_text'
455
+ from /Users/davec/Projects/ThirdTank/blog-demo/local-gems/gem-home/gems/playwright-ruby-client-1.52.0/lib/playwright_api/locator_assertions.rb:642:in 'Playwright::LocatorAssertions#to_have_text'
456
+
457
+ «HUGE STACK TRACE»
458
+
459
+ from /Users/davec/Projects/ThirdTank/blog-demo/local-gems/gem-home/gems/rspec-core-3.13.5/lib/rspec/core/runner.rb:45:in 'RSpec::Core::Runner.invoke'
460
+ from /Users/davec/Projects/ThirdTank/blog-demo/local-gems/gem-home/gems/rspec-core-3.13.5/exe/rspec:4:in '<top (required)>'
461
+ from bin/rspec:16:in 'Kernel#load'
462
+ from bin/rspec:16:in '<main>'
463
+ # ./specs/e2e/home_page.spec.rb:39:in 'block (2 levels) in <top (required)>'
464
+
465
+ «HUGE STACK TRACE»
466
+
467
+ # ./local-gems/gem-home/gems/brut-0.5.0/lib/brut/spec_support/rspec_setup.rb:185:in 'Brut::SpecSupport::RSpecSetup::OptionalSidekiqSupport#disable_sidekiq_testing'
468
+ # ./local-gems/gem-home/gems/brut-0.5.0/lib/brut/spec_support/rspec_setup.rb:129:in 'block in Brut::SpecSupport::RSpecSetup#setup!'
469
+
470
+ Finished in 8.31 seconds (files took 0.66944 seconds to load)
471
+ 1 example, 1 failure
472
+
473
+ Failed examples:
474
+
475
+ bin/test run ./specs/e2e/home_page.spec.rb:4 # We can post a new blog post allows posting a post
476
+
477
+ Randomized with seed 29349
478
+
479
+ [ bin/test ] error: ["bin/rspec -I /Users/davec/Projects/ThirdTank/blog-demo/specs -I /Users/davec/Projects/ThirdTank/blog-demo/app/src -I lib/ --tag e2e -P \"**/*.spec.rb\" /Users/davec/Projects/ThirdTank/blog-demo/specs/"] failed - exited 1
480
+ ```
481
+
482
+ Line 39 is the same line that failed when we first added the confirmation. Since Playwright interacts
483
+ with browser dialogs via an event, the event listener we added is never fired, so our error is simply
484
+ that the page didn't refresh.
485
+
486
+ Let's remove the listener and instead interact with the new dialog. We should click "cancel" to make
487
+ sure it doens't do anything, then click "ok".
488
+
489
+ One problem with Playwright (well, with web pages in general) is that it's not easy to assert that
490
+ something didn't happen or isn't there. We can't click the cancel button, then assert that there is no
491
+ error message.
492
+
493
+ Instead, we'll assert that the dialog is not being shown.
494
+
495
+ To do that, we'll locate the dialog, the ok button, and the cancel button. The assertion that the
496
+ dialog isn't shown requires accessing the JavaScript `open` property and checking that it's false. The
497
+ rest of the test works as before, punctuated with calls to `dialog_ok_button.click` to accept the
498
+ dialog.
499
+
500
+ ```ruby:line-numbers=25 {6-8,12,13,15,16,25}
501
+ expect(content_error_message).to have_text("This field is required")
502
+
503
+ title_field.fill("New blog post")
504
+ content_field.fill("Too short")
505
+
506
+ dialog = page.locator("brut-confirmation-dialog dialog")
507
+ dialog_ok_button = page.locator("brut-confirmation-dialog button[value='ok']")
508
+ dialog_cancel_button = page.locator("brut-confirmation-dialog button[value='cancel']")
509
+
510
+ submit_button.click
511
+
512
+ dialog_cancel_button.click
513
+ expect(dialog).to have_js_property(:open,false)
514
+
515
+ submit_button.click
516
+ dialog_ok_button.click
517
+
518
+ expect(page).to be_page_for(BlogPostEditorPage)
519
+
520
+ expect(content_error_message).to have_text("This field does not have enough words")
521
+
522
+ content_field.fill("This is a longer post, so we should be OK")
523
+
524
+ submit_button.click
525
+ dialog_ok_button.click
526
+ expect(page).to be_page_for(HomePage)
527
+
528
+ new_post = DB::BlogPost.order(Sequel.desc(:created_at)).first
529
+ ```
530
+
531
+ The test should now pass:
532
+
533
+ ```bash
534
+ bin/test e2e
535
+ ```
536
+
537
+ ```txt {15}
538
+ #OUTPUT
539
+ [ bin/test ] Rebuilding test database schema
540
+ [ bin/test ] Executing ["bin/db rebuild --env=test"]
541
+ [ bin/db ] Database exists. Dropping...
542
+ [ bin/db ] blog_test does not exit. Creating...
543
+ [ bin/db ] Migrations applied
544
+ [ bin/test ] ["bin/db rebuild --env=test"] succeeded
545
+
546
+ «TONS OF OUTPUT»
547
+
548
+ [7215] - Goodbye!
549
+ [7215] - Gracefully shutting down workers...
550
+
551
+ Finished in 3.45 seconds (files took 0.71481 seconds to load)
552
+ 1 example, 0 failures
553
+
554
+ Randomized with seed 30988
555
+
556
+ [ bin/test ] ["bin/rspec -I /Users/davec/Projects/ThirdTank/blog-demo/specs -I /Users/davec/Projects/ThirdTank/blog-demo/app/src -I lib/ --tag e2e -P \"**/*.spec.rb\" /Users/davec/Projects/ThirdTank/blog-demo/specs/"] succeeded
557
+ [ bin/test ] Re-Rebuilding test database schema
558
+ [ bin/test ] Executing ["bin/db rebuild --env=test"]
559
+ [ bin/db ] Database exists. Dropping...
560
+ [ bin/db ] blog_test does not exit. Creating...
561
+ [ bin/db ] Migrations applied
562
+ [ bin/test ] ["bin/db rebuild --env=test"] succeeded
563
+ ```
564
+
565
+ ## Areas for Self-Exploration
566
+
567
+ * Extract the dialog into its own component
568
+ * Use Internationalization for all the dialog values
569
+
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.vjGWMSnJ.js"></script>
12
+ <script type="module" src="/assets/app.0-aKXKdt.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\":\"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>
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\":\"BzVRwegp\",\"configuration.md\":\"eM5wFVi5\",\"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\":\"KTv5cdR4\",\"forms.md\":\"B3BHvCV3\",\"getting-started.md\":\"BgR0ZHsl\",\"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_form-errors.md\":\"Bv5RCKqH\",\"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\":\"BM40jnoq\",\"tutorials_01-intro.md\":\"BXvYWcO9\",\"tutorials_02-dialog.md\":\"CIeg8R--\",\"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\":\"Styling Form Errors\",\"link\":\"/recipes/form-errors\"},{\"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>