plutonium 0.62.2 → 0.64.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 (256) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +44 -0
  3. data/.claude/skills/plutonium-app/SKILL.md +3 -3
  4. data/.claude/skills/plutonium-async-interactions/SKILL.md +191 -0
  5. data/.claude/skills/plutonium-auth/SKILL.md +36 -0
  6. data/.claude/skills/plutonium-behavior/SKILL.md +121 -24
  7. data/.claude/skills/plutonium-kanban/SKILL.md +17 -3
  8. data/.claude/skills/plutonium-resource/SKILL.md +259 -12
  9. data/.claude/skills/plutonium-tenancy/SKILL.md +32 -3
  10. data/.claude/skills/plutonium-ui/SKILL.md +115 -14
  11. data/.claude/skills/plutonium-wizard/SKILL.md +73 -4
  12. data/.standard.yml +1 -1
  13. data/CHANGELOG.md +73 -0
  14. data/CLAUDE.md +87 -0
  15. data/Rakefile +34 -0
  16. data/SECURITY.md +1 -1
  17. data/app/assets/plutonium.css +1 -1
  18. data/app/assets/plutonium.js +685 -102
  19. data/app/assets/plutonium.js.map +4 -4
  20. data/app/assets/plutonium.min.js +53 -53
  21. data/app/assets/plutonium.min.js.map +4 -4
  22. data/app/views/rodauth/_login_form.html.erb +13 -0
  23. data/db/migrate/async_interactions/20260817000001_create_plutonium_async_runs.rb +170 -0
  24. data/docs/.vitepress/config.ts +81 -3
  25. data/docs/.vitepress/theme/blog.data.ts +44 -0
  26. data/docs/.vitepress/theme/components/BlogIndex.vue +87 -0
  27. data/docs/.vitepress/theme/components/BlogMeta.vue +47 -0
  28. data/docs/.vitepress/theme/components/HomeFeatureTour.vue +293 -0
  29. data/docs/.vitepress/theme/components/HomeHero.vue +3 -3
  30. data/docs/.vitepress/theme/components/HomeInTheBox.vue +8 -0
  31. data/docs/.vitepress/theme/components/HomeStopWriting.vue +1 -0
  32. data/docs/.vitepress/theme/components/HomeWhyPlutonium.vue +84 -0
  33. data/docs/.vitepress/theme/index.ts +8 -4
  34. data/docs/blog/association-inputs-post-signed-ids.md +70 -0
  35. data/docs/blog/fix-the-model-not-the-policy.md +122 -0
  36. data/docs/blog/fractional-ordering-runs-out-of-room.md +67 -0
  37. data/docs/blog/half-finished-forms-are-pii.md +76 -0
  38. data/docs/blog/index.md +13 -0
  39. data/docs/blog/interactions-are-presentation-objects.md +152 -0
  40. data/docs/blog/introducing-plutonium.md +253 -0
  41. data/docs/blog/jobs-are-not-permission-snapshots.md +100 -0
  42. data/docs/blog/plutonium-and-ai-agents.md +48 -0
  43. data/docs/blog/realtime-is-one-line-and-four-dependencies.md +70 -0
  44. data/docs/blog/two-forms-one-dom-id.md +69 -0
  45. data/docs/blog/whats-new-async-kanban-wizards.md +130 -0
  46. data/docs/getting-started/tutorial/04-authorization.md +12 -3
  47. data/docs/getting-started/tutorial/06-nested-resources.md +3 -1
  48. data/docs/getting-started/tutorial/07-author-portal.md +2 -2
  49. data/docs/guides/authentication.md +73 -0
  50. data/docs/guides/authorization.md +2 -0
  51. data/docs/guides/creating-packages.md +5 -3
  52. data/docs/guides/custom-actions.md +74 -12
  53. data/docs/guides/customizing-ui.md +9 -2
  54. data/docs/guides/index.md +1 -0
  55. data/docs/guides/kanban.md +7 -5
  56. data/docs/guides/nested-resources.md +11 -1
  57. data/docs/guides/performance.md +104 -0
  58. data/docs/guides/user-invites.md +1 -1
  59. data/docs/guides/wizards.md +9 -1
  60. data/docs/index.md +3 -3
  61. data/docs/public/images/home/tour-actions.png +0 -0
  62. data/docs/public/images/home/tour-async.png +0 -0
  63. data/docs/public/images/home/tour-kanban.png +0 -0
  64. data/docs/public/images/home/tour-tenancy.png +0 -0
  65. data/docs/public/images/home/tour-wizard.png +0 -0
  66. data/docs/public/images/reference/async-progress-page.png +0 -0
  67. data/docs/public/images/reference/async-running-banner.png +0 -0
  68. data/docs/public/templates/experimental.rb +34 -0
  69. data/docs/public/templates/pluton8.rb +14 -0
  70. data/docs/reference/app/portals.md +15 -3
  71. data/docs/reference/auth/accounts.md +19 -0
  72. data/docs/reference/behavior/async-interactions.md +295 -0
  73. data/docs/reference/behavior/controllers.md +17 -4
  74. data/docs/reference/behavior/index.md +7 -1
  75. data/docs/reference/behavior/interactions.md +152 -22
  76. data/docs/reference/configuration.md +5 -0
  77. data/docs/reference/index.md +1 -0
  78. data/docs/reference/kanban/dsl.md +7 -4
  79. data/docs/reference/kanban/index.md +1 -1
  80. data/docs/reference/kanban/positioning.md +26 -4
  81. data/docs/reference/positioning.md +568 -0
  82. data/docs/reference/resource/actions.md +97 -4
  83. data/docs/reference/resource/definition.md +181 -9
  84. data/docs/reference/tenancy/invites.md +1 -1
  85. data/docs/reference/tenancy/nested-resources.md +60 -2
  86. data/docs/reference/ui/assets.md +4 -0
  87. data/docs/reference/ui/components.md +57 -4
  88. data/docs/reference/ui/displays.md +20 -10
  89. data/docs/reference/ui/index.md +1 -1
  90. data/docs/reference/wizard/dsl.md +33 -0
  91. data/docs/reference/wizard/storage-config.md +1 -0
  92. data/docs/superpowers/plans/2026-07-16-homepage-depth-upgrade.md +624 -0
  93. data/docs/superpowers/plans/2026-07-16-homepage-depth-upgrade.md.tasks.json +32 -0
  94. data/docs/superpowers/plans/2026-07-31-positioned-drag-and-drop.md +1787 -0
  95. data/docs/superpowers/plans/2026-07-31-positioned-drag-and-drop.md.tasks.json +91 -0
  96. data/docs/superpowers/plans/2026-08-17-async-interactions.md +1414 -0
  97. data/docs/superpowers/plans/2026-08-17-async-interactions.md.tasks.json +66 -0
  98. data/docs/superpowers/specs/2026-07-16-homepage-depth-upgrade-design.md +111 -0
  99. data/docs/superpowers/specs/2026-07-17-action-html-attributes-design.md +124 -0
  100. data/docs/superpowers/specs/2026-07-31-positioned-drag-and-drop-design.md +506 -0
  101. data/docs/superpowers/specs/2026-08-17-async-interactions-design.md +185 -0
  102. data/gemfiles/postgres.gemfile.lock +85 -85
  103. data/gemfiles/rails_7.gemfile.lock +323 -141
  104. data/gemfiles/rails_8.0.gemfile.lock +126 -116
  105. data/gemfiles/rails_8.1.gemfile.lock +127 -117
  106. data/lib/generators/pu/async_interactions/install_generator.rb +111 -0
  107. data/lib/generators/pu/async_interactions/templates/app/controllers/async_runs_controller.rb.tt +15 -0
  108. data/lib/generators/pu/core/typespec/typespec_generator.rb +7 -4
  109. data/lib/generators/pu/invites/install_generator.rb +3 -3
  110. data/lib/generators/pu/invites/templates/packages/invites/app/views/layouts/invites/invitation.html.erb.tt +2 -2
  111. data/lib/generators/pu/lib/plutonium_generators/concerns/mounts_engines.rb +47 -2
  112. data/lib/generators/pu/lib/plutonium_generators/concerns/resource_registration.rb +41 -0
  113. data/lib/generators/pu/lite/litestream/litestream_generator.rb +1 -1
  114. data/lib/generators/pu/lite/solid_queue/solid_queue_generator.rb +1 -1
  115. data/lib/generators/pu/res/conn/conn_generator.rb +19 -39
  116. data/lib/generators/pu/res/conn/templates/app/controllers/resource_controller.rb.tt +4 -0
  117. data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +15 -6
  118. data/lib/generators/pu/rodauth/templates/app/rodauth/rodauth_plugin.rb.tt +7 -0
  119. data/lib/generators/pu/saas/welcome/templates/app/views/layouts/welcome.html.erb.tt +2 -2
  120. data/lib/generators/pu/wizards/install_generator.rb +78 -0
  121. data/lib/plutonium/action/base.rb +71 -9
  122. data/lib/plutonium/action/interactive.rb +9 -0
  123. data/lib/plutonium/attachments.rb +254 -0
  124. data/lib/plutonium/configuration.rb +82 -1
  125. data/lib/plutonium/core/controller.rb +50 -7
  126. data/lib/plutonium/core/controllers/authorizable.rb +16 -0
  127. data/lib/plutonium/core/controllers/entity_scoping.rb +12 -2
  128. data/lib/plutonium/definition/base.rb +51 -0
  129. data/lib/plutonium/definition/display_layout.rb +112 -0
  130. data/lib/plutonium/definition/index_views.rb +8 -7
  131. data/lib/plutonium/definition/input_aliases.rb +38 -0
  132. data/lib/plutonium/definition/page_widths.rb +65 -0
  133. data/lib/plutonium/definition/positioning.rb +126 -0
  134. data/lib/plutonium/definition/sorting.rb +17 -2
  135. data/lib/plutonium/helpers/turbo_helper.rb +7 -0
  136. data/lib/plutonium/interaction/README.md +61 -24
  137. data/lib/plutonium/interaction/async/configuration.rb +38 -0
  138. data/lib/plutonium/interaction/async/context.rb +419 -0
  139. data/lib/plutonium/interaction/async/executor.rb +422 -0
  140. data/lib/plutonium/interaction/async/job.rb +80 -0
  141. data/lib/plutonium/interaction/async/reap_job.rb +81 -0
  142. data/lib/plutonium/interaction/async/run.rb +394 -0
  143. data/lib/plutonium/interaction/async/run_definition.rb +155 -0
  144. data/lib/plutonium/interaction/async/run_policy.rb +86 -0
  145. data/lib/plutonium/interaction/base.rb +34 -7
  146. data/lib/plutonium/interaction/concerns/dispatchable.rb +518 -0
  147. data/lib/plutonium/interaction/concerns/scoping.rb +70 -9
  148. data/lib/plutonium/interaction/response/redirect.rb +11 -3
  149. data/lib/plutonium/kanban/board.rb +14 -0
  150. data/lib/plutonium/kanban/column.rb +4 -2
  151. data/lib/plutonium/kanban/dsl.rb +4 -1
  152. data/lib/plutonium/kanban/grouping.rb +9 -22
  153. data/lib/plutonium/kanban/positioning.rb +5 -65
  154. data/lib/plutonium/positioning/config.rb +94 -0
  155. data/lib/plutonium/positioning/model.rb +128 -0
  156. data/lib/plutonium/positioning.rb +25 -86
  157. data/lib/plutonium/railtie.rb +1 -0
  158. data/lib/plutonium/resource/controller.rb +118 -38
  159. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +32 -2
  160. data/lib/plutonium/resource/controllers/crud_actions.rb +30 -2
  161. data/lib/plutonium/resource/controllers/eager_loading.rb +87 -0
  162. data/lib/plutonium/resource/controllers/export_csv.rb +10 -1
  163. data/lib/plutonium/resource/controllers/kanban_actions.rb +53 -14
  164. data/lib/plutonium/resource/controllers/position_actions.rb +390 -0
  165. data/lib/plutonium/resource/controllers/presentable.rb +19 -13
  166. data/lib/plutonium/resource/controllers/queryable.rb +5 -1
  167. data/lib/plutonium/resource/controllers/wizard_actions.rb +21 -0
  168. data/lib/plutonium/resource/policy.rb +33 -0
  169. data/lib/plutonium/resource/query_object.rb +36 -0
  170. data/lib/plutonium/routing/mapper_extensions.rb +100 -8
  171. data/lib/plutonium/routing/route_set_extensions.rb +15 -1
  172. data/lib/plutonium/routing/wizard_registration.rb +4 -0
  173. data/lib/plutonium/testing/resource_policy.rb +6 -2
  174. data/lib/plutonium/ui/action_button.rb +12 -7
  175. data/lib/plutonium/ui/actions_dropdown.rb +1 -1
  176. data/lib/plutonium/ui/block.rb +21 -1
  177. data/lib/plutonium/ui/breadcrumbs.rb +187 -55
  178. data/lib/plutonium/ui/component/methods.rb +5 -0
  179. data/lib/plutonium/ui/component/positionable.rb +112 -0
  180. data/lib/plutonium/ui/component/resolves_tags.rb +57 -0
  181. data/lib/plutonium/ui/component/section.rb +185 -0
  182. data/lib/plutonium/ui/display/base.rb +13 -1
  183. data/lib/plutonium/ui/display/components/formatted_value.rb +26 -0
  184. data/lib/plutonium/ui/display/components/section.rb +18 -0
  185. data/lib/plutonium/ui/display/resource.rb +141 -22
  186. data/lib/plutonium/ui/display/theme.rb +20 -1
  187. data/lib/plutonium/ui/export_button.rb +1 -1
  188. data/lib/plutonium/ui/form/base.rb +8 -7
  189. data/lib/plutonium/ui/form/components/intl_tel_input.rb +1 -1
  190. data/lib/plutonium/ui/form/components/section.rb +7 -62
  191. data/lib/plutonium/ui/form/components/uppy.rb +12 -1
  192. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +16 -3
  193. data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +5 -1
  194. data/lib/plutonium/ui/form/query.rb +2 -4
  195. data/lib/plutonium/ui/form/resource.rb +92 -15
  196. data/lib/plutonium/ui/form/theme.rb +17 -0
  197. data/lib/plutonium/ui/form/wizard.rb +25 -1
  198. data/lib/plutonium/ui/grid/card.rb +79 -11
  199. data/lib/plutonium/ui/grid/resource.rb +47 -5
  200. data/lib/plutonium/ui/interaction/async/run_progress.rb +227 -0
  201. data/lib/plutonium/ui/interaction/async/running_banner.rb +65 -0
  202. data/lib/plutonium/ui/kanban/card.rb +2 -1
  203. data/lib/plutonium/ui/kanban/column.rb +12 -6
  204. data/lib/plutonium/ui/kanban/resource.rb +6 -7
  205. data/lib/plutonium/ui/layout/base.rb +10 -3
  206. data/lib/plutonium/ui/nav_grid_menu.rb +1 -0
  207. data/lib/plutonium/ui/page/base.rb +19 -0
  208. data/lib/plutonium/ui/page/edit.rb +4 -1
  209. data/lib/plutonium/ui/page/index.rb +69 -18
  210. data/lib/plutonium/ui/page/interactive_action.rb +5 -1
  211. data/lib/plutonium/ui/page/new.rb +4 -1
  212. data/lib/plutonium/ui/page/show.rb +27 -10
  213. data/lib/plutonium/ui/page/wizard.rb +10 -1
  214. data/lib/plutonium/ui/page/wizard_chooser.rb +36 -11
  215. data/lib/plutonium/ui/page_width.rb +58 -0
  216. data/lib/plutonium/ui/table/base.rb +34 -1
  217. data/lib/plutonium/ui/table/components/attachment.rb +1 -1
  218. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +32 -8
  219. data/lib/plutonium/ui/table/components/drag_handle.rb +120 -0
  220. data/lib/plutonium/ui/table/components/filter_form.rb +1 -4
  221. data/lib/plutonium/ui/table/components/filter_pills.rb +1 -1
  222. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
  223. data/lib/plutonium/ui/table/resource.rb +50 -5
  224. data/lib/plutonium/ui/table/theme.rb +59 -2
  225. data/lib/plutonium/ui/wizard/review.rb +4 -2
  226. data/lib/plutonium/ui/wizard/summary_display.rb +42 -14
  227. data/lib/plutonium/version.rb +1 -1
  228. data/lib/plutonium/wizard/attachments.rb +32 -197
  229. data/lib/plutonium/wizard/base.rb +6 -1
  230. data/lib/plutonium/wizard/configuration.rb +12 -0
  231. data/lib/plutonium/wizard/controller.rb +14 -0
  232. data/lib/plutonium/wizard/driving.rb +99 -15
  233. data/lib/plutonium/wizard/dsl.rb +23 -0
  234. data/lib/plutonium/wizard/resume.rb +127 -49
  235. data/lib/plutonium/wizard/runner.rb +46 -2
  236. data/lib/plutonium/wizard/step_adapter.rb +1 -1
  237. data/lib/plutonium/wizard/sweep_job.rb +16 -0
  238. data/lib/plutonium.rb +21 -0
  239. data/lib/rodauth/features/session_isolation.rb +92 -0
  240. data/lib/rodauth/plugins.rb +1 -0
  241. data/package.json +2 -1
  242. data/plutonium.gemspec +20 -20
  243. data/src/css/components.css +89 -1
  244. data/src/css/slim_select.css +20 -0
  245. data/src/js/controllers/breadcrumbs_controller.js +112 -0
  246. data/src/js/controllers/bulk_actions_controller.js +10 -2
  247. data/src/js/controllers/kanban_controller.js +30 -21
  248. data/src/js/controllers/positioned_controller.js +452 -0
  249. data/src/js/controllers/register_controllers.js +6 -0
  250. data/src/js/controllers/resource_drop_down_controller.js +5 -0
  251. data/src/js/controllers/run_progress_controller.js +73 -0
  252. data/src/js/drag/sortable.js +186 -0
  253. data/yarn.lock +108 -63
  254. metadata +101 -22
  255. data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +0 -53
  256. data/docs/.vitepress/theme/components/HomePillars.vue +0 -42
@@ -13180,6 +13180,7 @@
13180
13180
  this.menu.classList.remove("hidden");
13181
13181
  this.menu.classList.add("block");
13182
13182
  this.menu.removeAttribute("aria-hidden");
13183
+ this.triggerTarget.setAttribute("aria-expanded", "true");
13183
13184
  this.popperInstance.setOptions((options2) => ({
13184
13185
  ...options2,
13185
13186
  modifiers: [
@@ -13195,6 +13196,7 @@
13195
13196
  this.menu.classList.remove("block");
13196
13197
  this.menu.classList.add("hidden");
13197
13198
  this.menu.setAttribute("aria-hidden", "true");
13199
+ this.triggerTarget.setAttribute("aria-expanded", "false");
13198
13200
  this.popperInstance.setOptions((options2) => ({
13199
13201
  ...options2,
13200
13202
  modifiers: [
@@ -13660,7 +13662,7 @@
13660
13662
  var mathMlDisallowed = freeze(["maction", "maligngroup", "malignmark", "mlongdiv", "mscarries", "mscarry", "msgroup", "mstack", "msline", "msrow", "semantics", "annotation", "annotation-xml", "mprescripts", "none"]);
13661
13663
  var text = freeze(["#text"]);
13662
13664
  var html = freeze(["accept", "action", "align", "alt", "autocapitalize", "autocomplete", "autopictureinpicture", "autoplay", "background", "bgcolor", "border", "capture", "cellpadding", "cellspacing", "checked", "cite", "class", "clear", "color", "cols", "colspan", "command", "commandfor", "controls", "controlslist", "coords", "crossorigin", "datetime", "decoding", "default", "dir", "disabled", "disablepictureinpicture", "disableremoteplayback", "download", "draggable", "enctype", "enterkeyhint", "exportparts", "face", "for", "headers", "height", "hidden", "high", "href", "hreflang", "id", "inert", "inputmode", "integrity", "ismap", "kind", "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min", "minlength", "multiple", "muted", "name", "nonce", "noshade", "novalidate", "nowrap", "open", "optimum", "part", "pattern", "placeholder", "playsinline", "popover", "popovertarget", "popovertargetaction", "poster", "preload", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "spellcheck", "scope", "selected", "shape", "size", "sizes", "slot", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "title", "translate", "type", "usemap", "valign", "value", "width", "wrap", "xmlns"]);
13663
- var svg = freeze(["accent-height", "accumulate", "additive", "alignment-baseline", "amplitude", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clippathunits", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "exponent", "fill", "fill-opacity", "fill-rule", "filter", "filterunits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "intercept", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "mask-type", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "primitiveunits", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "slope", "specularconstant", "specularexponent", "spreadmethod", "startoffset", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "systemlanguage", "tabindex", "tablevalues", "targetx", "targety", "transform", "transform-origin", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xmlns", "y", "y1", "y2", "z", "zoomandpan"]);
13665
+ var svg = freeze(["accent-height", "accumulate", "additive", "alignment-baseline", "amplitude", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clippathunits", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dominant-baseline", "dur", "edgemode", "elevation", "end", "exponent", "fill", "fill-opacity", "fill-rule", "filter", "filterunits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "intercept", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "mask-type", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "pointer-events", "points", "preservealpha", "preserveaspectratio", "primitiveunits", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "slope", "specularconstant", "specularexponent", "spreadmethod", "startoffset", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "systemlanguage", "tabindex", "tablevalues", "targetx", "targety", "transform", "transform-origin", "text-anchor", "text-decoration", "text-orientation", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "vector-effect", "viewbox", "visibility", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xmlns", "y", "y1", "y2", "z", "zoomandpan"]);
13664
13666
  var mathMl = freeze(["accent", "accentunder", "align", "bevelled", "close", "columnalign", "columnlines", "columnspacing", "columnspan", "denomalign", "depth", "dir", "display", "displaystyle", "encoding", "fence", "frame", "height", "href", "id", "largeop", "length", "linethickness", "lquote", "lspace", "mathbackground", "mathcolor", "mathsize", "mathvariant", "maxsize", "minsize", "movablelimits", "notation", "numalign", "open", "rowalign", "rowlines", "rowspacing", "rowspan", "rspace", "rquote", "scriptlevel", "scriptminsize", "scriptsizemultiplier", "selection", "separator", "separators", "stretchy", "subscriptshift", "supscriptshift", "symmetric", "voffset", "width", "xmlns"]);
13665
13667
  var xml = freeze(["xlink:href", "xml:id", "xlink:title", "xml:space", "xmlns:xlink"]);
13666
13668
  var MUSTACHE_EXPR = seal(/{{[\w\W]*|^[\w\W]*}}/g);
@@ -13700,6 +13702,15 @@
13700
13702
  notation: 12
13701
13703
  // Deprecated
13702
13704
  };
13705
+ var LITERAL_TEXT_ELEMENT_NAMES = ["style", "script", "xmp", "iframe", "noembed", "noframes", "plaintext", "noscript"];
13706
+ var LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, LITERAL_TEXT_ELEMENT_NAMES));
13707
+ var LITERAL_TEXT_CLOSE = (function() {
13708
+ const map = {};
13709
+ arrayForEach(LITERAL_TEXT_ELEMENT_NAMES, (name) => {
13710
+ map[name] = seal(new RegExp("</" + name + "(?=[\\t\\n\\f\\r />])", "i"));
13711
+ });
13712
+ return freeze(map);
13713
+ })();
13703
13714
  var getGlobal = function getGlobal2() {
13704
13715
  return typeof window === "undefined" ? null : window;
13705
13716
  };
@@ -13743,10 +13754,14 @@
13743
13754
  var _resolveSetOption = function _resolveSetOption2(cfg, key, fallback, options2) {
13744
13755
  return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options2.base ? clone(options2.base) : {}, cfg[key], options2.transform) : fallback;
13745
13756
  };
13757
+ var _resolveObjectOption = function _resolveObjectOption2(cfg, key, makeFallback) {
13758
+ const value = objectHasOwnProperty(cfg, key) ? cfg[key] : void 0;
13759
+ return value && typeof value === "object" ? clone(value) : makeFallback();
13760
+ };
13746
13761
  function createDOMPurify() {
13747
13762
  let window2 = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
13748
13763
  const DOMPurify = (root) => createDOMPurify(root);
13749
- DOMPurify.version = "3.4.11";
13764
+ DOMPurify.version = "3.4.14";
13750
13765
  DOMPurify.removed = [];
13751
13766
  if (!window2 || !window2.document || window2.document.nodeType !== NODE_TYPE.document || !window2.Element) {
13752
13767
  DOMPurify.isSupported = false;
@@ -13770,6 +13785,13 @@
13770
13785
  const getAttributes = lookupGetter(ElementPrototype, "attributes");
13771
13786
  const getNodeType = Node2 && Node2.prototype ? lookupGetter(Node2.prototype, "nodeType") : null;
13772
13787
  const getNodeName2 = Node2 && Node2.prototype ? lookupGetter(Node2.prototype, "nodeName") : null;
13788
+ const getOwnerDocument = Node2 && Node2.prototype ? lookupGetter(Node2.prototype, "ownerDocument") : null;
13789
+ const _readNodeType = function _readNodeType2(node) {
13790
+ return getNodeType ? getNodeType(node) : node.nodeType;
13791
+ };
13792
+ const _readNodeName = function _readNodeName2(node) {
13793
+ return getNodeName2 ? getNodeName2(node) : node.nodeName;
13794
+ };
13773
13795
  if (typeof HTMLTemplateElement2 === "function") {
13774
13796
  const template = document2.createElement("template");
13775
13797
  if (template.content && template.content.ownerDocument) {
@@ -13996,9 +14018,19 @@
13996
14018
  IN_PLACE = cfg.IN_PLACE || false;
13997
14019
  IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI;
13998
14020
  NAMESPACE = typeof cfg.NAMESPACE === "string" ? cfg.NAMESPACE : HTML_NAMESPACE;
13999
- MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, "MATHML_TEXT_INTEGRATION_POINTS") && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === "object" ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
14000
- HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, "HTML_INTEGRATION_POINTS") && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === "object" ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
14001
- const customElementHandling = objectHasOwnProperty(cfg, "CUSTOM_ELEMENT_HANDLING") && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === "object" ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
14021
+ MATHML_TEXT_INTEGRATION_POINTS = _resolveObjectOption(
14022
+ cfg,
14023
+ "MATHML_TEXT_INTEGRATION_POINTS",
14024
+ () => addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS)
14025
+ // Default built-in map
14026
+ );
14027
+ HTML_INTEGRATION_POINTS = _resolveObjectOption(
14028
+ cfg,
14029
+ "HTML_INTEGRATION_POINTS",
14030
+ () => addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS)
14031
+ // Default built-in map
14032
+ );
14033
+ const customElementHandling = _resolveObjectOption(cfg, "CUSTOM_ELEMENT_HANDLING", () => create(null));
14002
14034
  CUSTOM_ELEMENT_HANDLING = create(null);
14003
14035
  if (objectHasOwnProperty(customElementHandling, "tagNameCheck") && isRegexOrFunction(customElementHandling.tagNameCheck)) {
14004
14036
  CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck;
@@ -14061,15 +14093,6 @@
14061
14093
  addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
14062
14094
  }
14063
14095
  }
14064
- if (objectHasOwnProperty(cfg, "ADD_URI_SAFE_ATTR") && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) {
14065
- addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
14066
- }
14067
- if (objectHasOwnProperty(cfg, "FORBID_CONTENTS") && arrayIsArray(cfg.FORBID_CONTENTS)) {
14068
- if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
14069
- FORBID_CONTENTS = clone(FORBID_CONTENTS);
14070
- }
14071
- addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
14072
- }
14073
14096
  if (objectHasOwnProperty(cfg, "ADD_FORBID_CONTENTS") && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
14074
14097
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
14075
14098
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
@@ -14186,7 +14209,18 @@
14186
14209
  }
14187
14210
  }
14188
14211
  };
14212
+ const _stripAttributeNode = function _stripAttributeNode2(element, attribute, name) {
14213
+ try {
14214
+ element.removeAttributeNode(attribute);
14215
+ } catch (_4) {
14216
+ try {
14217
+ element.removeAttribute(name);
14218
+ } catch (_5) {
14219
+ }
14220
+ }
14221
+ };
14189
14222
  const _neutralizeRoot = function _neutralizeRoot2(root) {
14223
+ _neutralizeSubtree(root);
14190
14224
  const childNodes = getChildNodes(root);
14191
14225
  if (childNodes) {
14192
14226
  const snapshot = [];
@@ -14206,27 +14240,35 @@
14206
14240
  const attribute = attributes[i4];
14207
14241
  const name = attribute && attribute.name;
14208
14242
  if (typeof name === "string") {
14209
- try {
14210
- root.removeAttribute(name);
14211
- } catch (_4) {
14212
- }
14243
+ _stripAttributeNode(root, attribute, name);
14213
14244
  }
14214
14245
  }
14215
14246
  }
14216
14247
  };
14217
- const _removeAttribute = function _removeAttribute2(name, element) {
14248
+ const _removeAttribute = function _removeAttribute2(name, element, attr) {
14249
+ if (!attr) {
14250
+ try {
14251
+ attr = element.getAttributeNode(name);
14252
+ } catch (_4) {
14253
+ attr = null;
14254
+ }
14255
+ }
14256
+ arrayPush(DOMPurify.removed, {
14257
+ attribute: attr || null,
14258
+ from: element
14259
+ });
14218
14260
  try {
14219
- arrayPush(DOMPurify.removed, {
14220
- attribute: element.getAttributeNode(name),
14221
- from: element
14222
- });
14261
+ if (attr) {
14262
+ element.removeAttributeNode(attr);
14263
+ } else {
14264
+ element.removeAttribute(name);
14265
+ }
14223
14266
  } catch (_4) {
14224
- arrayPush(DOMPurify.removed, {
14225
- attribute: null,
14226
- from: element
14227
- });
14267
+ try {
14268
+ element.removeAttribute(name);
14269
+ } catch (_5) {
14270
+ }
14228
14271
  }
14229
- element.removeAttribute(name);
14230
14272
  if (name === "is") {
14231
14273
  if (RETURN_DOM || RETURN_DOM_FRAGMENT) {
14232
14274
  try {
@@ -14252,17 +14294,14 @@
14252
14294
  if (typeof name !== "string" || ALLOWED_ATTR[transformCaseFunc(name)]) {
14253
14295
  continue;
14254
14296
  }
14255
- try {
14256
- element.removeAttribute(name);
14257
- } catch (_4) {
14258
- }
14297
+ _stripAttributeNode(element, attribute, name);
14259
14298
  }
14260
14299
  };
14261
14300
  const _neutralizeSubtree = function _neutralizeSubtree2(root) {
14262
14301
  const stack = [root];
14263
14302
  while (stack.length > 0) {
14264
14303
  const node = stack.pop();
14265
- const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
14304
+ const nodeType = _readNodeType(node);
14266
14305
  if (nodeType === NODE_TYPE.element) {
14267
14306
  _stripDisallowedAttributes(node);
14268
14307
  }
@@ -14274,6 +14313,51 @@
14274
14313
  }
14275
14314
  }
14276
14315
  };
14316
+ const _isPatchLinkageAttribute = function _isPatchLinkageAttribute2(lcName, lcTag) {
14317
+ if (!SAFE_FOR_XML) {
14318
+ return false;
14319
+ }
14320
+ if (lcName === "patchsrc") {
14321
+ return true;
14322
+ }
14323
+ return lcName === "for" && lcTag !== "label" && lcTag !== "output";
14324
+ };
14325
+ const _neutralizePatchLinkage = function _neutralizePatchLinkage2(root) {
14326
+ if (!SAFE_FOR_XML) {
14327
+ return;
14328
+ }
14329
+ const stack = [root];
14330
+ while (stack.length > 0) {
14331
+ const node = stack.pop();
14332
+ const nodeType = _readNodeType(node);
14333
+ if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
14334
+ try {
14335
+ remove(node);
14336
+ } catch (_4) {
14337
+ }
14338
+ continue;
14339
+ }
14340
+ if (nodeType === NODE_TYPE.element) {
14341
+ const element = node;
14342
+ const lcTag = transformCaseFunc(_readNodeName(node));
14343
+ try {
14344
+ if (element.hasAttribute && element.hasAttribute("patchsrc")) {
14345
+ element.removeAttribute("patchsrc");
14346
+ }
14347
+ if (element.hasAttribute && element.hasAttribute("for") && _isPatchLinkageAttribute("for", lcTag)) {
14348
+ element.removeAttribute("for");
14349
+ }
14350
+ } catch (_4) {
14351
+ }
14352
+ }
14353
+ const childNodes = getChildNodes(node);
14354
+ if (childNodes) {
14355
+ for (let i4 = childNodes.length - 1; i4 >= 0; --i4) {
14356
+ stack.push(childNodes[i4]);
14357
+ }
14358
+ }
14359
+ }
14360
+ };
14277
14361
  const _initDocument = function _initDocument2(dirty) {
14278
14362
  let doc = null;
14279
14363
  let leadingWhitespace = null;
@@ -14310,8 +14394,9 @@
14310
14394
  return WHOLE_DOCUMENT ? doc.documentElement : body;
14311
14395
  };
14312
14396
  const _createNodeIterator = function _createNodeIterator2(root) {
14397
+ const doc = getOwnerDocument ? getOwnerDocument(root) : root.ownerDocument;
14313
14398
  return createNodeIterator.call(
14314
- root.ownerDocument || root,
14399
+ doc || root,
14315
14400
  root,
14316
14401
  // eslint-disable-next-line no-bitwise
14317
14402
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION,
@@ -14327,8 +14412,9 @@
14327
14412
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
14328
14413
  var _node$querySelectorAl;
14329
14414
  node.normalize();
14415
+ const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
14330
14416
  const walker = createNodeIterator.call(
14331
- node.ownerDocument || node,
14417
+ doc || node,
14332
14418
  node,
14333
14419
  // eslint-disable-next-line no-bitwise
14334
14420
  NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION,
@@ -14413,7 +14499,7 @@
14413
14499
  if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
14414
14500
  return true;
14415
14501
  }
14416
- if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === "style" && _isNode(currentNode.firstElementChild)) {
14502
+ if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && LITERAL_TEXT_ELEMENTS[tagName] && (_isNode(currentNode.firstElementChild) || typeof currentNode.textContent === "string" && regExpTest(LITERAL_TEXT_CLOSE[tagName], currentNode.textContent))) {
14417
14503
  return true;
14418
14504
  }
14419
14505
  if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
@@ -14424,14 +14510,21 @@
14424
14510
  }
14425
14511
  return false;
14426
14512
  };
14427
- const _sanitizeDisallowedNode = function _sanitizeDisallowedNode2(currentNode, tagName) {
14428
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
14429
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
14430
- return false;
14431
- }
14432
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
14433
- return false;
14513
+ const _matchesNameCheck = function _matchesNameCheck2(check, name) {
14514
+ if (check instanceof RegExp) {
14515
+ return regExpTest(check, name);
14516
+ }
14517
+ if (check instanceof Function) {
14518
+ for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
14519
+ args[_key - 2] = arguments[_key];
14434
14520
  }
14521
+ return Boolean(check(name, ...args));
14522
+ }
14523
+ return false;
14524
+ };
14525
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode2(currentNode, tagName, root) {
14526
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
14527
+ return false;
14435
14528
  }
14436
14529
  if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
14437
14530
  const parentNode = getParentNode2(currentNode);
@@ -14439,7 +14532,7 @@
14439
14532
  if (childNodes && parentNode) {
14440
14533
  const childCount = childNodes.length;
14441
14534
  for (let i4 = childCount - 1; i4 >= 0; --i4) {
14442
- const hoisted = IN_PLACE ? childNodes[i4] : cloneNode(childNodes[i4], true);
14535
+ const hoisted = currentNode === root ? cloneNode(childNodes[i4], true) : childNodes[i4];
14443
14536
  parentNode.insertBefore(hoisted, getNextSibling(currentNode));
14444
14537
  }
14445
14538
  }
@@ -14447,25 +14540,51 @@
14447
14540
  _forceRemove(currentNode);
14448
14541
  return true;
14449
14542
  };
14450
- const _sanitizeElements = function _sanitizeElements2(currentNode) {
14543
+ const _forkSharedAllowlist = function _forkSharedAllowlist2(hookList, set, defaultSet, setConfigSet) {
14544
+ if (hookList.length === 0) {
14545
+ return set;
14546
+ }
14547
+ return set === defaultSet || set === setConfigSet ? clone(set) : set;
14548
+ };
14549
+ const _handleHookDetachedNode = function _handleHookDetachedNode2(currentNode, root) {
14550
+ if (currentNode === root || getParentNode2(currentNode) !== null) {
14551
+ return false;
14552
+ }
14553
+ if (IN_PLACE) {
14554
+ _neutralizeSubtree(currentNode);
14555
+ }
14556
+ return true;
14557
+ };
14558
+ const _sanitizeElements = function _sanitizeElements2(currentNode, root) {
14451
14559
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
14560
+ if (_handleHookDetachedNode(currentNode, root)) {
14561
+ return true;
14562
+ }
14452
14563
  if (_isClobbered(currentNode)) {
14453
14564
  _forceRemove(currentNode);
14454
14565
  return true;
14455
14566
  }
14456
- const tagName = transformCaseFunc(getNodeName2 ? getNodeName2(currentNode) : currentNode.nodeName);
14567
+ const tagName = transformCaseFunc(_readNodeName(currentNode));
14568
+ ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
14457
14569
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
14458
14570
  tagName,
14459
14571
  allowedTags: ALLOWED_TAGS
14460
14572
  });
14573
+ if (_handleHookDetachedNode(currentNode, root)) {
14574
+ return true;
14575
+ }
14461
14576
  if (_isUnsafeNode(currentNode, tagName)) {
14462
14577
  _forceRemove(currentNode);
14463
14578
  return true;
14464
14579
  }
14465
14580
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
14466
- return _sanitizeDisallowedNode(currentNode, tagName);
14581
+ const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
14582
+ if (removed === false) {
14583
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
14584
+ }
14585
+ return removed;
14467
14586
  }
14468
- const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;
14587
+ const nt = _readNodeType(currentNode);
14469
14588
  if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
14470
14589
  _forceRemove(currentNode);
14471
14590
  return true;
@@ -14490,32 +14609,43 @@
14490
14609
  if (FORBID_ATTR[lcName]) {
14491
14610
  return false;
14492
14611
  }
14612
+ if (_isPatchLinkageAttribute(lcName, lcTag)) {
14613
+ return false;
14614
+ }
14493
14615
  if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document2 || value in formElement)) {
14494
14616
  return false;
14495
14617
  }
14496
14618
  const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
14497
- if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) ;
14498
- else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ;
14499
- else if (!nameIsPermitted) {
14500
- if (
14501
- // First condition does a very basic check if a) it's basically a valid custom element tagname AND
14502
- // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
14503
- // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck
14504
- _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName, lcTag)) || // Alternative, second condition checks if it's an `is`-attribute, AND
14505
- // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
14506
- lcName === "is" && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))
14507
- ) ;
14508
- else {
14509
- return false;
14510
- }
14511
- } else if (URI_SAFE_ATTRIBUTES[lcName]) ;
14512
- else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ""))) ;
14513
- else if ((lcName === "src" || lcName === "xlink:href" || lcName === "href") && lcTag !== "script" && stringIndexOf(value, "data:") === 0 && DATA_URI_TAGS[lcTag]) ;
14514
- else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ""))) ;
14515
- else if (value) {
14516
- return false;
14517
- } else ;
14518
- return true;
14619
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) {
14620
+ return true;
14621
+ }
14622
+ if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) {
14623
+ return true;
14624
+ }
14625
+ if (!nameIsPermitted) {
14626
+ return (
14627
+ // Condition a) covers a basically valid custom element tag name whose
14628
+ // tag passes the configured tagNameCheck and whose attribute name
14629
+ // passes the configured attributeNameCheck ...
14630
+ _isBasicCustomElement(lcTag) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName, lcTag) || // Condition b) covers an `is` attribute whose value passes the
14631
+ // configured tagNameCheck while customized built-in elements are
14632
+ // allowed.
14633
+ lcName === "is" && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value)
14634
+ );
14635
+ }
14636
+ if (URI_SAFE_ATTRIBUTES[lcName]) {
14637
+ return true;
14638
+ }
14639
+ if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ""))) {
14640
+ return true;
14641
+ }
14642
+ if ((lcName === "src" || lcName === "xlink:href" || lcName === "href") && lcTag !== "script" && stringIndexOf(value, "data:") === 0 && DATA_URI_TAGS[lcTag]) {
14643
+ return true;
14644
+ }
14645
+ if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ""))) {
14646
+ return true;
14647
+ }
14648
+ return !value;
14519
14649
  };
14520
14650
  const RESERVED_CUSTOM_ELEMENT_NAMES = addToSet({}, ["annotation-xml", "color-profile", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "missing-glyph"]);
14521
14651
  const _isBasicCustomElement = function _isBasicCustomElement2(tagName) {
@@ -14556,6 +14686,7 @@
14556
14686
  if (!attributes || _isClobbered(currentNode)) {
14557
14687
  return;
14558
14688
  }
14689
+ ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR);
14559
14690
  const hookEvent = {
14560
14691
  attrName: "",
14561
14692
  attrValue: "",
@@ -14578,33 +14709,33 @@
14578
14709
  _executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent);
14579
14710
  value = hookEvent.attrValue;
14580
14711
  if (SANITIZE_NAMED_PROPS && (lcName === "id" || lcName === "name") && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
14581
- _removeAttribute(name, currentNode);
14712
+ _removeAttribute(name, currentNode, attr);
14582
14713
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
14583
14714
  }
14584
14715
  if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
14585
- _removeAttribute(name, currentNode);
14716
+ _removeAttribute(name, currentNode, attr);
14586
14717
  continue;
14587
14718
  }
14588
14719
  if (lcName === "attributename" && stringMatch(value, "href")) {
14589
- _removeAttribute(name, currentNode);
14720
+ _removeAttribute(name, currentNode, attr);
14590
14721
  continue;
14591
14722
  }
14592
14723
  if (hookEvent.forceKeepAttr) {
14593
14724
  continue;
14594
14725
  }
14595
14726
  if (!hookEvent.keepAttr) {
14596
- _removeAttribute(name, currentNode);
14727
+ _removeAttribute(name, currentNode, attr);
14597
14728
  continue;
14598
14729
  }
14599
14730
  if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
14600
- _removeAttribute(name, currentNode);
14731
+ _removeAttribute(name, currentNode, attr);
14601
14732
  continue;
14602
14733
  }
14603
14734
  if (SAFE_FOR_TEMPLATES) {
14604
14735
  value = _stripTemplateExpressions(value);
14605
14736
  }
14606
14737
  if (!_isValidAttribute(lcTag, lcName, value)) {
14607
- _removeAttribute(name, currentNode);
14738
+ _removeAttribute(name, currentNode, attr);
14608
14739
  continue;
14609
14740
  }
14610
14741
  value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
@@ -14620,13 +14751,12 @@
14620
14751
  _executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null);
14621
14752
  while (shadowNode = shadowIterator.nextNode()) {
14622
14753
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
14623
- _sanitizeElements(shadowNode);
14754
+ _sanitizeElements(shadowNode, fragment);
14624
14755
  _sanitizeAttributes(shadowNode);
14625
14756
  if (_isDocumentFragment(shadowNode.content)) {
14626
14757
  _sanitizeShadowDOM2(shadowNode.content);
14627
14758
  }
14628
- const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
14629
- if (shadowNodeType === NODE_TYPE.element) {
14759
+ if (_readNodeType(shadowNode) === NODE_TYPE.element) {
14630
14760
  const innerSr = getShadowRoot(shadowNode);
14631
14761
  if (_isDocumentFragment(innerSr)) {
14632
14762
  _sanitizeAttachedShadowRoots(innerSr);
@@ -14648,7 +14778,7 @@
14648
14778
  continue;
14649
14779
  }
14650
14780
  const node = item.node;
14651
- const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
14781
+ const nodeType = _readNodeType(node);
14652
14782
  const isElement2 = nodeType === NODE_TYPE.element;
14653
14783
  const childNodes = getChildNodes(node);
14654
14784
  if (childNodes) {
@@ -14719,14 +14849,17 @@
14719
14849
  DOMPurify.removed = [];
14720
14850
  const inPlace = IN_PLACE && typeof dirty !== "string" && _isNode(dirty);
14721
14851
  if (inPlace) {
14722
- const nn2 = getNodeName2 ? getNodeName2(dirty) : dirty.nodeName;
14852
+ _neutralizePatchLinkage(dirty);
14853
+ const nn2 = _readNodeName(dirty);
14723
14854
  if (typeof nn2 === "string") {
14724
14855
  const tagName = transformCaseFunc(nn2);
14725
14856
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
14857
+ _neutralizeRoot(dirty);
14726
14858
  throw typeErrorCreate("root node is forbidden and cannot be sanitized in-place");
14727
14859
  }
14728
14860
  }
14729
14861
  if (_isClobbered(dirty)) {
14862
+ _neutralizeRoot(dirty);
14730
14863
  throw typeErrorCreate("root node is clobbered and cannot be sanitized in-place");
14731
14864
  }
14732
14865
  try {
@@ -14759,10 +14892,11 @@
14759
14892
  if (body && FORCE_BODY) {
14760
14893
  _forceRemove(body.firstChild);
14761
14894
  }
14762
- const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
14895
+ const walkRoot = inPlace ? dirty : body;
14763
14896
  try {
14897
+ const nodeIterator = _createNodeIterator(walkRoot);
14764
14898
  while (currentNode = nodeIterator.nextNode()) {
14765
- _sanitizeElements(currentNode);
14899
+ _sanitizeElements(currentNode, walkRoot);
14766
14900
  _sanitizeAttributes(currentNode);
14767
14901
  if (_isDocumentFragment(currentNode.content)) {
14768
14902
  _sanitizeShadowDOM2(currentNode.content);
@@ -14771,6 +14905,11 @@
14771
14905
  } catch (error2) {
14772
14906
  if (inPlace) {
14773
14907
  _neutralizeRoot(dirty);
14908
+ arrayForEach(DOMPurify.removed, (entry) => {
14909
+ if (entry.element) {
14910
+ _neutralizeSubtree(entry.element);
14911
+ }
14912
+ });
14774
14913
  }
14775
14914
  throw error2;
14776
14915
  }
@@ -18614,7 +18753,7 @@ ${text2}</tr>
18614
18753
  var nanoid = (size = 21) => {
18615
18754
  let id2 = "";
18616
18755
  let i4 = size | 0;
18617
- while (i4--) {
18756
+ while (i4-- > 0) {
18618
18757
  id2 += urlAlphabet[Math.random() * 64 | 0];
18619
18758
  }
18620
18759
  return id2;
@@ -28216,7 +28355,9 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
28216
28355
  const baseUrl = button.dataset.bulkActionUrl;
28217
28356
  const actionName = button.dataset.bulkActionName;
28218
28357
  if (baseUrl) {
28219
- button.href = idsParam ? `${baseUrl}?${idsParam}` : baseUrl;
28358
+ const [path, query] = baseUrl.split("?");
28359
+ const merged = [query, idsParam].filter(Boolean).join("&");
28360
+ button.href = merged ? `${path}?${merged}` : path;
28220
28361
  }
28221
28362
  button.style.display = allowedActions.has(actionName) ? "" : "none";
28222
28363
  });
@@ -28913,6 +29054,97 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
28913
29054
  };
28914
29055
  };
28915
29056
 
29057
+ // src/js/drag/sortable.js
29058
+ function computeDropIndex(clientY, items) {
29059
+ for (let i4 = 0; i4 < items.length; i4++) {
29060
+ const rect = items[i4].getBoundingClientRect();
29061
+ if (clientY < rect.top + rect.height / 2) return i4;
29062
+ }
29063
+ return items.length;
29064
+ }
29065
+ function computeDropIndexHorizontal(clientX, items) {
29066
+ for (let i4 = 0; i4 < items.length; i4++) {
29067
+ const rect = items[i4].getBoundingClientRect();
29068
+ if (clientX < rect.left + rect.width / 2) return i4;
29069
+ }
29070
+ return items.length;
29071
+ }
29072
+ function beginDrag(event, element, { draggingClass, payload, dragImage = null }) {
29073
+ event.dataTransfer.effectAllowed = "move";
29074
+ event.dataTransfer.setData("text/plain", payload);
29075
+ if (dragImage) {
29076
+ const rect = dragImage.getBoundingClientRect();
29077
+ event.dataTransfer.setDragImage(dragImage, event.clientX - rect.left, event.clientY - rect.top);
29078
+ }
29079
+ requestAnimationFrame(() => element.classList.add(draggingClass));
29080
+ }
29081
+ function endDrag(element, { draggingClass }) {
29082
+ element.classList.remove(draggingClass);
29083
+ hideInsertionMarker();
29084
+ }
29085
+ var MARKER_ID = "pu-drag-insertion-marker";
29086
+ function insertionMarker() {
29087
+ let marker = document.getElementById(MARKER_ID);
29088
+ if (!marker) {
29089
+ marker = document.createElement("div");
29090
+ marker.id = MARKER_ID;
29091
+ marker.className = "pu-drag-marker";
29092
+ marker.setAttribute("aria-hidden", "true");
29093
+ document.body.appendChild(marker);
29094
+ }
29095
+ return marker;
29096
+ }
29097
+ function showInsertionMarker(items, index, { axis = "vertical", container = null, gap = null } = {}) {
29098
+ const marker = insertionMarker();
29099
+ if (gap) {
29100
+ marker.style.setProperty("--pu-drag-marker-gap", gap);
29101
+ } else {
29102
+ marker.style.removeProperty("--pu-drag-marker-gap");
29103
+ }
29104
+ const vertical = axis !== "horizontal";
29105
+ const near = (rect) => vertical ? rect.top : rect.left;
29106
+ const far = (rect) => vertical ? rect.bottom : rect.right;
29107
+ let span;
29108
+ let offset2;
29109
+ let edge;
29110
+ if (items.length === 0) {
29111
+ if (!container) return hideInsertionMarker();
29112
+ span = container.getBoundingClientRect();
29113
+ offset2 = near(span);
29114
+ edge = "leading";
29115
+ } else if (index <= 0) {
29116
+ span = items[0].getBoundingClientRect();
29117
+ offset2 = near(span);
29118
+ edge = "leading";
29119
+ } else if (index >= items.length) {
29120
+ span = items[items.length - 1].getBoundingClientRect();
29121
+ offset2 = far(span);
29122
+ edge = "trailing";
29123
+ } else {
29124
+ span = items[index].getBoundingClientRect();
29125
+ offset2 = (far(items[index - 1].getBoundingClientRect()) + near(span)) / 2;
29126
+ edge = "between";
29127
+ }
29128
+ if (vertical) {
29129
+ marker.style.left = `${span.left}px`;
29130
+ marker.style.top = `${offset2}px`;
29131
+ marker.style.width = `${span.width}px`;
29132
+ marker.style.height = "";
29133
+ } else {
29134
+ marker.style.left = `${offset2}px`;
29135
+ marker.style.top = `${span.top}px`;
29136
+ marker.style.height = `${span.height}px`;
29137
+ marker.style.width = "";
29138
+ }
29139
+ marker.dataset.axis = axis;
29140
+ marker.dataset.edge = edge;
29141
+ marker.style.display = "block";
29142
+ }
29143
+ function hideInsertionMarker() {
29144
+ const marker = document.getElementById(MARKER_ID);
29145
+ if (marker) marker.style.display = "none";
29146
+ }
29147
+
28916
29148
  // src/js/controllers/kanban_controller.js
28917
29149
  var kanban_controller_default = class extends Controller {
28918
29150
  static values = { moveUrlTemplate: String, collapseCookie: String, collapsePath: String };
@@ -29166,9 +29398,10 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
29166
29398
  const card = event.target.closest("[data-kanban-record-id]");
29167
29399
  if (!card) return;
29168
29400
  this.draggedCard = card;
29169
- event.dataTransfer.effectAllowed = "move";
29170
- event.dataTransfer.setData("text/plain", card.dataset.kanbanRecordId);
29171
- requestAnimationFrame(() => card.classList.add("pu-kanban-dragging"));
29401
+ beginDrag(event, card, {
29402
+ draggingClass: "pu-kanban-dragging",
29403
+ payload: card.dataset.kanbanRecordId
29404
+ });
29172
29405
  this.#applyDropHints(card.dataset.kanbanColumnKey);
29173
29406
  }
29174
29407
  #onDragOver(event) {
@@ -29179,15 +29412,25 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
29179
29412
  event.preventDefault();
29180
29413
  event.dataTransfer.dropEffect = "move";
29181
29414
  this.#highlightColumn(column);
29415
+ const existingCards = [...column.querySelectorAll("[data-kanban-record-id]")].filter((c4) => c4 !== this.draggedCard);
29416
+ showInsertionMarker(existingCards, computeDropIndex(event.clientY, existingCards), {
29417
+ axis: "vertical",
29418
+ container: column,
29419
+ // Cards sit further apart than table rows, so the table's clearance reads
29420
+ // as cramped against a card's edge here.
29421
+ gap: "10px"
29422
+ });
29182
29423
  }
29183
29424
  #onDragLeave(event) {
29184
29425
  if (!this.element.contains(event.relatedTarget)) {
29185
29426
  this.#clearHighlights();
29427
+ hideInsertionMarker();
29186
29428
  }
29187
29429
  }
29188
29430
  async #onDrop(event) {
29189
29431
  event.preventDefault();
29190
29432
  this.#clearHighlights();
29433
+ hideInsertionMarker();
29191
29434
  if (!this.draggedCard) return;
29192
29435
  const wrapper = event.target.closest("[data-kanban-col]");
29193
29436
  if (wrapper?.classList.contains("pu-kanban-no-drop")) return;
@@ -29197,7 +29440,7 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
29197
29440
  const fromColumn = this.draggedCard.dataset.kanbanColumnKey;
29198
29441
  const toColumn = column.dataset.kanbanColumnKeyValue;
29199
29442
  const existingCards = [...column.querySelectorAll("[data-kanban-record-id]")].filter((c4) => c4 !== this.draggedCard);
29200
- const toIndex = this.#computeDropIndex(event.clientY, existingCards);
29443
+ const toIndex = computeDropIndex(event.clientY, existingCards);
29201
29444
  const destWrapper = column.closest("[data-kanban-col]");
29202
29445
  if (destWrapper?.dataset.kanbanDropInteraction === "true" && fromColumn !== toColumn) {
29203
29446
  if (destWrapper.dataset.kanbanDropImmediate === "true") {
@@ -29276,7 +29519,7 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
29276
29519
  this.#clearHighlights();
29277
29520
  this.#clearDropHints();
29278
29521
  if (this.draggedCard) {
29279
- this.draggedCard.classList.remove("pu-kanban-dragging");
29522
+ endDrag(this.draggedCard, { draggingClass: "pu-kanban-dragging" });
29280
29523
  this.draggedCard = null;
29281
29524
  }
29282
29525
  }
@@ -29336,17 +29579,6 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
29336
29579
  document.cookie = `${name}=${value}; path=${path}; max-age=${60 * 60 * 24 * 180}; SameSite=Lax`;
29337
29580
  }
29338
29581
  // ─── helpers ─────────────────────────────────────────────────────────────────
29339
- // Returns the 0-based insertion index within the destination column by
29340
- // comparing the cursor y-position against each card's vertical midpoint.
29341
- // The card is inserted before the first card whose midpoint is below the
29342
- // cursor, or appended after all cards if the cursor is below every midpoint.
29343
- #computeDropIndex(clientY, cards) {
29344
- for (let i4 = 0; i4 < cards.length; i4++) {
29345
- const rect = cards[i4].getBoundingClientRect();
29346
- if (clientY < rect.top + rect.height / 2) return i4;
29347
- }
29348
- return cards.length;
29349
- }
29350
29582
  #highlightColumn(column) {
29351
29583
  this.columnTargets.forEach((c4) => {
29352
29584
  c4.classList.toggle("pu-kanban-drop-target", c4 === column);
@@ -29357,6 +29589,254 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
29357
29589
  }
29358
29590
  };
29359
29591
 
29592
+ // src/js/controllers/positioned_controller.js
29593
+ var DRAGGING_CLASS = "opacity-30";
29594
+ var positioned_controller_default = class extends Controller {
29595
+ static values = { urlTemplate: String, axis: { type: String, default: "vertical" } };
29596
+ connect() {
29597
+ this.draggedRow = null;
29598
+ this.onDragStart = this.#onDragStart.bind(this);
29599
+ this.onDragOver = this.#onDragOver.bind(this);
29600
+ this.onDragLeave = this.#onDragLeave.bind(this);
29601
+ this.onDrop = this.#onDrop.bind(this);
29602
+ this.onDragEnd = this.#onDragEnd.bind(this);
29603
+ this.onKeyDown = this.#onKeyDown.bind(this);
29604
+ this.element.addEventListener("dragstart", this.onDragStart);
29605
+ this.element.addEventListener("dragover", this.onDragOver);
29606
+ this.element.addEventListener("dragleave", this.onDragLeave);
29607
+ this.element.addEventListener("drop", this.onDrop);
29608
+ this.element.addEventListener("dragend", this.onDragEnd);
29609
+ this.element.addEventListener("keydown", this.onKeyDown);
29610
+ }
29611
+ disconnect() {
29612
+ this.element.removeEventListener("dragstart", this.onDragStart);
29613
+ this.element.removeEventListener("dragover", this.onDragOver);
29614
+ this.element.removeEventListener("dragleave", this.onDragLeave);
29615
+ this.element.removeEventListener("drop", this.onDrop);
29616
+ this.element.removeEventListener("dragend", this.onDragEnd);
29617
+ this.element.removeEventListener("keydown", this.onKeyDown);
29618
+ hideInsertionMarker();
29619
+ }
29620
+ // ─── drag lifecycle ─────────────────────────────────────────────────────────
29621
+ #onDragStart(event) {
29622
+ const grip = event.target.closest("[data-positioned-grip]");
29623
+ if (!grip) return;
29624
+ const row = this.#rowFor(grip);
29625
+ if (!row) return;
29626
+ this.draggedRow = row;
29627
+ beginDrag(event, row, {
29628
+ draggingClass: DRAGGING_CLASS,
29629
+ payload: row.dataset.positionedRowId,
29630
+ // The grip carries draggable="true", so without this the browser ghosts
29631
+ // the grip alone and the row looks like it never left.
29632
+ dragImage: row
29633
+ });
29634
+ }
29635
+ #onDragOver(event) {
29636
+ if (!this.draggedRow) return;
29637
+ if (!this.#sameGroupAs(event.target)) {
29638
+ hideInsertionMarker();
29639
+ return;
29640
+ }
29641
+ event.preventDefault();
29642
+ event.dataTransfer.dropEffect = "move";
29643
+ const others = this.#rows().filter((r4) => r4 !== this.draggedRow);
29644
+ showInsertionMarker(others, this.#dropIndex(event, others), {
29645
+ axis: this.axisValue === "horizontal" ? "horizontal" : "vertical",
29646
+ container: this.element
29647
+ });
29648
+ }
29649
+ #onDragLeave(event) {
29650
+ if (event.relatedTarget && this.element.contains(event.relatedTarget)) return;
29651
+ hideInsertionMarker();
29652
+ }
29653
+ #onDrop(event) {
29654
+ event.preventDefault();
29655
+ hideInsertionMarker();
29656
+ if (!this.draggedRow) return;
29657
+ const row = this.draggedRow;
29658
+ const others = this.#rows().filter((r4) => r4 !== row);
29659
+ this.#applyMove(row, others, this.#dropIndex(event, others));
29660
+ }
29661
+ // Where a drop at the cursor lands, in `items`' own order.
29662
+ //
29663
+ // A table stacks: one row per line, so the vertical midpoint test is the
29664
+ // whole story. A grid WRAPS: cards flow left-to-right and then onto a new
29665
+ // line, and reading that layout off clientY alone would collapse every card
29666
+ // on a line into a single slot — the drop would land at the start of the row
29667
+ // no matter which gap the user aimed at.
29668
+ //
29669
+ // But clientX alone is no better, because every visual row starts at the same
29670
+ // left edge: x=250 is "after the third card" in row 1 and row 4 alike. So
29671
+ // narrow to the row the cursor is over FIRST, then let the horizontal
29672
+ // midpoint test choose the gap inside it.
29673
+ #dropIndex(event, items) {
29674
+ if (this.axisValue !== "horizontal") return computeDropIndex(event.clientY, items);
29675
+ if (items.length === 0) return 0;
29676
+ const rows = this.#visualRows(items);
29677
+ if (event.clientY < rows[0].top) return 0;
29678
+ const row = rows.find((r4) => event.clientY <= r4.bottom);
29679
+ if (!row) return items.length;
29680
+ return row.start + computeDropIndexHorizontal(event.clientX, items.slice(row.start, row.end));
29681
+ }
29682
+ // Groups `items` into the lines the browser actually laid them out on, as
29683
+ // {top, bottom, start, end} spans over `items`.
29684
+ //
29685
+ // Recovered from geometry rather than from a column count on purpose: the
29686
+ // grid is responsive (1/2/3/4 columns by breakpoint) and a definition may pin
29687
+ // its own, so the only thing that reliably knows how the cards wrapped is
29688
+ // where they ended up. A new line starts wherever an item's top edge leaves
29689
+ // the current one's — with a pixel of slack, since equal-height cards in a
29690
+ // row can still differ by a sub-pixel.
29691
+ #visualRows(items) {
29692
+ const rows = [];
29693
+ items.forEach((item, i4) => {
29694
+ const rect = item.getBoundingClientRect();
29695
+ const row = rows[rows.length - 1];
29696
+ if (row && Math.abs(rect.top - row.top) <= 1) {
29697
+ row.end = i4 + 1;
29698
+ row.bottom = Math.max(row.bottom, rect.bottom);
29699
+ } else {
29700
+ rows.push({ top: rect.top, bottom: rect.bottom, start: i4, end: i4 + 1 });
29701
+ }
29702
+ });
29703
+ return rows;
29704
+ }
29705
+ #onDragEnd(_event) {
29706
+ if (!this.draggedRow) return;
29707
+ endDrag(this.draggedRow, { draggingClass: DRAGGING_CLASS });
29708
+ this.draggedRow = null;
29709
+ }
29710
+ // ─── keyboard ───────────────────────────────────────────────────────────────
29711
+ #onKeyDown(event) {
29712
+ if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
29713
+ const grip = event.target.closest("[data-positioned-grip]");
29714
+ if (!grip) return;
29715
+ const row = this.#rowFor(grip);
29716
+ if (!row) return;
29717
+ event.preventDefault();
29718
+ const rows = this.#rows();
29719
+ const from = rows.indexOf(row);
29720
+ const to = event.key === "ArrowUp" ? from - 1 : from + 1;
29721
+ if (to < 0 || to >= rows.length) return;
29722
+ const others = rows.filter((r4) => r4 !== row);
29723
+ this.#applyMove(row, others, to);
29724
+ grip.focus();
29725
+ }
29726
+ // ─── the move itself ────────────────────────────────────────────────────────
29727
+ // Moves `row` to `index` among `others` (which must already exclude `row`),
29728
+ // optimistically, then tells the server. Shared by drop and keyboard so the
29729
+ // two can never compute neighbours differently.
29730
+ #applyMove(row, others, index) {
29731
+ const before = others[index] ?? null;
29732
+ const after = others[index - 1] ?? null;
29733
+ if (row.nextElementSibling === before && row.previousElementSibling === after) return;
29734
+ const restoreAnchor = row.nextSibling;
29735
+ row.parentElement.insertBefore(row, before);
29736
+ this.#submit(row, restoreAnchor, {
29737
+ // The ids of the row's VISIBLE neighbours. A blank one means "nothing on
29738
+ // that side of my page" — the server treats that as a claim about the
29739
+ // viewport, not about the positioning group, and looks the real boundary
29740
+ // neighbour up itself (see PositionActions#resolve_position_boundaries).
29741
+ prevId: after?.dataset.positionedRowId ?? "",
29742
+ nextId: before?.dataset.positionedRowId ?? "",
29743
+ toIndex: index
29744
+ });
29745
+ }
29746
+ // `row` is the already-moved element and `restoreAnchor` the sibling it sat
29747
+ // in front of beforehand, so every path that does not end in the server's own
29748
+ // truth can undo the optimistic move rather than leave it standing.
29749
+ async #submit(row, restoreAnchor, { prevId, nextId, toIndex }) {
29750
+ const recordId = row.dataset.positionedRowId;
29751
+ const url = this.urlTemplateValue.replace("__ID__", recordId) + window.location.search;
29752
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content ?? "";
29753
+ try {
29754
+ const response = await fetch(url, {
29755
+ method: "POST",
29756
+ headers: {
29757
+ "Accept": "text/vnd.turbo-stream.html",
29758
+ "Content-Type": "application/x-www-form-urlencoded",
29759
+ "X-CSRF-Token": csrfToken
29760
+ },
29761
+ body: new URLSearchParams({
29762
+ prev_id: prevId,
29763
+ next_id: nextId,
29764
+ to_index: toIndex
29765
+ }),
29766
+ credentials: "same-origin"
29767
+ });
29768
+ if (response.status === 204) return;
29769
+ const contentType = response.headers.get("Content-Type") || "";
29770
+ if (contentType.includes("text/vnd.turbo-stream.html") && window.Turbo) {
29771
+ const focusedRowId = this.#focusedRowId();
29772
+ window.Turbo.renderStreamMessage(await response.text());
29773
+ if (focusedRowId) this.#restoreFocus(focusedRowId);
29774
+ } else if (!response.ok) {
29775
+ console.error(`[positioned] reposition rejected (${response.status}); reverting the move`);
29776
+ this.#revertMove(row, restoreAnchor);
29777
+ } else {
29778
+ console.warn("[positioned] reposition returned a non-stream response (session expired?); reverting the move");
29779
+ this.#revertMove(row, restoreAnchor);
29780
+ }
29781
+ } catch (error2) {
29782
+ console.error("[positioned] reposition request failed:", error2);
29783
+ this.#revertMove(row, restoreAnchor);
29784
+ }
29785
+ }
29786
+ // Undoes the optimistic move. Only meaningful while the row is still the node
29787
+ // we moved: once a turbo-stream has replaced the collection the server's truth
29788
+ // owns the DOM, and this instance's nodes are detached.
29789
+ //
29790
+ // A missing/relocated anchor falls back to appending, which is also what a
29791
+ // null anchor legitimately means — the row was last before the move.
29792
+ #revertMove(row, anchor) {
29793
+ const parent = row.parentElement;
29794
+ if (!parent || !row.isConnected) return;
29795
+ const refocus = row.contains(document.activeElement);
29796
+ parent.insertBefore(row, anchor?.parentNode === parent ? anchor : null);
29797
+ if (refocus) row.querySelector("[data-positioned-grip]")?.focus();
29798
+ }
29799
+ // ─── helpers ────────────────────────────────────────────────────────────────
29800
+ #rows() {
29801
+ return [...this.element.querySelectorAll("[data-positioned-row-id]")];
29802
+ }
29803
+ #rowFor(element) {
29804
+ return element.closest("[data-positioned-row-id]");
29805
+ }
29806
+ // Whether the row under the cursor shares the dragged row's positioning
29807
+ // group. An UNSCOPED resource emits no group attribute at all, so both sides
29808
+ // read undefined and every row matches — the guard costs nothing there.
29809
+ //
29810
+ // Pointing at no row (the gutter below the last one, the wrapper's padding)
29811
+ // counts as a match: the drop resolves against the collection's own ends,
29812
+ // which are in the dragged row's group by construction.
29813
+ #sameGroupAs(target) {
29814
+ const row = this.#rowFor(target);
29815
+ if (!row) return true;
29816
+ return row.dataset.positionedGroup === this.draggedRow.dataset.positionedGroup;
29817
+ }
29818
+ #focusedRowId() {
29819
+ const grip = document.activeElement?.closest?.("[data-positioned-grip]");
29820
+ return grip ? this.#rowFor(grip)?.dataset.positionedRowId : null;
29821
+ }
29822
+ // Deferred a frame: renderStreamMessage resolves its render asynchronously,
29823
+ // so the replacement rows are not in the document yet when it returns.
29824
+ //
29825
+ // Queried off `document`, NOT `this.element`. The stream replaces the contents
29826
+ // of the collection wrapper (Page::Index.collection_dom_id), and this
29827
+ // controller's element — the div around the <table>, or the grid div — is one
29828
+ // of the nodes it discards. By the time this callback runs `this.element` is
29829
+ // detached, so scoping the lookup to it finds nothing and focus is silently
29830
+ // lost on exactly the path this method exists to cover: a keyboard move that
29831
+ // triggered a rebalance. A fresh controller is already connected to the
29832
+ // replacement; `document` is the only handle that spans both.
29833
+ #restoreFocus(rowId) {
29834
+ requestAnimationFrame(() => {
29835
+ document.querySelector(`[data-positioned-row-id="${CSS.escape(rowId)}"] [data-positioned-grip]`)?.focus();
29836
+ });
29837
+ }
29838
+ };
29839
+
29360
29840
  // src/js/controllers/currency_input_controller.js
29361
29841
  var currency_input_controller_default = class extends Controller {
29362
29842
  static targets = ["prefix", "field"];
@@ -29375,6 +29855,106 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
29375
29855
  }
29376
29856
  };
29377
29857
 
29858
+ // src/js/controllers/breadcrumbs_controller.js
29859
+ var breadcrumbs_controller_default = class extends Controller {
29860
+ static targets = ["list", "item", "last", "overflow", "menuItem"];
29861
+ connect() {
29862
+ this.menuItems = this.menuItemTargets;
29863
+ this.observer = new ResizeObserver(() => this.reflow());
29864
+ this.observer.observe(this.element);
29865
+ this.reflow();
29866
+ document.fonts?.ready.then(() => this.reflow());
29867
+ }
29868
+ disconnect() {
29869
+ this.observer.disconnect();
29870
+ }
29871
+ reflow() {
29872
+ if (!this.element.isConnected || !this.hasListTarget) return;
29873
+ this.itemTargets.forEach((el) => this.#show(el));
29874
+ if (this.hasOverflowTarget) this.#hide(this.overflowTarget);
29875
+ if (this.hasLastTarget) this.lastTarget.classList.add("shrink-0");
29876
+ if (this.#overflows()) {
29877
+ if (this.hasOverflowTarget) {
29878
+ this.#show(this.overflowTarget);
29879
+ for (const item of this.itemTargets) {
29880
+ if (!this.#overflows()) break;
29881
+ this.#hide(item);
29882
+ }
29883
+ }
29884
+ if (this.#overflows() && this.hasLastTarget) {
29885
+ this.lastTarget.classList.remove("shrink-0");
29886
+ }
29887
+ }
29888
+ this.#syncMenu();
29889
+ }
29890
+ // Measured on the nav rather than the <ol>, because the nav is the element
29891
+ // that actually clips (`overflow-hidden`). A non-scrolling box reports
29892
+ // overflow inconsistently once direction is rtl; the clipping box doesn't.
29893
+ #overflows() {
29894
+ return this.element.scrollWidth > this.element.clientWidth;
29895
+ }
29896
+ #show(el) {
29897
+ el.classList.remove("hidden");
29898
+ el.classList.add("flex");
29899
+ }
29900
+ #hide(el) {
29901
+ el.classList.remove("flex");
29902
+ el.classList.add("hidden");
29903
+ }
29904
+ // A menu row is shown exactly when its inline twin has been folded away.
29905
+ #syncMenu() {
29906
+ if (!this.hasOverflowTarget) return;
29907
+ const folded = this.itemTargets.map((el) => el.classList.contains("hidden"));
29908
+ this.menuItems.forEach((row, i4) => {
29909
+ row.classList.toggle("hidden", !folded[i4]);
29910
+ });
29911
+ if (!folded.some(Boolean)) this.#closeOverflowMenu();
29912
+ }
29913
+ // The trail grew back far enough that the control is gone. Its menu may
29914
+ // still be open — and teleported to <body>, so hiding the control does not
29915
+ // hide the menu. Popper would go on positioning it against a display:none
29916
+ // trigger, whose rect is all zeros, parking the menu in the viewport corner.
29917
+ #closeOverflowMenu() {
29918
+ const host = this.overflowTarget.querySelector('[data-controller~="resource-drop-down"]');
29919
+ const dropdown = this.application.getControllerForElementAndIdentifier(host, "resource-drop-down");
29920
+ if (dropdown?.visible) dropdown.hide();
29921
+ }
29922
+ };
29923
+
29924
+ // src/js/controllers/run_progress_controller.js
29925
+ var run_progress_controller_default = class extends Controller {
29926
+ static values = {
29927
+ url: String,
29928
+ interval: { type: Number, default: 2e3 },
29929
+ // Set by the server on the LAST poll only — see AsyncRunProgress#poll_attributes.
29930
+ finished: Boolean
29931
+ };
29932
+ connect() {
29933
+ if (this.finishedValue) return this.reloadPage();
29934
+ this.schedule();
29935
+ }
29936
+ disconnect() {
29937
+ if (this.timer) clearTimeout(this.timer);
29938
+ this.timer = null;
29939
+ }
29940
+ schedule() {
29941
+ this.timer = setTimeout(() => this.refresh(), this.intervalValue);
29942
+ }
29943
+ reloadPage() {
29944
+ window.Turbo?.visit(window.location.href, { action: "replace" });
29945
+ }
29946
+ refresh() {
29947
+ const frame = this.element.closest("turbo-frame");
29948
+ if (!frame) return;
29949
+ this.schedule();
29950
+ if (frame.src) {
29951
+ frame.reload();
29952
+ } else {
29953
+ frame.src = this.urlValue;
29954
+ }
29955
+ }
29956
+ };
29957
+
29378
29958
  // src/js/controllers/register_controllers.js
29379
29959
  function register_controllers_default(application2) {
29380
29960
  application2.register("password-visibility", password_visibility_controller_default);
@@ -29415,7 +29995,10 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
29415
29995
  application2.register("dirty-form-guard", dirty_form_guard_controller_default);
29416
29996
  application2.register("wizard", wizard_controller_default);
29417
29997
  application2.register("kanban", kanban_controller_default);
29998
+ application2.register("positioned", positioned_controller_default);
29418
29999
  application2.register("currency-input", currency_input_controller_default);
30000
+ application2.register("breadcrumbs", breadcrumbs_controller_default);
30001
+ application2.register("run-progress", run_progress_controller_default);
29419
30002
  }
29420
30003
 
29421
30004
  // src/js/turbo/turbo_actions.js
@@ -29573,7 +30156,7 @@ cropperjs/dist/cropper.js:
29573
30156
  *)
29574
30157
 
29575
30158
  dompurify/dist/purify.es.mjs:
29576
- (*! @license DOMPurify 3.4.11 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.11/LICENSE *)
30159
+ (*! @license DOMPurify 3.4.14 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.14/LICENSE *)
29577
30160
 
29578
30161
  @uppy/utils/lib/Translator.js:
29579
30162
  (**