plutonium 0.60.5 → 0.61.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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +19 -1
  3. data/.claude/skills/plutonium-app/SKILL.md +41 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +40 -0
  5. data/.claude/skills/plutonium-behavior/SKILL.md +47 -1
  6. data/.claude/skills/plutonium-kanban/SKILL.md +313 -0
  7. data/.claude/skills/plutonium-resource/SKILL.md +40 -0
  8. data/.claude/skills/plutonium-tenancy/SKILL.md +43 -0
  9. data/.claude/skills/plutonium-testing/SKILL.md +38 -0
  10. data/.claude/skills/plutonium-ui/SKILL.md +51 -0
  11. data/.claude/skills/plutonium-wizard/SKILL.md +469 -0
  12. data/.cliff.toml +6 -0
  13. data/Appraisals +3 -0
  14. data/CHANGELOG.md +549 -439
  15. data/CLAUDE.md +15 -7
  16. data/app/assets/plutonium.css +1 -1
  17. data/app/assets/plutonium.js +895 -193
  18. data/app/assets/plutonium.js.map +4 -4
  19. data/app/assets/plutonium.min.js +53 -53
  20. data/app/assets/plutonium.min.js.map +4 -4
  21. data/app/views/layouts/basic.html.erb +7 -0
  22. data/app/views/plutonium/_flash_toasts.html.erb +2 -46
  23. data/app/views/plutonium/_toast.html.erb +52 -0
  24. data/app/views/resource/_resource_kanban.html.erb +1 -0
  25. data/db/migrate/wizard/20260615000001_create_plutonium_wizard_sessions.rb +57 -0
  26. data/docs/.vitepress/config.ts +24 -0
  27. data/docs/guides/index.md +2 -0
  28. data/docs/guides/kanban.md +447 -0
  29. data/docs/guides/wizards.md +447 -0
  30. data/docs/public/images/guides/kanban-after-move.png +0 -0
  31. data/docs/public/images/guides/kanban-board-light.png +0 -0
  32. data/docs/public/images/guides/kanban-board.png +0 -0
  33. data/docs/public/images/guides/kanban-show-centered-modal.png +0 -0
  34. data/docs/public/images/guides/kanban-wip-toast.png +0 -0
  35. data/docs/public/images/guides/wizards-chooser.png +0 -0
  36. data/docs/public/images/guides/wizards-completed.png +0 -0
  37. data/docs/public/images/guides/wizards-index-action.png +0 -0
  38. data/docs/public/images/guides/wizards-repeater.png +0 -0
  39. data/docs/public/images/guides/wizards-review.png +0 -0
  40. data/docs/public/images/guides/wizards-step.png +0 -0
  41. data/docs/reference/behavior/policies.md +1 -1
  42. data/docs/reference/index.md +14 -0
  43. data/docs/reference/kanban/authorization.md +62 -0
  44. data/docs/reference/kanban/dsl.md +293 -0
  45. data/docs/reference/kanban/index.md +40 -0
  46. data/docs/reference/kanban/positioning.md +162 -0
  47. data/docs/reference/resource/definition.md +16 -0
  48. data/docs/reference/ui/forms.md +36 -0
  49. data/docs/reference/ui/pages.md +2 -0
  50. data/docs/reference/wizard/anchoring-resume.md +194 -0
  51. data/docs/reference/wizard/dsl.md +332 -0
  52. data/docs/reference/wizard/index.md +33 -0
  53. data/docs/reference/wizard/one-time.md +129 -0
  54. data/docs/reference/wizard/registration-launch.md +177 -0
  55. data/docs/reference/wizard/storage-config.md +151 -0
  56. data/docs/superpowers/plans/2026-06-14-form-sectioning.md +2 -2
  57. data/docs/superpowers/plans/2026-06-15-wizard-dsl.md +1619 -0
  58. data/docs/superpowers/plans/2026-06-15-wizard-dsl.md.tasks.json +68 -0
  59. data/docs/superpowers/plans/2026-06-26-kanban-dsl.md +1128 -0
  60. data/docs/superpowers/plans/2026-06-26-kanban-dsl.md.tasks.json +24 -0
  61. data/docs/superpowers/specs/2026-06-15-wizard-dsl-design.md +836 -0
  62. data/docs/superpowers/specs/2026-06-15-wizard-dsl-examples.rb +245 -0
  63. data/docs/superpowers/specs/2026-06-17-wizard-relaunch-prompt-design.md +86 -0
  64. data/docs/superpowers/specs/2026-06-18-wizard-attachments-design.md +101 -0
  65. data/docs/superpowers/specs/2026-06-18-wizard-hosting-design.md +220 -0
  66. data/docs/superpowers/specs/2026-06-26-kanban-dsl-design.md +388 -0
  67. data/gemfiles/postgres.gemfile +8 -0
  68. data/gemfiles/postgres.gemfile.lock +321 -0
  69. data/gemfiles/rails_7.gemfile +1 -0
  70. data/gemfiles/rails_7.gemfile.lock +1 -1
  71. data/gemfiles/rails_8.0.gemfile +1 -0
  72. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  73. data/gemfiles/rails_8.1.gemfile +1 -0
  74. data/gemfiles/rails_8.1.gemfile.lock +14 -1
  75. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +6 -1
  76. data/lib/plutonium/action/base.rb +9 -0
  77. data/lib/plutonium/auth/rodauth.rb +1 -2
  78. data/lib/plutonium/configuration.rb +4 -0
  79. data/lib/plutonium/core/controller.rb +20 -1
  80. data/lib/plutonium/definition/base.rb +25 -0
  81. data/lib/plutonium/definition/form_layout.rb +54 -35
  82. data/lib/plutonium/definition/index_views.rb +54 -1
  83. data/lib/plutonium/definition/wizards.rb +209 -0
  84. data/lib/plutonium/invites/concerns/invite_token.rb +9 -0
  85. data/lib/plutonium/invites/concerns/invite_user.rb +9 -0
  86. data/lib/plutonium/invites/controller.rb +4 -1
  87. data/lib/plutonium/kanban/action.rb +7 -0
  88. data/lib/plutonium/kanban/board.rb +40 -0
  89. data/lib/plutonium/kanban/broadcaster.rb +54 -0
  90. data/lib/plutonium/kanban/column.rb +69 -0
  91. data/lib/plutonium/kanban/context.rb +15 -0
  92. data/lib/plutonium/kanban/dsl.rb +71 -0
  93. data/lib/plutonium/kanban/grouping.rb +51 -0
  94. data/lib/plutonium/kanban/positioning.rb +75 -0
  95. data/lib/plutonium/kanban.rb +11 -0
  96. data/lib/plutonium/migrations.rb +40 -0
  97. data/lib/plutonium/positioning.rb +146 -0
  98. data/lib/plutonium/railtie.rb +33 -0
  99. data/lib/plutonium/resource/controller.rb +2 -0
  100. data/lib/plutonium/resource/controllers/crud_actions.rb +1 -1
  101. data/lib/plutonium/resource/controllers/kanban_actions.rb +455 -0
  102. data/lib/plutonium/resource/controllers/wizard_actions.rb +165 -0
  103. data/lib/plutonium/resource/policy.rb +8 -0
  104. data/lib/plutonium/routing/mapper_extensions.rb +44 -0
  105. data/lib/plutonium/routing/wizard_registration.rb +289 -0
  106. data/lib/plutonium/ui/display/resource.rb +17 -12
  107. data/lib/plutonium/ui/form/base.rb +19 -5
  108. data/lib/plutonium/ui/form/components/password.rb +126 -0
  109. data/lib/plutonium/ui/form/components/uppy.rb +6 -3
  110. data/lib/plutonium/ui/form/options/inferred_types.rb +20 -0
  111. data/lib/plutonium/ui/form/resource.rb +1 -1
  112. data/lib/plutonium/ui/form/wizard.rb +63 -0
  113. data/lib/plutonium/ui/grid/card.rb +16 -5
  114. data/lib/plutonium/ui/kanban/card.rb +67 -0
  115. data/lib/plutonium/ui/kanban/color_dot.rb +36 -0
  116. data/lib/plutonium/ui/kanban/column.rb +324 -0
  117. data/lib/plutonium/ui/kanban/resource.rb +212 -0
  118. data/lib/plutonium/ui/layout/resource_layout.rb +7 -1
  119. data/lib/plutonium/ui/modal/base.rb +30 -3
  120. data/lib/plutonium/ui/modal/centered.rb +5 -2
  121. data/lib/plutonium/ui/page/index.rb +1 -0
  122. data/lib/plutonium/ui/page/show.rb +23 -0
  123. data/lib/plutonium/ui/page/wizard.rb +371 -0
  124. data/lib/plutonium/ui/page/wizard_chooser.rb +97 -0
  125. data/lib/plutonium/ui/page/wizard_completed.rb +86 -0
  126. data/lib/plutonium/ui/table/base.rb +1 -1
  127. data/lib/plutonium/ui/table/components/view_switcher.rb +2 -1
  128. data/lib/plutonium/ui/wizard/review.rb +196 -0
  129. data/lib/plutonium/ui/wizard/stepper.rb +122 -0
  130. data/lib/plutonium/ui/wizard/summary_display.rb +59 -0
  131. data/lib/plutonium/version.rb +1 -1
  132. data/lib/plutonium/wizard/attachment_data.rb +42 -0
  133. data/lib/plutonium/wizard/attachments.rb +226 -0
  134. data/lib/plutonium/wizard/base.rb +216 -0
  135. data/lib/plutonium/wizard/base_controller.rb +31 -0
  136. data/lib/plutonium/wizard/configuration.rb +42 -0
  137. data/lib/plutonium/wizard/controller.rb +162 -0
  138. data/lib/plutonium/wizard/data.rb +134 -0
  139. data/lib/plutonium/wizard/driving.rb +639 -0
  140. data/lib/plutonium/wizard/dsl.rb +336 -0
  141. data/lib/plutonium/wizard/errors.rb +27 -0
  142. data/lib/plutonium/wizard/field_capture.rb +157 -0
  143. data/lib/plutonium/wizard/field_importer.rb +208 -0
  144. data/lib/plutonium/wizard/gate.rb +171 -0
  145. data/lib/plutonium/wizard/instance_key.rb +97 -0
  146. data/lib/plutonium/wizard/lazy_persisted.rb +77 -0
  147. data/lib/plutonium/wizard/resume.rb +250 -0
  148. data/lib/plutonium/wizard/review_step.rb +48 -0
  149. data/lib/plutonium/wizard/route_resolution.rb +40 -0
  150. data/lib/plutonium/wizard/runner.rb +684 -0
  151. data/lib/plutonium/wizard/session.rb +53 -0
  152. data/lib/plutonium/wizard/state.rb +35 -0
  153. data/lib/plutonium/wizard/step.rb +61 -0
  154. data/lib/plutonium/wizard/step_adapter.rb +103 -0
  155. data/lib/plutonium/wizard/store/active_record.rb +174 -0
  156. data/lib/plutonium/wizard/store/base.rb +42 -0
  157. data/lib/plutonium/wizard/store/memory.rb +44 -0
  158. data/lib/plutonium/wizard/sweep_job.rb +76 -0
  159. data/lib/plutonium/wizard.rb +86 -0
  160. data/lib/plutonium.rb +5 -0
  161. data/lib/rodauth/features/case_insensitive_login.rb +1 -1
  162. data/lib/tasks/release.rake +144 -191
  163. data/package.json +3 -3
  164. data/src/css/components.css +132 -0
  165. data/src/js/controllers/attachment_input_controller.js +15 -1
  166. data/src/js/controllers/dirty_form_guard_controller.js +155 -27
  167. data/src/js/controllers/kanban_controller.js +330 -0
  168. data/src/js/controllers/password_sentinel_controller.js +39 -0
  169. data/src/js/controllers/register_controllers.js +6 -0
  170. data/src/js/controllers/remote_modal_controller.js +10 -0
  171. data/src/js/controllers/row_click_controller.js +14 -1
  172. data/src/js/controllers/wizard_controller.js +54 -0
  173. data/src/js/turbo/turbo_confirm.js +1 -1
  174. data/yarn.lock +271 -282
  175. metadata +100 -1
@@ -6,7 +6,11 @@
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __commonJS = (cb, mod) => function __require() {
9
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ try {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ } catch (e4) {
12
+ throw mod = 0, e4;
13
+ }
10
14
  };
11
15
  var __copyProps = (to, from, except, desc) => {
12
16
  if (from && typeof from === "object" || typeof from === "function") {
@@ -13655,7 +13659,7 @@
13655
13659
  var mathMl$1 = freeze(["math", "menclose", "merror", "mfenced", "mfrac", "mglyph", "mi", "mlabeledtr", "mmultiscripts", "mn", "mo", "mover", "mpadded", "mphantom", "mroot", "mrow", "ms", "mspace", "msqrt", "mstyle", "msub", "msup", "msubsup", "mtable", "mtd", "mtext", "mtr", "munder", "munderover", "mprescripts"]);
13656
13660
  var mathMlDisallowed = freeze(["maction", "maligngroup", "malignmark", "mlongdiv", "mscarries", "mscarry", "msgroup", "mstack", "msline", "msrow", "semantics", "annotation", "annotation-xml", "mprescripts", "none"]);
13657
13661
  var text = freeze(["#text"]);
13658
- var html = freeze(["accept", "action", "align", "alt", "autocapitalize", "autocomplete", "autopictureinpicture", "autoplay", "background", "bgcolor", "border", "capture", "cellpadding", "cellspacing", "checked", "cite", "class", "clear", "color", "cols", "colspan", "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"]);
13662
+ 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"]);
13659
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"]);
13660
13664
  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"]);
13661
13665
  var xml = freeze(["xlink:href", "xml:id", "xlink:title", "xml:space", "xmlns:xlink"]);
@@ -13675,13 +13679,26 @@
13675
13679
  );
13676
13680
  var DOCTYPE_NAME = seal(/^html$/i);
13677
13681
  var CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
13682
+ var ELEMENT_MARKUP_PROBE = seal(/<[/\w!]/g);
13683
+ var COMMENT_MARKUP_PROBE = seal(/<[/\w]/g);
13684
+ var FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i);
13685
+ var SELF_CLOSING_TAG = seal(/\/>/i);
13678
13686
  var NODE_TYPE = {
13679
13687
  element: 1,
13688
+ attribute: 2,
13680
13689
  text: 3,
13690
+ cdataSection: 4,
13691
+ entityReference: 5,
13692
+ // Deprecated
13693
+ entityNode: 6,
13681
13694
  // Deprecated
13682
- progressingInstruction: 7,
13695
+ processingInstruction: 7,
13683
13696
  comment: 8,
13684
- document: 9
13697
+ document: 9,
13698
+ documentType: 10,
13699
+ documentFragment: 11,
13700
+ notation: 12
13701
+ // Deprecated
13685
13702
  };
13686
13703
  var getGlobal = function getGlobal2() {
13687
13704
  return typeof window === "undefined" ? null : window;
@@ -13723,10 +13740,13 @@
13723
13740
  uponSanitizeShadowNode: []
13724
13741
  };
13725
13742
  };
13743
+ var _resolveSetOption = function _resolveSetOption2(cfg, key, fallback, options2) {
13744
+ return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options2.base ? clone(options2.base) : {}, cfg[key], options2.transform) : fallback;
13745
+ };
13726
13746
  function createDOMPurify() {
13727
13747
  let window2 = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
13728
13748
  const DOMPurify = (root) => createDOMPurify(root);
13729
- DOMPurify.version = "3.4.3";
13749
+ DOMPurify.version = "3.4.11";
13730
13750
  DOMPurify.removed = [];
13731
13751
  if (!window2 || !window2.document || window2.document.nodeType !== NODE_TYPE.document || !window2.Element) {
13732
13752
  DOMPurify.isSupported = false;
@@ -13735,13 +13755,21 @@
13735
13755
  let document2 = window2.document;
13736
13756
  const originalDocument = document2;
13737
13757
  const currentScript = originalDocument.currentScript;
13738
- const DocumentFragment = window2.DocumentFragment, HTMLTemplateElement2 = window2.HTMLTemplateElement, Node2 = window2.Node, Element2 = window2.Element, NodeFilter = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap, NamedNodeMap = _window$NamedNodeMap === void 0 ? window2.NamedNodeMap || window2.MozNamedAttrMap : _window$NamedNodeMap, HTMLFormElement2 = window2.HTMLFormElement, DOMParser2 = window2.DOMParser, trustedTypes = window2.trustedTypes;
13758
+ window2.DocumentFragment;
13759
+ const HTMLTemplateElement2 = window2.HTMLTemplateElement, Node2 = window2.Node, Element2 = window2.Element, NodeFilter = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap;
13760
+ _window$NamedNodeMap === void 0 ? window2.NamedNodeMap || window2.MozNamedAttrMap : _window$NamedNodeMap;
13761
+ window2.HTMLFormElement;
13762
+ const DOMParser2 = window2.DOMParser, trustedTypes = window2.trustedTypes;
13739
13763
  const ElementPrototype = Element2.prototype;
13740
13764
  const cloneNode = lookupGetter(ElementPrototype, "cloneNode");
13741
13765
  const remove = lookupGetter(ElementPrototype, "remove");
13742
13766
  const getNextSibling = lookupGetter(ElementPrototype, "nextSibling");
13743
13767
  const getChildNodes = lookupGetter(ElementPrototype, "childNodes");
13744
13768
  const getParentNode2 = lookupGetter(ElementPrototype, "parentNode");
13769
+ const getShadowRoot = lookupGetter(ElementPrototype, "shadowRoot");
13770
+ const getAttributes = lookupGetter(ElementPrototype, "attributes");
13771
+ const getNodeType = Node2 && Node2.prototype ? lookupGetter(Node2.prototype, "nodeType") : null;
13772
+ const getNodeName2 = Node2 && Node2.prototype ? lookupGetter(Node2.prototype, "nodeName") : null;
13745
13773
  if (typeof HTMLTemplateElement2 === "function") {
13746
13774
  const template = document2.createElement("template");
13747
13775
  if (template.content && template.content.ownerDocument) {
@@ -13750,6 +13778,39 @@
13750
13778
  }
13751
13779
  let trustedTypesPolicy;
13752
13780
  let emptyHTML = "";
13781
+ let defaultTrustedTypesPolicy;
13782
+ let defaultTrustedTypesPolicyResolved = false;
13783
+ let IN_TRUSTED_TYPES_POLICY = 0;
13784
+ const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy2() {
13785
+ if (IN_TRUSTED_TYPES_POLICY > 0) {
13786
+ throw typeErrorCreate('A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section of the README.');
13787
+ }
13788
+ };
13789
+ const _createTrustedHTML = function _createTrustedHTML2(html3) {
13790
+ _assertNotInTrustedTypesPolicy();
13791
+ IN_TRUSTED_TYPES_POLICY++;
13792
+ try {
13793
+ return trustedTypesPolicy.createHTML(html3);
13794
+ } finally {
13795
+ IN_TRUSTED_TYPES_POLICY--;
13796
+ }
13797
+ };
13798
+ const _createTrustedScriptURL = function _createTrustedScriptURL2(scriptUrl) {
13799
+ _assertNotInTrustedTypesPolicy();
13800
+ IN_TRUSTED_TYPES_POLICY++;
13801
+ try {
13802
+ return trustedTypesPolicy.createScriptURL(scriptUrl);
13803
+ } finally {
13804
+ IN_TRUSTED_TYPES_POLICY--;
13805
+ }
13806
+ };
13807
+ const _getDefaultTrustedTypesPolicy = function _getDefaultTrustedTypesPolicy2() {
13808
+ if (!defaultTrustedTypesPolicyResolved) {
13809
+ defaultTrustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
13810
+ defaultTrustedTypesPolicyResolved = true;
13811
+ }
13812
+ return defaultTrustedTypesPolicy;
13813
+ };
13753
13814
  const _document = document2, implementation = _document.implementation, createNodeIterator = _document.createNodeIterator, createDocumentFragment2 = _document.createDocumentFragment, getElementsByTagName = _document.getElementsByTagName;
13754
13815
  const importNode = originalDocument.importNode;
13755
13816
  let hooks = _createHooksMap();
@@ -13804,6 +13865,8 @@
13804
13865
  let SAFE_FOR_XML = true;
13805
13866
  let WHOLE_DOCUMENT = false;
13806
13867
  let SET_CONFIG = false;
13868
+ let SET_CONFIG_ALLOWED_TAGS = null;
13869
+ let SET_CONFIG_ALLOWED_ATTR = null;
13807
13870
  let FORCE_BODY = false;
13808
13871
  let RETURN_DOM = false;
13809
13872
  let RETURN_DOM_FRAGMENT = false;
@@ -13815,7 +13878,43 @@
13815
13878
  let IN_PLACE = false;
13816
13879
  let USE_PROFILES = {};
13817
13880
  let FORBID_CONTENTS = null;
13818
- const DEFAULT_FORBID_CONTENTS = addToSet({}, ["annotation-xml", "audio", "colgroup", "desc", "foreignobject", "head", "iframe", "math", "mi", "mn", "mo", "ms", "mtext", "noembed", "noframes", "noscript", "plaintext", "script", "style", "svg", "template", "thead", "title", "video", "xmp"]);
13881
+ const DEFAULT_FORBID_CONTENTS = addToSet({}, [
13882
+ "annotation-xml",
13883
+ "audio",
13884
+ "colgroup",
13885
+ "desc",
13886
+ "foreignobject",
13887
+ "head",
13888
+ "iframe",
13889
+ "math",
13890
+ "mi",
13891
+ "mn",
13892
+ "mo",
13893
+ "ms",
13894
+ "mtext",
13895
+ "noembed",
13896
+ "noframes",
13897
+ "noscript",
13898
+ "plaintext",
13899
+ "script",
13900
+ // <selectedcontent> mirrors the selected <option>'s subtree, cloned by
13901
+ // the UA (customizable <select>) — including any on* handlers — and the
13902
+ // engine re-mirrors synchronously whenever a removal changes which
13903
+ // option/selectedcontent is current, even inside DOMPurify's inert
13904
+ // DOMParser document. Hoisting its children on removal re-inserts a fresh
13905
+ // mirror target ahead of the walk, which the engine refills, looping
13906
+ // forever (DoS) and amplifying output. Dropping its content on removal
13907
+ // (rather than hoisting) breaks that cascade; the content is a duplicate
13908
+ // of the option, which is sanitized on its own. See campaign-3 F1/F6.
13909
+ "selectedcontent",
13910
+ "style",
13911
+ "svg",
13912
+ "template",
13913
+ "thead",
13914
+ "title",
13915
+ "video",
13916
+ "xmp"
13917
+ ]);
13819
13918
  let DATA_URI_TAGS = null;
13820
13919
  const DEFAULT_DATA_URI_TAGS = addToSet({}, ["audio", "video", "img", "source", "image", "track"]);
13821
13920
  let URI_SAFE_ATTRIBUTES = null;
@@ -13827,8 +13926,10 @@
13827
13926
  let IS_EMPTY_INPUT = false;
13828
13927
  let ALLOWED_NAMESPACES = null;
13829
13928
  const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
13830
- let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ["mi", "mo", "mn", "ms", "mtext"]);
13831
- let HTML_INTEGRATION_POINTS = addToSet({}, ["annotation-xml"]);
13929
+ const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze(["mi", "mo", "mn", "ms", "mtext"]);
13930
+ let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
13931
+ const DEFAULT_HTML_INTEGRATION_POINTS = freeze(["annotation-xml"]);
13932
+ let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
13832
13933
  const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ["title", "style", "font", "a", "script"]);
13833
13934
  let PARSER_MEDIA_TYPE = null;
13834
13935
  const SUPPORTED_PARSER_MEDIA_TYPES = ["application/xhtml+xml", "text/html"];
@@ -13851,14 +13952,32 @@
13851
13952
  PARSER_MEDIA_TYPE = // eslint-disable-next-line unicorn/prefer-includes
13852
13953
  SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE;
13853
13954
  transformCaseFunc = PARSER_MEDIA_TYPE === "application/xhtml+xml" ? stringToString : stringToLowerCase;
13854
- ALLOWED_TAGS = objectHasOwnProperty(cfg, "ALLOWED_TAGS") && arrayIsArray(cfg.ALLOWED_TAGS) ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
13855
- ALLOWED_ATTR = objectHasOwnProperty(cfg, "ALLOWED_ATTR") && arrayIsArray(cfg.ALLOWED_ATTR) ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
13856
- ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, "ALLOWED_NAMESPACES") && arrayIsArray(cfg.ALLOWED_NAMESPACES) ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
13857
- URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, "ADD_URI_SAFE_ATTR") && arrayIsArray(cfg.ADD_URI_SAFE_ATTR) ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;
13858
- DATA_URI_TAGS = objectHasOwnProperty(cfg, "ADD_DATA_URI_TAGS") && arrayIsArray(cfg.ADD_DATA_URI_TAGS) ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;
13859
- FORBID_CONTENTS = objectHasOwnProperty(cfg, "FORBID_CONTENTS") && arrayIsArray(cfg.FORBID_CONTENTS) ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
13860
- FORBID_TAGS = objectHasOwnProperty(cfg, "FORBID_TAGS") && arrayIsArray(cfg.FORBID_TAGS) ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
13861
- FORBID_ATTR = objectHasOwnProperty(cfg, "FORBID_ATTR") && arrayIsArray(cfg.FORBID_ATTR) ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
13955
+ ALLOWED_TAGS = _resolveSetOption(cfg, "ALLOWED_TAGS", DEFAULT_ALLOWED_TAGS, {
13956
+ transform: transformCaseFunc
13957
+ });
13958
+ ALLOWED_ATTR = _resolveSetOption(cfg, "ALLOWED_ATTR", DEFAULT_ALLOWED_ATTR, {
13959
+ transform: transformCaseFunc
13960
+ });
13961
+ ALLOWED_NAMESPACES = _resolveSetOption(cfg, "ALLOWED_NAMESPACES", DEFAULT_ALLOWED_NAMESPACES, {
13962
+ transform: stringToString
13963
+ });
13964
+ URI_SAFE_ATTRIBUTES = _resolveSetOption(cfg, "ADD_URI_SAFE_ATTR", DEFAULT_URI_SAFE_ATTRIBUTES, {
13965
+ transform: transformCaseFunc,
13966
+ base: DEFAULT_URI_SAFE_ATTRIBUTES
13967
+ });
13968
+ DATA_URI_TAGS = _resolveSetOption(cfg, "ADD_DATA_URI_TAGS", DEFAULT_DATA_URI_TAGS, {
13969
+ transform: transformCaseFunc,
13970
+ base: DEFAULT_DATA_URI_TAGS
13971
+ });
13972
+ FORBID_CONTENTS = _resolveSetOption(cfg, "FORBID_CONTENTS", DEFAULT_FORBID_CONTENTS, {
13973
+ transform: transformCaseFunc
13974
+ });
13975
+ FORBID_TAGS = _resolveSetOption(cfg, "FORBID_TAGS", clone({}), {
13976
+ transform: transformCaseFunc
13977
+ });
13978
+ FORBID_ATTR = _resolveSetOption(cfg, "FORBID_ATTR", clone({}), {
13979
+ transform: transformCaseFunc
13980
+ });
13862
13981
  USE_PROFILES = objectHasOwnProperty(cfg, "USE_PROFILES") ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === "object" ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false;
13863
13982
  ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false;
13864
13983
  ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false;
@@ -13877,8 +13996,8 @@
13877
13996
  IN_PLACE = cfg.IN_PLACE || false;
13878
13997
  IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI;
13879
13998
  NAMESPACE = typeof cfg.NAMESPACE === "string" ? cfg.NAMESPACE : HTML_NAMESPACE;
13880
- 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({}, ["mi", "mo", "mn", "ms", "mtext"]);
13881
- HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, "HTML_INTEGRATION_POINTS") && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === "object" ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, ["annotation-xml"]);
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);
13882
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);
13883
14002
  CUSTOM_ELEMENT_HANDLING = create(null);
13884
14003
  if (objectHasOwnProperty(customElementHandling, "tagNameCheck") && isRegexOrFunction(customElementHandling.tagNameCheck)) {
@@ -13890,6 +14009,7 @@
13890
14009
  if (objectHasOwnProperty(customElementHandling, "allowCustomizedBuiltInElements") && typeof customElementHandling.allowCustomizedBuiltInElements === "boolean") {
13891
14010
  CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements;
13892
14011
  }
14012
+ seal(CUSTOM_ELEMENT_HANDLING);
13893
14013
  if (SAFE_FOR_TEMPLATES) {
13894
14014
  ALLOW_DATA_ATTR = false;
13895
14015
  }
@@ -13973,14 +14093,23 @@
13973
14093
  if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== "function") {
13974
14094
  throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
13975
14095
  }
14096
+ const previousTrustedTypesPolicy = trustedTypesPolicy;
13976
14097
  trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
13977
- emptyHTML = trustedTypesPolicy.createHTML("");
14098
+ try {
14099
+ emptyHTML = _createTrustedHTML("");
14100
+ } catch (error2) {
14101
+ trustedTypesPolicy = previousTrustedTypesPolicy;
14102
+ throw error2;
14103
+ }
14104
+ } else if (cfg.TRUSTED_TYPES_POLICY === null) {
14105
+ trustedTypesPolicy = void 0;
14106
+ emptyHTML = "";
13978
14107
  } else {
13979
14108
  if (trustedTypesPolicy === void 0) {
13980
- trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
14109
+ trustedTypesPolicy = _getDefaultTrustedTypesPolicy();
13981
14110
  }
13982
- if (trustedTypesPolicy !== null && typeof emptyHTML === "string") {
13983
- emptyHTML = trustedTypesPolicy.createHTML("");
14111
+ if (trustedTypesPolicy && typeof emptyHTML === "string") {
14112
+ emptyHTML = _createTrustedHTML("");
13984
14113
  }
13985
14114
  }
13986
14115
  if (freeze) {
@@ -13990,6 +14119,33 @@
13990
14119
  };
13991
14120
  const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
13992
14121
  const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
14122
+ const _checkSvgNamespace = function _checkSvgNamespace2(tagName, parent, parentTagName) {
14123
+ if (parent.namespaceURI === HTML_NAMESPACE) {
14124
+ return tagName === "svg";
14125
+ }
14126
+ if (parent.namespaceURI === MATHML_NAMESPACE) {
14127
+ return tagName === "svg" && (parentTagName === "annotation-xml" || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
14128
+ }
14129
+ return Boolean(ALL_SVG_TAGS[tagName]);
14130
+ };
14131
+ const _checkMathMlNamespace = function _checkMathMlNamespace2(tagName, parent, parentTagName) {
14132
+ if (parent.namespaceURI === HTML_NAMESPACE) {
14133
+ return tagName === "math";
14134
+ }
14135
+ if (parent.namespaceURI === SVG_NAMESPACE) {
14136
+ return tagName === "math" && HTML_INTEGRATION_POINTS[parentTagName];
14137
+ }
14138
+ return Boolean(ALL_MATHML_TAGS[tagName]);
14139
+ };
14140
+ const _checkHtmlNamespace = function _checkHtmlNamespace2(tagName, parent, parentTagName) {
14141
+ if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
14142
+ return false;
14143
+ }
14144
+ if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
14145
+ return false;
14146
+ }
14147
+ return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
14148
+ };
13993
14149
  const _checkValidNamespace = function _checkValidNamespace2(element) {
13994
14150
  let parent = getParentNode2(element);
13995
14151
  if (!parent || !parent.tagName) {
@@ -14004,31 +14160,13 @@
14004
14160
  return false;
14005
14161
  }
14006
14162
  if (element.namespaceURI === SVG_NAMESPACE) {
14007
- if (parent.namespaceURI === HTML_NAMESPACE) {
14008
- return tagName === "svg";
14009
- }
14010
- if (parent.namespaceURI === MATHML_NAMESPACE) {
14011
- return tagName === "svg" && (parentTagName === "annotation-xml" || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
14012
- }
14013
- return Boolean(ALL_SVG_TAGS[tagName]);
14163
+ return _checkSvgNamespace(tagName, parent, parentTagName);
14014
14164
  }
14015
14165
  if (element.namespaceURI === MATHML_NAMESPACE) {
14016
- if (parent.namespaceURI === HTML_NAMESPACE) {
14017
- return tagName === "math";
14018
- }
14019
- if (parent.namespaceURI === SVG_NAMESPACE) {
14020
- return tagName === "math" && HTML_INTEGRATION_POINTS[parentTagName];
14021
- }
14022
- return Boolean(ALL_MATHML_TAGS[tagName]);
14166
+ return _checkMathMlNamespace(tagName, parent, parentTagName);
14023
14167
  }
14024
14168
  if (element.namespaceURI === HTML_NAMESPACE) {
14025
- if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
14026
- return false;
14027
- }
14028
- if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
14029
- return false;
14030
- }
14031
- return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
14169
+ return _checkHtmlNamespace(tagName, parent, parentTagName);
14032
14170
  }
14033
14171
  if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && ALLOWED_NAMESPACES[element.namespaceURI]) {
14034
14172
  return true;
@@ -14043,6 +14181,37 @@
14043
14181
  getParentNode2(node).removeChild(node);
14044
14182
  } catch (_4) {
14045
14183
  remove(node);
14184
+ if (!getParentNode2(node)) {
14185
+ throw typeErrorCreate("a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place");
14186
+ }
14187
+ }
14188
+ };
14189
+ const _neutralizeRoot = function _neutralizeRoot2(root) {
14190
+ const childNodes = getChildNodes(root);
14191
+ if (childNodes) {
14192
+ const snapshot = [];
14193
+ arrayForEach(childNodes, (child) => {
14194
+ arrayPush(snapshot, child);
14195
+ });
14196
+ arrayForEach(snapshot, (child) => {
14197
+ try {
14198
+ remove(child);
14199
+ } catch (_4) {
14200
+ }
14201
+ });
14202
+ }
14203
+ const attributes = getAttributes(root);
14204
+ if (attributes) {
14205
+ for (let i4 = attributes.length - 1; i4 >= 0; --i4) {
14206
+ const attribute = attributes[i4];
14207
+ const name = attribute && attribute.name;
14208
+ if (typeof name === "string") {
14209
+ try {
14210
+ root.removeAttribute(name);
14211
+ } catch (_4) {
14212
+ }
14213
+ }
14214
+ }
14046
14215
  }
14047
14216
  };
14048
14217
  const _removeAttribute = function _removeAttribute2(name, element) {
@@ -14072,6 +14241,39 @@
14072
14241
  }
14073
14242
  }
14074
14243
  };
14244
+ const _stripDisallowedAttributes = function _stripDisallowedAttributes2(element) {
14245
+ const attributes = getAttributes(element);
14246
+ if (!attributes) {
14247
+ return;
14248
+ }
14249
+ for (let i4 = attributes.length - 1; i4 >= 0; --i4) {
14250
+ const attribute = attributes[i4];
14251
+ const name = attribute && attribute.name;
14252
+ if (typeof name !== "string" || ALLOWED_ATTR[transformCaseFunc(name)]) {
14253
+ continue;
14254
+ }
14255
+ try {
14256
+ element.removeAttribute(name);
14257
+ } catch (_4) {
14258
+ }
14259
+ }
14260
+ };
14261
+ const _neutralizeSubtree = function _neutralizeSubtree2(root) {
14262
+ const stack = [root];
14263
+ while (stack.length > 0) {
14264
+ const node = stack.pop();
14265
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
14266
+ if (nodeType === NODE_TYPE.element) {
14267
+ _stripDisallowedAttributes(node);
14268
+ }
14269
+ const childNodes = getChildNodes(node);
14270
+ if (childNodes) {
14271
+ for (let i4 = childNodes.length - 1; i4 >= 0; --i4) {
14272
+ stack.push(childNodes[i4]);
14273
+ }
14274
+ }
14275
+ }
14276
+ };
14075
14277
  const _initDocument = function _initDocument2(dirty) {
14076
14278
  let doc = null;
14077
14279
  let leadingWhitespace = null;
@@ -14084,7 +14286,7 @@
14084
14286
  if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && NAMESPACE === HTML_NAMESPACE) {
14085
14287
  dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + "</body></html>";
14086
14288
  }
14087
- const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
14289
+ const dirtyPayload = trustedTypesPolicy ? _createTrustedHTML(dirty) : dirty;
14088
14290
  if (NAMESPACE === HTML_NAMESPACE) {
14089
14291
  try {
14090
14292
  doc = new DOMParser2().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);
@@ -14116,81 +14318,164 @@
14116
14318
  null
14117
14319
  );
14118
14320
  };
14321
+ const _stripTemplateExpressions = function _stripTemplateExpressions2(value) {
14322
+ value = stringReplace(value, MUSTACHE_EXPR$1, " ");
14323
+ value = stringReplace(value, ERB_EXPR$1, " ");
14324
+ value = stringReplace(value, TMPLIT_EXPR$1, " ");
14325
+ return value;
14326
+ };
14327
+ const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
14328
+ var _node$querySelectorAl;
14329
+ node.normalize();
14330
+ const walker = createNodeIterator.call(
14331
+ node.ownerDocument || node,
14332
+ node,
14333
+ // eslint-disable-next-line no-bitwise
14334
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION,
14335
+ null
14336
+ );
14337
+ let currentNode = walker.nextNode();
14338
+ while (currentNode) {
14339
+ currentNode.data = _stripTemplateExpressions(currentNode.data);
14340
+ currentNode = walker.nextNode();
14341
+ }
14342
+ const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, "template");
14343
+ if (templates) {
14344
+ arrayForEach(templates, (tmpl) => {
14345
+ if (_isDocumentFragment(tmpl.content)) {
14346
+ _scrubTemplateExpressions2(tmpl.content);
14347
+ }
14348
+ });
14349
+ }
14350
+ };
14119
14351
  const _isClobbered = function _isClobbered2(element) {
14120
- return element instanceof HTMLFormElement2 && (typeof element.nodeName !== "string" || typeof element.textContent !== "string" || typeof element.removeChild !== "function" || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== "function" || typeof element.setAttribute !== "function" || typeof element.namespaceURI !== "string" || typeof element.insertBefore !== "function" || typeof element.hasChildNodes !== "function");
14352
+ const realTagName = getNodeName2 ? getNodeName2(element) : null;
14353
+ if (typeof realTagName !== "string") {
14354
+ return false;
14355
+ }
14356
+ if (transformCaseFunc(realTagName) !== "form") {
14357
+ return false;
14358
+ }
14359
+ return typeof element.nodeName !== "string" || typeof element.textContent !== "string" || typeof element.removeChild !== "function" || // Realm-safe NamedNodeMap detection: equality against the cached
14360
+ // prototype getter. Clobbered .attributes (e.g. <input name="attributes">)
14361
+ // makes the direct read diverge from the cached read; a clean form
14362
+ // (same-realm OR foreign-realm) has both reads pointing at the same
14363
+ // canonical NamedNodeMap.
14364
+ element.attributes !== getAttributes(element) || typeof element.removeAttribute !== "function" || typeof element.setAttribute !== "function" || typeof element.namespaceURI !== "string" || typeof element.insertBefore !== "function" || typeof element.hasChildNodes !== "function" || // NodeType clobbering probe. Cached Node.prototype.nodeType getter
14365
+ // returns the integer 1 for any Element regardless of realm; direct
14366
+ // read on a clobbered form (e.g. <input name="nodeType">) returns
14367
+ // the named child element. Cheap addition — nodeType is read from
14368
+ // an internal slot, no serialization cost — and removes a residual
14369
+ // clobbering surface used by several mXSS / PI / comment branches
14370
+ // in _sanitizeElements that compare currentNode.nodeType directly.
14371
+ element.nodeType !== getNodeType(element) || // HTMLFormElement has [LegacyOverrideBuiltIns]: a descendant named
14372
+ // "childNodes" shadows the prototype getter. Direct reads of
14373
+ // form.childNodes from a clobbered form return the named child
14374
+ // instead of the real NodeList, so any walk that reads it directly
14375
+ // skips the form's real children. Compare the direct read to the
14376
+ // cached Node.prototype getter — when the form's named-property
14377
+ // getter intercepts the read, the two values differ and we flag
14378
+ // the form. This catches every clobbering child type (input,
14379
+ // select, etc.) regardless of whether the named child happens to
14380
+ // carry a numeric .length, which a typeof-based probe would miss
14381
+ // (e.g. HTMLSelectElement.length is a defined unsigned-long).
14382
+ element.childNodes !== getChildNodes(element);
14383
+ };
14384
+ const _isDocumentFragment = function _isDocumentFragment2(value) {
14385
+ if (!getNodeType || typeof value !== "object" || value === null) {
14386
+ return false;
14387
+ }
14388
+ try {
14389
+ return getNodeType(value) === NODE_TYPE.documentFragment;
14390
+ } catch (_4) {
14391
+ return false;
14392
+ }
14121
14393
  };
14122
14394
  const _isNode = function _isNode2(value) {
14123
- return typeof Node2 === "function" && value instanceof Node2;
14395
+ if (!getNodeType || typeof value !== "object" || value === null) {
14396
+ return false;
14397
+ }
14398
+ try {
14399
+ return typeof getNodeType(value) === "number";
14400
+ } catch (_4) {
14401
+ return false;
14402
+ }
14124
14403
  };
14125
14404
  function _executeHooks(hooks2, currentNode, data) {
14405
+ if (hooks2.length === 0) {
14406
+ return;
14407
+ }
14126
14408
  arrayForEach(hooks2, (hook) => {
14127
14409
  hook.call(DOMPurify, currentNode, data, CONFIG);
14128
14410
  });
14129
14411
  }
14130
- const _sanitizeElements = function _sanitizeElements2(currentNode) {
14131
- let content = null;
14132
- _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
14133
- if (_isClobbered(currentNode)) {
14134
- _forceRemove(currentNode);
14135
- return true;
14136
- }
14137
- const tagName = transformCaseFunc(currentNode.nodeName);
14138
- _executeHooks(hooks.uponSanitizeElement, currentNode, {
14139
- tagName,
14140
- allowedTags: ALLOWED_TAGS
14141
- });
14142
- if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
14143
- _forceRemove(currentNode);
14412
+ const _isUnsafeNode = function _isUnsafeNode2(currentNode, tagName) {
14413
+ if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
14144
14414
  return true;
14145
14415
  }
14146
14416
  if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === "style" && _isNode(currentNode.firstElementChild)) {
14147
- _forceRemove(currentNode);
14148
14417
  return true;
14149
14418
  }
14150
- if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
14151
- _forceRemove(currentNode);
14419
+ if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
14152
14420
  return true;
14153
14421
  }
14154
- if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) {
14155
- _forceRemove(currentNode);
14422
+ if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) {
14156
14423
  return true;
14157
14424
  }
14158
- if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
14159
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
14160
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
14161
- return false;
14162
- }
14163
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
14164
- return false;
14165
- }
14425
+ return false;
14426
+ };
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;
14166
14431
  }
14167
- if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
14168
- const parentNode = getParentNode2(currentNode) || currentNode.parentNode;
14169
- const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
14170
- if (childNodes && parentNode) {
14171
- const childCount = childNodes.length;
14172
- for (let i4 = childCount - 1; i4 >= 0; --i4) {
14173
- const childClone = cloneNode(childNodes[i4], true);
14174
- parentNode.insertBefore(childClone, getNextSibling(currentNode));
14175
- }
14432
+ if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
14433
+ return false;
14434
+ }
14435
+ }
14436
+ if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
14437
+ const parentNode = getParentNode2(currentNode);
14438
+ const childNodes = getChildNodes(currentNode);
14439
+ if (childNodes && parentNode) {
14440
+ const childCount = childNodes.length;
14441
+ for (let i4 = childCount - 1; i4 >= 0; --i4) {
14442
+ const hoisted = IN_PLACE ? childNodes[i4] : cloneNode(childNodes[i4], true);
14443
+ parentNode.insertBefore(hoisted, getNextSibling(currentNode));
14176
14444
  }
14177
14445
  }
14446
+ }
14447
+ _forceRemove(currentNode);
14448
+ return true;
14449
+ };
14450
+ const _sanitizeElements = function _sanitizeElements2(currentNode) {
14451
+ _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
14452
+ if (_isClobbered(currentNode)) {
14453
+ _forceRemove(currentNode);
14454
+ return true;
14455
+ }
14456
+ const tagName = transformCaseFunc(getNodeName2 ? getNodeName2(currentNode) : currentNode.nodeName);
14457
+ _executeHooks(hooks.uponSanitizeElement, currentNode, {
14458
+ tagName,
14459
+ allowedTags: ALLOWED_TAGS
14460
+ });
14461
+ if (_isUnsafeNode(currentNode, tagName)) {
14178
14462
  _forceRemove(currentNode);
14179
14463
  return true;
14180
14464
  }
14181
- if (currentNode instanceof Element2 && !_checkValidNamespace(currentNode)) {
14465
+ if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
14466
+ return _sanitizeDisallowedNode(currentNode, tagName);
14467
+ }
14468
+ const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;
14469
+ if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
14182
14470
  _forceRemove(currentNode);
14183
14471
  return true;
14184
14472
  }
14185
- if ((tagName === "noscript" || tagName === "noembed" || tagName === "noframes") && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
14473
+ if ((tagName === "noscript" || tagName === "noembed" || tagName === "noframes") && regExpTest(FALLBACK_TAG_CLOSE, currentNode.innerHTML)) {
14186
14474
  _forceRemove(currentNode);
14187
14475
  return true;
14188
14476
  }
14189
14477
  if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
14190
- content = currentNode.textContent;
14191
- arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], (expr) => {
14192
- content = stringReplace(content, expr, " ");
14193
- });
14478
+ const content = _stripTemplateExpressions(currentNode.textContent);
14194
14479
  if (currentNode.textContent !== content) {
14195
14480
  arrayPush(DOMPurify.removed, {
14196
14481
  element: currentNode.cloneNode()
@@ -14209,9 +14494,9 @@
14209
14494
  return false;
14210
14495
  }
14211
14496
  const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
14212
- if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR$1, lcName)) ;
14497
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) ;
14213
14498
  else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ;
14214
- else if (!nameIsPermitted || FORBID_ATTR[lcName]) {
14499
+ else if (!nameIsPermitted) {
14215
14500
  if (
14216
14501
  // First condition does a very basic check if a) it's basically a valid custom element tagname AND
14217
14502
  // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
@@ -14236,6 +14521,35 @@
14236
14521
  const _isBasicCustomElement = function _isBasicCustomElement2(tagName) {
14237
14522
  return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
14238
14523
  };
14524
+ const _applyTrustedTypesToAttribute = function _applyTrustedTypesToAttribute2(lcTag, lcName, namespaceURI, value) {
14525
+ if (trustedTypesPolicy && typeof trustedTypes === "object" && typeof trustedTypes.getAttributeType === "function" && !namespaceURI) {
14526
+ switch (trustedTypes.getAttributeType(lcTag, lcName)) {
14527
+ case "TrustedHTML": {
14528
+ return _createTrustedHTML(value);
14529
+ }
14530
+ case "TrustedScriptURL": {
14531
+ return _createTrustedScriptURL(value);
14532
+ }
14533
+ }
14534
+ }
14535
+ return value;
14536
+ };
14537
+ const _setAttributeValue = function _setAttributeValue2(currentNode, name, namespaceURI, value) {
14538
+ try {
14539
+ if (namespaceURI) {
14540
+ currentNode.setAttributeNS(namespaceURI, name, value);
14541
+ } else {
14542
+ currentNode.setAttribute(name, value);
14543
+ }
14544
+ if (_isClobbered(currentNode)) {
14545
+ _forceRemove(currentNode);
14546
+ } else {
14547
+ arrayPop(DOMPurify.removed);
14548
+ }
14549
+ } catch (_4) {
14550
+ _removeAttribute(name, currentNode);
14551
+ }
14552
+ };
14239
14553
  const _sanitizeAttributes = function _sanitizeAttributes2(currentNode) {
14240
14554
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
14241
14555
  const attributes = currentNode.attributes;
@@ -14250,6 +14564,7 @@
14250
14564
  forceKeepAttr: void 0
14251
14565
  };
14252
14566
  let l4 = attributes.length;
14567
+ const lcTag = transformCaseFunc(currentNode.nodeName);
14253
14568
  while (l4--) {
14254
14569
  const attr = attributes[l4];
14255
14570
  const name = attr.name, namespaceURI = attr.namespaceURI, attrValue = attr.value;
@@ -14281,50 +14596,20 @@
14281
14596
  _removeAttribute(name, currentNode);
14282
14597
  continue;
14283
14598
  }
14284
- if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
14599
+ if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
14285
14600
  _removeAttribute(name, currentNode);
14286
14601
  continue;
14287
14602
  }
14288
14603
  if (SAFE_FOR_TEMPLATES) {
14289
- arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], (expr) => {
14290
- value = stringReplace(value, expr, " ");
14291
- });
14604
+ value = _stripTemplateExpressions(value);
14292
14605
  }
14293
- const lcTag = transformCaseFunc(currentNode.nodeName);
14294
14606
  if (!_isValidAttribute(lcTag, lcName, value)) {
14295
14607
  _removeAttribute(name, currentNode);
14296
14608
  continue;
14297
14609
  }
14298
- if (trustedTypesPolicy && typeof trustedTypes === "object" && typeof trustedTypes.getAttributeType === "function") {
14299
- if (namespaceURI) ;
14300
- else {
14301
- switch (trustedTypes.getAttributeType(lcTag, lcName)) {
14302
- case "TrustedHTML": {
14303
- value = trustedTypesPolicy.createHTML(value);
14304
- break;
14305
- }
14306
- case "TrustedScriptURL": {
14307
- value = trustedTypesPolicy.createScriptURL(value);
14308
- break;
14309
- }
14310
- }
14311
- }
14312
- }
14610
+ value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
14313
14611
  if (value !== initValue) {
14314
- try {
14315
- if (namespaceURI) {
14316
- currentNode.setAttributeNS(namespaceURI, name, value);
14317
- } else {
14318
- currentNode.setAttribute(name, value);
14319
- }
14320
- if (_isClobbered(currentNode)) {
14321
- _forceRemove(currentNode);
14322
- } else {
14323
- arrayPop(DOMPurify.removed);
14324
- }
14325
- } catch (_4) {
14326
- _removeAttribute(name, currentNode);
14327
- }
14612
+ _setAttributeValue(currentNode, name, namespaceURI, value);
14328
14613
  }
14329
14614
  }
14330
14615
  _executeHooks(hooks.afterSanitizeAttributes, currentNode, null);
@@ -14337,28 +14622,67 @@
14337
14622
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
14338
14623
  _sanitizeElements(shadowNode);
14339
14624
  _sanitizeAttributes(shadowNode);
14340
- if (shadowNode.content instanceof DocumentFragment) {
14625
+ if (_isDocumentFragment(shadowNode.content)) {
14341
14626
  _sanitizeShadowDOM2(shadowNode.content);
14342
14627
  }
14628
+ const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
14629
+ if (shadowNodeType === NODE_TYPE.element) {
14630
+ const innerSr = getShadowRoot(shadowNode);
14631
+ if (_isDocumentFragment(innerSr)) {
14632
+ _sanitizeAttachedShadowRoots(innerSr);
14633
+ _sanitizeShadowDOM2(innerSr);
14634
+ }
14635
+ }
14343
14636
  }
14344
14637
  _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
14345
14638
  };
14346
- const _sanitizeAttachedShadowRoots2 = function _sanitizeAttachedShadowRoots(root) {
14347
- if (root.nodeType === NODE_TYPE.element && root.shadowRoot instanceof DocumentFragment) {
14348
- const sr = root.shadowRoot;
14349
- _sanitizeAttachedShadowRoots2(sr);
14350
- _sanitizeShadowDOM2(sr);
14351
- }
14352
- const childNodes = root.childNodes;
14353
- if (!childNodes) {
14354
- return;
14355
- }
14356
- const snapshot = [];
14357
- arrayForEach(childNodes, (child) => {
14358
- arrayPush(snapshot, child);
14359
- });
14360
- for (const child of snapshot) {
14361
- _sanitizeAttachedShadowRoots2(child);
14639
+ const _sanitizeAttachedShadowRoots = function _sanitizeAttachedShadowRoots2(root) {
14640
+ const stack = [{
14641
+ node: root,
14642
+ shadow: null
14643
+ }];
14644
+ while (stack.length > 0) {
14645
+ const item = stack.pop();
14646
+ if (item.shadow) {
14647
+ _sanitizeShadowDOM2(item.shadow);
14648
+ continue;
14649
+ }
14650
+ const node = item.node;
14651
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
14652
+ const isElement2 = nodeType === NODE_TYPE.element;
14653
+ const childNodes = getChildNodes(node);
14654
+ if (childNodes) {
14655
+ for (let i4 = childNodes.length - 1; i4 >= 0; --i4) {
14656
+ stack.push({
14657
+ node: childNodes[i4],
14658
+ shadow: null
14659
+ });
14660
+ }
14661
+ }
14662
+ if (isElement2) {
14663
+ const rootName = getNodeName2 ? getNodeName2(node) : null;
14664
+ if (typeof rootName === "string" && transformCaseFunc(rootName) === "template") {
14665
+ const content = node.content;
14666
+ if (_isDocumentFragment(content)) {
14667
+ stack.push({
14668
+ node: content,
14669
+ shadow: null
14670
+ });
14671
+ }
14672
+ }
14673
+ }
14674
+ if (isElement2) {
14675
+ const sr = getShadowRoot(node);
14676
+ if (_isDocumentFragment(sr)) {
14677
+ stack.push({
14678
+ node: null,
14679
+ shadow: sr
14680
+ }, {
14681
+ node: sr,
14682
+ shadow: null
14683
+ });
14684
+ }
14685
+ }
14362
14686
  }
14363
14687
  };
14364
14688
  DOMPurify.sanitize = function(dirty) {
@@ -14380,23 +14704,38 @@
14380
14704
  if (!DOMPurify.isSupported) {
14381
14705
  return dirty;
14382
14706
  }
14383
- if (!SET_CONFIG) {
14707
+ if (SET_CONFIG) {
14708
+ ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
14709
+ ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
14710
+ } else {
14384
14711
  _parseConfig(cfg);
14385
14712
  }
14386
- DOMPurify.removed = [];
14387
- if (typeof dirty === "string") {
14388
- IN_PLACE = false;
14713
+ if (hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) {
14714
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
14715
+ }
14716
+ if (hooks.uponSanitizeAttribute.length > 0) {
14717
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
14389
14718
  }
14390
- if (IN_PLACE) {
14391
- const nn2 = dirty.nodeName;
14719
+ DOMPurify.removed = [];
14720
+ const inPlace = IN_PLACE && typeof dirty !== "string" && _isNode(dirty);
14721
+ if (inPlace) {
14722
+ const nn2 = getNodeName2 ? getNodeName2(dirty) : dirty.nodeName;
14392
14723
  if (typeof nn2 === "string") {
14393
14724
  const tagName = transformCaseFunc(nn2);
14394
14725
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
14395
14726
  throw typeErrorCreate("root node is forbidden and cannot be sanitized in-place");
14396
14727
  }
14397
14728
  }
14398
- _sanitizeAttachedShadowRoots2(dirty);
14399
- } else if (dirty instanceof Node2) {
14729
+ if (_isClobbered(dirty)) {
14730
+ throw typeErrorCreate("root node is clobbered and cannot be sanitized in-place");
14731
+ }
14732
+ try {
14733
+ _sanitizeAttachedShadowRoots(dirty);
14734
+ } catch (error2) {
14735
+ _neutralizeRoot(dirty);
14736
+ throw error2;
14737
+ }
14738
+ } else if (_isNode(dirty)) {
14400
14739
  body = _initDocument("<!---->");
14401
14740
  importedNode = body.ownerDocument.importNode(dirty, true);
14402
14741
  if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === "BODY") {
@@ -14406,11 +14745,11 @@
14406
14745
  } else {
14407
14746
  body.appendChild(importedNode);
14408
14747
  }
14409
- _sanitizeAttachedShadowRoots2(importedNode);
14748
+ _sanitizeAttachedShadowRoots(importedNode);
14410
14749
  } else {
14411
14750
  if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && // eslint-disable-next-line unicorn/prefer-includes
14412
14751
  dirty.indexOf("<") === -1) {
14413
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
14752
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
14414
14753
  }
14415
14754
  body = _initDocument(dirty);
14416
14755
  if (!body) {
@@ -14420,25 +14759,35 @@
14420
14759
  if (body && FORCE_BODY) {
14421
14760
  _forceRemove(body.firstChild);
14422
14761
  }
14423
- const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
14424
- while (currentNode = nodeIterator.nextNode()) {
14425
- _sanitizeElements(currentNode);
14426
- _sanitizeAttributes(currentNode);
14427
- if (currentNode.content instanceof DocumentFragment) {
14428
- _sanitizeShadowDOM2(currentNode.content);
14762
+ const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
14763
+ try {
14764
+ while (currentNode = nodeIterator.nextNode()) {
14765
+ _sanitizeElements(currentNode);
14766
+ _sanitizeAttributes(currentNode);
14767
+ if (_isDocumentFragment(currentNode.content)) {
14768
+ _sanitizeShadowDOM2(currentNode.content);
14769
+ }
14429
14770
  }
14771
+ } catch (error2) {
14772
+ if (inPlace) {
14773
+ _neutralizeRoot(dirty);
14774
+ }
14775
+ throw error2;
14430
14776
  }
14431
- if (IN_PLACE) {
14777
+ if (inPlace) {
14778
+ arrayForEach(DOMPurify.removed, (entry) => {
14779
+ if (entry.element) {
14780
+ _neutralizeSubtree(entry.element);
14781
+ }
14782
+ });
14783
+ if (SAFE_FOR_TEMPLATES) {
14784
+ _scrubTemplateExpressions2(dirty);
14785
+ }
14432
14786
  return dirty;
14433
14787
  }
14434
14788
  if (RETURN_DOM) {
14435
14789
  if (SAFE_FOR_TEMPLATES) {
14436
- body.normalize();
14437
- let html3 = body.innerHTML;
14438
- arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], (expr) => {
14439
- html3 = stringReplace(html3, expr, " ");
14440
- });
14441
- body.innerHTML = html3;
14790
+ _scrubTemplateExpressions2(body);
14442
14791
  }
14443
14792
  if (RETURN_DOM_FRAGMENT) {
14444
14793
  returnNode = createDocumentFragment2.call(body.ownerDocument);
@@ -14458,20 +14807,24 @@
14458
14807
  serializedHTML = "<!DOCTYPE " + body.ownerDocument.doctype.name + ">\n" + serializedHTML;
14459
14808
  }
14460
14809
  if (SAFE_FOR_TEMPLATES) {
14461
- arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], (expr) => {
14462
- serializedHTML = stringReplace(serializedHTML, expr, " ");
14463
- });
14810
+ serializedHTML = _stripTemplateExpressions(serializedHTML);
14464
14811
  }
14465
- return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
14812
+ return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
14466
14813
  };
14467
14814
  DOMPurify.setConfig = function() {
14468
14815
  let cfg = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {};
14469
14816
  _parseConfig(cfg);
14470
14817
  SET_CONFIG = true;
14818
+ SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
14819
+ SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
14471
14820
  };
14472
14821
  DOMPurify.clearConfig = function() {
14473
14822
  CONFIG = null;
14474
14823
  SET_CONFIG = false;
14824
+ SET_CONFIG_ALLOWED_TAGS = null;
14825
+ SET_CONFIG_ALLOWED_ATTR = null;
14826
+ trustedTypesPolicy = defaultTrustedTypesPolicy;
14827
+ emptyHTML = "";
14475
14828
  };
14476
14829
  DOMPurify.isValidAttribute = function(tag2, attr, value) {
14477
14830
  if (!CONFIG) {
@@ -14485,9 +14838,15 @@
14485
14838
  if (typeof hookFunction !== "function") {
14486
14839
  return;
14487
14840
  }
14841
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
14842
+ return;
14843
+ }
14488
14844
  arrayPush(hooks[entryPoint], hookFunction);
14489
14845
  };
14490
14846
  DOMPurify.removeHook = function(entryPoint, hookFunction) {
14847
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
14848
+ return void 0;
14849
+ }
14491
14850
  if (hookFunction !== void 0) {
14492
14851
  const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
14493
14852
  return index === -1 ? void 0 : arraySplice(hooks[entryPoint], index, 1)[0];
@@ -14495,6 +14854,9 @@
14495
14854
  return arrayPop(hooks[entryPoint]);
14496
14855
  };
14497
14856
  DOMPurify.removeHooks = function(entryPoint) {
14857
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
14858
+ return;
14859
+ }
14498
14860
  hooks[entryPoint] = [];
14499
14861
  };
14500
14862
  DOMPurify.removeAllHooks = function() {
@@ -27293,6 +27655,9 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
27293
27655
  }
27294
27656
  //======= Config
27295
27657
  configureUppy() {
27658
+ const dashboardOptions = { inline: false, closeAfterFinish: true };
27659
+ const dialog2 = this.element.closest("dialog");
27660
+ if (dialog2) dashboardOptions.target = dialog2;
27296
27661
  this.uppy = new Uppy_default({
27297
27662
  restrictions: {
27298
27663
  maxFileSize: this.maxFileSizeValue,
@@ -27303,7 +27668,7 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
27303
27668
  allowedFileTypes: this.allowedFileTypesValue,
27304
27669
  requiredMetaFields: this.requiredMetaFieldsValue
27305
27670
  }
27306
- }).use(Dashboard2, { inline: false, closeAfterFinish: true }).use(ImageEditor, { target: Dashboard2 });
27671
+ }).use(Dashboard2, dashboardOptions).use(ImageEditor, { target: Dashboard2 });
27307
27672
  this.#configureUploader();
27308
27673
  this.#configureEventHandlers();
27309
27674
  }
@@ -27638,6 +28003,28 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
27638
28003
  }
27639
28004
  };
27640
28005
 
28006
+ // src/js/controllers/password_sentinel_controller.js
28007
+ var password_sentinel_controller_default = class extends Controller {
28008
+ static values = { sentinel: String };
28009
+ connect() {
28010
+ this.armed = this.element.value === this.sentinelValue;
28011
+ }
28012
+ beforeinput(event) {
28013
+ if (!this.armed) return;
28014
+ event.preventDefault();
28015
+ this.armed = false;
28016
+ let next = "";
28017
+ if (event.inputType === "insertText" && event.data != null) {
28018
+ next = event.data;
28019
+ } else if (event.inputType === "insertFromPaste" && event.dataTransfer) {
28020
+ next = event.dataTransfer.getData("text");
28021
+ }
28022
+ this.element.value = next;
28023
+ this.element.setSelectionRange(next.length, next.length);
28024
+ this.element.dispatchEvent(new Event("input", { bubbles: true }));
28025
+ }
28026
+ };
28027
+
27641
28028
  // src/js/controllers/remote_modal_controller.js
27642
28029
  var remote_modal_controller_default = class extends Controller {
27643
28030
  connect() {
@@ -27680,6 +28067,7 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
27680
28067
  async #animateClose() {
27681
28068
  if (this._closing) return;
27682
28069
  this._closing = true;
28070
+ this.element.getAnimations().forEach((animation) => animation.finish());
27683
28071
  this.element.removeAttribute("data-open");
27684
28072
  const animations = this.element.getAnimations({ subtree: true });
27685
28073
  await Promise.allSettled(animations.map((a4) => a4.finished));
@@ -28201,7 +28589,13 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
28201
28589
  if (event.target.closest("a, button, input, label, select, textarea, [data-row-click-ignore]")) {
28202
28590
  return;
28203
28591
  }
28204
- this.element.querySelector('[data-row-click-target="show"]')?.click();
28592
+ const show = this.element.querySelector('[data-row-click-target="show"]');
28593
+ if (!show) return;
28594
+ if (event.metaKey || event.ctrlKey || event.button === 1) {
28595
+ window.open(show.href, "_blank", "noopener");
28596
+ return;
28597
+ }
28598
+ show.click();
28205
28599
  }
28206
28600
  };
28207
28601
 
@@ -28256,40 +28650,52 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
28256
28650
  ]);
28257
28651
  connect() {
28258
28652
  this.dialog = this.element.closest("dialog");
28259
- if (!this.dialog) return;
28260
28653
  this.baseline = null;
28261
28654
  this.forceClose = false;
28262
28655
  this.submitting = false;
28263
28656
  this.onFirstIntent = this.#onFirstIntent.bind(this);
28264
- this.onCancel = this.#onCancel.bind(this);
28265
28657
  this.onSubmit = this.#onSubmit.bind(this);
28266
- this.onCloseButtonClick = this.#onCloseButtonClick.bind(this);
28267
- this.onConfirmCancel = this.#onConfirmCancel.bind(this);
28268
- this.onKeydown = this.#onKeydown.bind(this);
28658
+ this.onLeaveClick = this.#onLeaveClick.bind(this);
28659
+ this.onSettled = this.#onSettled.bind(this);
28269
28660
  this.element.addEventListener("pointerdown", this.onFirstIntent, true);
28270
28661
  this.element.addEventListener("keydown", this.onFirstIntent, true);
28271
- document.addEventListener("keydown", this.onKeydown, true);
28272
- this.dialog.addEventListener("cancel", this.onCancel, true);
28273
28662
  this.element.addEventListener("submit", this.onSubmit);
28274
- this.#closeButtons().forEach(
28275
- (btn) => btn.addEventListener("click", this.onCloseButtonClick, true)
28276
- );
28277
- if (this.hasConfirmDialogTarget) {
28278
- this.confirmDialogTarget.addEventListener("cancel", this.onConfirmCancel);
28663
+ this.element.addEventListener("turbo:submit-end", this.onSettled);
28664
+ if (!this.dialog) {
28665
+ document.addEventListener("click", this.onLeaveClick, true);
28666
+ }
28667
+ if (this.dialog) {
28668
+ this.onCancel = this.#onCancel.bind(this);
28669
+ this.onCloseButtonClick = this.#onCloseButtonClick.bind(this);
28670
+ this.onConfirmCancel = this.#onConfirmCancel.bind(this);
28671
+ this.onKeydown = this.#onKeydown.bind(this);
28672
+ document.addEventListener("keydown", this.onKeydown, true);
28673
+ this.dialog.addEventListener("cancel", this.onCancel, true);
28674
+ this.#closeButtons().forEach(
28675
+ (btn) => btn.addEventListener("click", this.onCloseButtonClick, true)
28676
+ );
28677
+ if (this.hasConfirmDialogTarget) {
28678
+ this.confirmDialogTarget.addEventListener("cancel", this.onConfirmCancel);
28679
+ }
28279
28680
  }
28280
28681
  }
28281
28682
  disconnect() {
28282
- if (!this.dialog) return;
28283
28683
  this.element.removeEventListener("pointerdown", this.onFirstIntent, true);
28284
28684
  this.element.removeEventListener("keydown", this.onFirstIntent, true);
28285
- document.removeEventListener("keydown", this.onKeydown, true);
28286
- this.dialog.removeEventListener("cancel", this.onCancel, true);
28287
28685
  this.element.removeEventListener("submit", this.onSubmit);
28288
- this.#closeButtons().forEach(
28289
- (btn) => btn.removeEventListener("click", this.onCloseButtonClick, true)
28290
- );
28291
- if (this.hasConfirmDialogTarget) {
28292
- this.confirmDialogTarget.removeEventListener("cancel", this.onConfirmCancel);
28686
+ this.element.removeEventListener("turbo:submit-end", this.onSettled);
28687
+ if (!this.dialog) {
28688
+ document.removeEventListener("click", this.onLeaveClick, true);
28689
+ }
28690
+ if (this.dialog) {
28691
+ document.removeEventListener("keydown", this.onKeydown, true);
28692
+ this.dialog.removeEventListener("cancel", this.onCancel, true);
28693
+ this.#closeButtons().forEach(
28694
+ (btn) => btn.removeEventListener("click", this.onCloseButtonClick, true)
28695
+ );
28696
+ if (this.hasConfirmDialogTarget) {
28697
+ this.confirmDialogTarget.removeEventListener("cancel", this.onConfirmCancel);
28698
+ }
28293
28699
  }
28294
28700
  }
28295
28701
  discard() {
@@ -28331,6 +28737,79 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
28331
28737
  #onSubmit() {
28332
28738
  this.submitting = true;
28333
28739
  }
28740
+ // A submission settled. Reset the transient guards so a same-URL Turbo-morph
28741
+ // re-render (which keeps this element and does NOT reconnect the controller)
28742
+ // doesn't leave the guard permanently disabled. Re-baseline on next interaction.
28743
+ #onSettled() {
28744
+ this.submitting = false;
28745
+ this.forceClose = false;
28746
+ this.baseline = null;
28747
+ }
28748
+ // Full-page leave guard. A control marked `data-dirty-form-guard-leave` posts
28749
+ // without this form's fields, so its unsaved edits would be lost. If the form
28750
+ // is dirty, confirm first through the app's themed dialog; the attribute's
28751
+ // value is the prompt. We always intercept the click (the themed confirm is
28752
+ // async), then re-submit the trigger's form if the user confirms.
28753
+ async #onLeaveClick(event) {
28754
+ const trigger = event.target.closest("[data-dirty-form-guard-leave]");
28755
+ if (!trigger) return;
28756
+ if (this.#guardedFormFor(trigger) !== this.element) return;
28757
+ if (this.forceClose || this.submitting) return;
28758
+ if (!this.#isDirty()) return;
28759
+ event.preventDefault();
28760
+ event.stopPropagation();
28761
+ const message = trigger.getAttribute("data-dirty-form-guard-leave") || "You have unsaved changes that will be lost. Continue?";
28762
+ const confirmed = await this.#confirm(message);
28763
+ if (!confirmed) return;
28764
+ this.forceClose = true;
28765
+ const form = trigger.closest("form");
28766
+ if (form) {
28767
+ const submitter2 = trigger.matches("button, input[type=submit], input[type=image]") ? trigger : null;
28768
+ form.requestSubmit(submitter2);
28769
+ }
28770
+ }
28771
+ // The CSS selector for a guarded form. The guard is attached as a Stimulus
28772
+ // controller (`data-controller="… dirty-form-guard"`), NOT as a CSS class — so
28773
+ // match on the controller token, not `form.dirty-form-guard` (which never
28774
+ // matches the framework's forms, leaving the leave guard silently dormant).
28775
+ static GUARDED_FORM_SELECTOR = "form[data-controller~='dirty-form-guard']";
28776
+ // The single guarded form a leave control discards: the one containing it, or —
28777
+ // for a control outside any form (a wizard's sibling nav strip) — the closest
28778
+ // guarded form, i.e. the one sharing the deepest common ancestor with the
28779
+ // trigger. Returns the only guarded form on simple pages.
28780
+ #guardedFormFor(trigger) {
28781
+ const selector = this.constructor.GUARDED_FORM_SELECTOR;
28782
+ const inside = trigger.closest(selector);
28783
+ if (inside) return inside;
28784
+ let best = null;
28785
+ let bestDepth = -1;
28786
+ document.querySelectorAll(selector).forEach((form) => {
28787
+ let ancestor = form;
28788
+ while (ancestor && !ancestor.contains(trigger)) ancestor = ancestor.parentElement;
28789
+ if (!ancestor) return;
28790
+ const depth = this.#depthOf(ancestor);
28791
+ if (depth > bestDepth) {
28792
+ bestDepth = depth;
28793
+ best = form;
28794
+ }
28795
+ });
28796
+ return best;
28797
+ }
28798
+ #depthOf(node) {
28799
+ let depth = 0;
28800
+ while (node = node.parentElement) depth++;
28801
+ return depth;
28802
+ }
28803
+ // Defer to the themed Turbo confirm dialog the app installs as the global
28804
+ // confirm method (a styled <dialog>, not the native chrome bar); fall back to
28805
+ // window.confirm only if it isn't available. Returns a Promise<boolean>.
28806
+ #confirm(message) {
28807
+ const turboConfirm = window.Turbo?.config?.forms?.confirm;
28808
+ if (typeof turboConfirm === "function") {
28809
+ return Promise.resolve(turboConfirm(message));
28810
+ }
28811
+ return Promise.resolve(window.confirm(message));
28812
+ }
28334
28813
  #confirmIsOpen() {
28335
28814
  return this.hasConfirmDialogTarget && this.confirmDialogTarget.open;
28336
28815
  }
@@ -28400,9 +28879,230 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
28400
28879
  }
28401
28880
  };
28402
28881
 
28882
+ // src/js/controllers/wizard_controller.js
28883
+ var wizard_controller_default = class extends Controller {
28884
+ static targets = ["direction"];
28885
+ connect() {
28886
+ this.submitting = false;
28887
+ this.element.addEventListener("submit", this.onSubmit);
28888
+ this.element.addEventListener("turbo:submit-end", this.onSettled);
28889
+ document.addEventListener("turbo:load", this.onSettled);
28890
+ window.addEventListener("pageshow", this.onSettled);
28891
+ }
28892
+ disconnect() {
28893
+ this.element.removeEventListener("submit", this.onSubmit);
28894
+ this.element.removeEventListener("turbo:submit-end", this.onSettled);
28895
+ document.removeEventListener("turbo:load", this.onSettled);
28896
+ window.removeEventListener("pageshow", this.onSettled);
28897
+ }
28898
+ // Set the hidden `_direction` value programmatically (optional helper).
28899
+ setDirection(value) {
28900
+ if (this.hasDirectionTarget) this.directionTarget.value = value;
28901
+ }
28902
+ onSubmit = (event) => {
28903
+ if (this.submitting) {
28904
+ event.preventDefault();
28905
+ return;
28906
+ }
28907
+ this.submitting = true;
28908
+ };
28909
+ onSettled = () => {
28910
+ this.submitting = false;
28911
+ };
28912
+ };
28913
+
28914
+ // src/js/controllers/kanban_controller.js
28915
+ var kanban_controller_default = class extends Controller {
28916
+ static values = { moveUrlTemplate: String };
28917
+ static targets = ["column"];
28918
+ connect() {
28919
+ this.draggedCard = null;
28920
+ this.onDragStart = this.#onDragStart.bind(this);
28921
+ this.onDragOver = this.#onDragOver.bind(this);
28922
+ this.onDragLeave = this.#onDragLeave.bind(this);
28923
+ this.onDrop = this.#onDrop.bind(this);
28924
+ this.onDragEnd = this.#onDragEnd.bind(this);
28925
+ this.element.addEventListener("dragstart", this.onDragStart);
28926
+ this.element.addEventListener("dragover", this.onDragOver);
28927
+ this.element.addEventListener("dragleave", this.onDragLeave);
28928
+ this.element.addEventListener("drop", this.onDrop);
28929
+ this.element.addEventListener("dragend", this.onDragEnd);
28930
+ this.#applyPersistedCollapseStates();
28931
+ }
28932
+ disconnect() {
28933
+ this.element.removeEventListener("dragstart", this.onDragStart);
28934
+ this.element.removeEventListener("dragover", this.onDragOver);
28935
+ this.element.removeEventListener("dragleave", this.onDragLeave);
28936
+ this.element.removeEventListener("drop", this.onDrop);
28937
+ this.element.removeEventListener("dragend", this.onDragEnd);
28938
+ }
28939
+ // ─── Collapse toggle ─────────────────────────────────────────────────────────
28940
+ // Stimulus action: data-action="click->kanban#toggleColumn"
28941
+ // Expected on the expand button in the collapsed strip and the collapse
28942
+ // button in the expanded header. data-kanban-column-key on the button
28943
+ // identifies which column to toggle.
28944
+ toggleColumn(event) {
28945
+ const key = event.currentTarget.dataset.kanbanColumnKey;
28946
+ if (!key) return;
28947
+ const wrapper = this.element.querySelector(`[data-kanban-col="${key}"]`);
28948
+ if (!wrapper) return;
28949
+ const strip = wrapper.querySelector("[data-kanban-role='strip']");
28950
+ const body = wrapper.querySelector("[data-kanban-role='body']");
28951
+ if (!strip || !body) return;
28952
+ const isCollapsed = wrapper.classList.contains("pu-kanban-column-collapsed");
28953
+ if (isCollapsed) {
28954
+ wrapper.classList.remove("pu-kanban-column-collapsed");
28955
+ this.#saveCollapseState(key, false);
28956
+ } else {
28957
+ wrapper.classList.add("pu-kanban-column-collapsed");
28958
+ this.#saveCollapseState(key, true);
28959
+ }
28960
+ }
28961
+ // ─── drag lifecycle ──────────────────────────────────────────────────────────
28962
+ #onDragStart(event) {
28963
+ const card = event.target.closest("[data-kanban-record-id]");
28964
+ if (!card) return;
28965
+ this.draggedCard = card;
28966
+ event.dataTransfer.effectAllowed = "move";
28967
+ event.dataTransfer.setData("text/plain", card.dataset.kanbanRecordId);
28968
+ requestAnimationFrame(() => card.classList.add("pu-kanban-dragging"));
28969
+ this.#applyDropHints(card.dataset.kanbanColumnKey);
28970
+ }
28971
+ #onDragOver(event) {
28972
+ const column = event.target.closest("[data-kanban-target='column']");
28973
+ if (!column) return;
28974
+ const wrapper = event.target.closest("[data-kanban-col]");
28975
+ if (wrapper?.classList.contains("pu-kanban-no-drop")) return;
28976
+ event.preventDefault();
28977
+ event.dataTransfer.dropEffect = "move";
28978
+ this.#highlightColumn(column);
28979
+ }
28980
+ #onDragLeave(event) {
28981
+ if (!this.element.contains(event.relatedTarget)) {
28982
+ this.#clearHighlights();
28983
+ }
28984
+ }
28985
+ async #onDrop(event) {
28986
+ event.preventDefault();
28987
+ this.#clearHighlights();
28988
+ if (!this.draggedCard) return;
28989
+ const wrapper = event.target.closest("[data-kanban-col]");
28990
+ if (wrapper?.classList.contains("pu-kanban-no-drop")) return;
28991
+ const column = event.target.closest("[data-kanban-target='column']");
28992
+ if (!column) return;
28993
+ const recordId = this.draggedCard.dataset.kanbanRecordId;
28994
+ const fromColumn = this.draggedCard.dataset.kanbanColumnKey;
28995
+ const toColumn = column.dataset.kanbanColumnKeyValue;
28996
+ const existingCards = [...column.querySelectorAll("[data-kanban-record-id]")].filter((c4) => c4 !== this.draggedCard);
28997
+ const toIndex = this.#computeDropIndex(event.clientY, existingCards);
28998
+ const url = this.moveUrlTemplateValue.replace("__ID__", recordId);
28999
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content ?? "";
29000
+ try {
29001
+ const response = await fetch(url, {
29002
+ method: "POST",
29003
+ headers: {
29004
+ "Accept": "text/vnd.turbo-stream.html",
29005
+ "Content-Type": "application/x-www-form-urlencoded",
29006
+ "X-CSRF-Token": csrfToken
29007
+ },
29008
+ body: new URLSearchParams({
29009
+ from_column: fromColumn,
29010
+ to_column: toColumn,
29011
+ to_index: toIndex
29012
+ }),
29013
+ credentials: "same-origin"
29014
+ });
29015
+ const body = await response.text();
29016
+ if (window.Turbo) {
29017
+ Turbo.renderStreamMessage(body);
29018
+ }
29019
+ } catch (error2) {
29020
+ console.error("[kanban] move request failed:", error2);
29021
+ }
29022
+ }
29023
+ #onDragEnd(_event) {
29024
+ this.#clearHighlights();
29025
+ this.#clearDropHints();
29026
+ if (this.draggedCard) {
29027
+ this.draggedCard.classList.remove("pu-kanban-dragging");
29028
+ this.draggedCard = null;
29029
+ }
29030
+ }
29031
+ // ─── drop hints ──────────────────────────────────────────────────────────────
29032
+ // Marks each column wrapper with `pu-kanban-no-drop` when it would refuse
29033
+ // a card dragged from sourceKey. The server remains the authority; this
29034
+ // is a display-only hint to give the user immediate visual feedback.
29035
+ #applyDropHints(sourceKey) {
29036
+ const sourceWrapper = this.element.querySelector(`[data-kanban-col="${sourceKey}"]`);
29037
+ const sourceLocked = sourceWrapper?.dataset.kanbanLocked === "true";
29038
+ this.element.querySelectorAll("[data-kanban-col]").forEach((wrapper) => {
29039
+ const noDrop = sourceLocked || !this.#columnAccepts(wrapper.dataset.kanbanAccepts, sourceKey);
29040
+ wrapper.classList.toggle("pu-kanban-no-drop", noDrop);
29041
+ });
29042
+ }
29043
+ #clearDropHints() {
29044
+ this.element.querySelectorAll("[data-kanban-col]").forEach((w4) => w4.classList.remove("pu-kanban-no-drop"));
29045
+ }
29046
+ // Returns true if the column described by `accepts` (the serialised form
29047
+ // from data-kanban-accepts) would accept a card from `sourceKey`.
29048
+ #columnAccepts(accepts, sourceKey) {
29049
+ if (!accepts || accepts === "all") return true;
29050
+ if (accepts === "none") return false;
29051
+ return accepts.split(",").map((k4) => k4.trim()).includes(sourceKey);
29052
+ }
29053
+ // ─── collapse persistence ─────────────────────────────────────────────────────
29054
+ // Applies localStorage collapse states to all column wrappers currently in
29055
+ // the DOM. Called on connect() and implicitly after Turbo frame swaps
29056
+ // because Stimulus re-connects the controller when the frame content changes.
29057
+ #applyPersistedCollapseStates() {
29058
+ this.element.querySelectorAll("[data-kanban-col]").forEach((wrapper) => {
29059
+ const key = wrapper.dataset.kanbanCol;
29060
+ const stored = localStorage.getItem(this.#storageKey(key));
29061
+ if (stored === null) return;
29062
+ const collapsed = stored === "1";
29063
+ wrapper.classList.toggle("pu-kanban-column-collapsed", collapsed);
29064
+ });
29065
+ }
29066
+ #saveCollapseState(key, collapsed) {
29067
+ try {
29068
+ localStorage.setItem(this.#storageKey(key), collapsed ? "1" : "0");
29069
+ } catch {
29070
+ }
29071
+ }
29072
+ // Derives a unique localStorage key from the resource collection path so
29073
+ // different boards (different resources / tenants) don't share state.
29074
+ // The move URL template is "/path/__ID__/kanban_move"; strip the suffix to
29075
+ // recover the collection path.
29076
+ #storageKey(key) {
29077
+ const path = this.moveUrlTemplateValue.replace("/__ID__/kanban_move", "");
29078
+ return `pu-kanban:${path}:${key}:collapsed`;
29079
+ }
29080
+ // ─── helpers ─────────────────────────────────────────────────────────────────
29081
+ // Returns the 0-based insertion index within the destination column by
29082
+ // comparing the cursor y-position against each card's vertical midpoint.
29083
+ // The card is inserted before the first card whose midpoint is below the
29084
+ // cursor, or appended after all cards if the cursor is below every midpoint.
29085
+ #computeDropIndex(clientY, cards) {
29086
+ for (let i4 = 0; i4 < cards.length; i4++) {
29087
+ const rect = cards[i4].getBoundingClientRect();
29088
+ if (clientY < rect.top + rect.height / 2) return i4;
29089
+ }
29090
+ return cards.length;
29091
+ }
29092
+ #highlightColumn(column) {
29093
+ this.columnTargets.forEach((c4) => {
29094
+ c4.classList.toggle("pu-kanban-drop-target", c4 === column);
29095
+ });
29096
+ }
29097
+ #clearHighlights() {
29098
+ this.columnTargets.forEach((c4) => c4.classList.remove("pu-kanban-drop-target"));
29099
+ }
29100
+ };
29101
+
28403
29102
  // src/js/controllers/register_controllers.js
28404
29103
  function register_controllers_default(application2) {
28405
29104
  application2.register("password-visibility", password_visibility_controller_default);
29105
+ application2.register("password-sentinel", password_sentinel_controller_default);
28406
29106
  application2.register("sidebar", sidebar_controller_default);
28407
29107
  application2.register("resource-header", resource_header_controller_default);
28408
29108
  application2.register("nested-resource-form-fields", nested_resource_form_fields_controller_default);
@@ -28437,6 +29137,8 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
28437
29137
  application2.register("view-switcher", view_switcher_controller_default);
28438
29138
  application2.register("autosubmit", autosubmit_controller_default);
28439
29139
  application2.register("dirty-form-guard", dirty_form_guard_controller_default);
29140
+ application2.register("wizard", wizard_controller_default);
29141
+ application2.register("kanban", kanban_controller_default);
28440
29142
  }
28441
29143
 
28442
29144
  // src/js/turbo/turbo_actions.js
@@ -28489,7 +29191,7 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
28489
29191
  "scale-95",
28490
29192
  "data-[open]:opacity-100",
28491
29193
  "data-[open]:scale-100",
28492
- "transition-[opacity,transform]",
29194
+ "transition-[opacity,scale]",
28493
29195
  "duration-200",
28494
29196
  "ease-out"
28495
29197
  ].join(" ");
@@ -28594,7 +29296,7 @@ cropperjs/dist/cropper.js:
28594
29296
  *)
28595
29297
 
28596
29298
  dompurify/dist/purify.es.mjs:
28597
- (*! @license DOMPurify 3.4.3 | (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.3/LICENSE *)
29299
+ (*! @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 *)
28598
29300
 
28599
29301
  @uppy/utils/lib/Translator.js:
28600
29302
  (**