@rulecms/source-components-angular 0.1.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 (215) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +117 -0
  3. package/THIRD_PARTY_NOTICES +50 -0
  4. package/fesm2022/rulecms-source-components-angular-icon-registry.mjs +144 -0
  5. package/fesm2022/rulecms-source-components-angular-icon-registry.mjs.map +1 -0
  6. package/fesm2022/rulecms-source-components-angular.mjs +3979 -0
  7. package/fesm2022/rulecms-source-components-angular.mjs.map +1 -0
  8. package/icon-registry/icon-aliases.d.ts +10 -0
  9. package/icon-registry/icon-aliases.d.ts.map +1 -0
  10. package/icon-registry/index.d.ts +6 -0
  11. package/icon-registry/index.d.ts.map +1 -0
  12. package/icon-registry/resolve-icon-name.d.ts +6 -0
  13. package/icon-registry/resolve-icon-name.d.ts.map +1 -0
  14. package/icon-registry/resolve-icon-node.d.ts +25 -0
  15. package/icon-registry/resolve-icon-node.d.ts.map +1 -0
  16. package/icon-registry/rulecms-source-components-angular-icon-registry.d.ts.map +1 -0
  17. package/icon-registry/search-icon-names.d.ts +11 -0
  18. package/icon-registry/search-icon-names.d.ts.map +1 -0
  19. package/index.d.ts +6 -0
  20. package/lib/base-components/display/card-entries/card-entries.d.ts +3 -0
  21. package/lib/base-components/display/card-entries/card-entries.d.ts.map +1 -0
  22. package/lib/base-components/display/card-groups.d.ts +3 -0
  23. package/lib/base-components/display/card-groups.d.ts.map +1 -0
  24. package/lib/base-components/display/component-attributes/attribute-list-for-component-types.d.ts +20 -0
  25. package/lib/base-components/display/component-attributes/attribute-list-for-component-types.d.ts.map +1 -0
  26. package/lib/base-components/display/component-attributes/component-name-to-modify-attribute.d.ts +12 -0
  27. package/lib/base-components/display/component-attributes/component-name-to-modify-attribute.d.ts.map +1 -0
  28. package/lib/base-components/display/components/cloudinary-advanced-image/attribute-edit-mode-configs.d.ts +3 -0
  29. package/lib/base-components/display/components/cloudinary-advanced-image/attribute-edit-mode-configs.d.ts.map +1 -0
  30. package/lib/base-components/display/components/cloudinary-advanced-image/build-cloudinary-image-urls.d.ts +7 -0
  31. package/lib/base-components/display/components/cloudinary-advanced-image/build-cloudinary-image-urls.d.ts.map +1 -0
  32. package/lib/base-components/display/components/cloudinary-advanced-image/cloudinary-advanced-image.component.d.ts +28 -0
  33. package/lib/base-components/display/components/cloudinary-advanced-image/cloudinary-advanced-image.component.d.ts.map +1 -0
  34. package/lib/base-components/display/components/cloudinary-advanced-image/default-attribute-values.d.ts +3 -0
  35. package/lib/base-components/display/components/cloudinary-advanced-image/default-attribute-values.d.ts.map +1 -0
  36. package/lib/base-components/display/components/cloudinary-advanced-image/display-card-entry.d.ts +3 -0
  37. package/lib/base-components/display/components/cloudinary-advanced-image/display-card-entry.d.ts.map +1 -0
  38. package/lib/base-components/display/components/cloudinary-advanced-image/resolve-no-container.d.ts +8 -0
  39. package/lib/base-components/display/components/cloudinary-advanced-image/resolve-no-container.d.ts.map +1 -0
  40. package/lib/base-components/display/components/cloudinary-advanced-image/resolve-renders-own-element.d.ts +6 -0
  41. package/lib/base-components/display/components/cloudinary-advanced-image/resolve-renders-own-element.d.ts.map +1 -0
  42. package/lib/base-components/display/components/r-accordion/accordion-attributes.d.ts +109 -0
  43. package/lib/base-components/display/components/r-accordion/accordion-attributes.d.ts.map +1 -0
  44. package/lib/base-components/display/components/r-accordion/attribute-edit-mode-configs.d.ts +16 -0
  45. package/lib/base-components/display/components/r-accordion/attribute-edit-mode-configs.d.ts.map +1 -0
  46. package/lib/base-components/display/components/r-accordion/default-attribute-values.d.ts +3 -0
  47. package/lib/base-components/display/components/r-accordion/default-attribute-values.d.ts.map +1 -0
  48. package/lib/base-components/display/components/r-accordion/display-card-entry.d.ts +3 -0
  49. package/lib/base-components/display/components/r-accordion/display-card-entry.d.ts.map +1 -0
  50. package/lib/base-components/display/components/r-accordion/r-accordion.component.d.ts +27 -0
  51. package/lib/base-components/display/components/r-accordion/r-accordion.component.d.ts.map +1 -0
  52. package/lib/base-components/display/components/r-accordion/resolve-no-container.d.ts +8 -0
  53. package/lib/base-components/display/components/r-accordion/resolve-no-container.d.ts.map +1 -0
  54. package/lib/base-components/display/components/r-accordion/resolve-renders-own-element.d.ts +6 -0
  55. package/lib/base-components/display/components/r-accordion/resolve-renders-own-element.d.ts.map +1 -0
  56. package/lib/base-components/display/components/r-button/attribute-edit-mode-configs.d.ts +12 -0
  57. package/lib/base-components/display/components/r-button/attribute-edit-mode-configs.d.ts.map +1 -0
  58. package/lib/base-components/display/components/r-button/button-attributes.d.ts +51 -0
  59. package/lib/base-components/display/components/r-button/button-attributes.d.ts.map +1 -0
  60. package/lib/base-components/display/components/r-button/button-css-variables.d.ts +28 -0
  61. package/lib/base-components/display/components/r-button/button-css-variables.d.ts.map +1 -0
  62. package/lib/base-components/display/components/r-button/button-instance-props.d.ts +23 -0
  63. package/lib/base-components/display/components/r-button/button-instance-props.d.ts.map +1 -0
  64. package/lib/base-components/display/components/r-button/button-preset-styles.d.ts +13 -0
  65. package/lib/base-components/display/components/r-button/button-preset-styles.d.ts.map +1 -0
  66. package/lib/base-components/display/components/r-button/button-presets.d.ts +76 -0
  67. package/lib/base-components/display/components/r-button/button-presets.d.ts.map +1 -0
  68. package/lib/base-components/display/components/r-button/default-attribute-values.d.ts +14 -0
  69. package/lib/base-components/display/components/r-button/default-attribute-values.d.ts.map +1 -0
  70. package/lib/base-components/display/components/r-button/display-card-entry.d.ts +3 -0
  71. package/lib/base-components/display/components/r-button/display-card-entry.d.ts.map +1 -0
  72. package/lib/base-components/display/components/r-button/r-button.component.d.ts +26 -0
  73. package/lib/base-components/display/components/r-button/r-button.component.d.ts.map +1 -0
  74. package/lib/base-components/display/components/r-button/resolve-button-preset.d.ts +11 -0
  75. package/lib/base-components/display/components/r-button/resolve-button-preset.d.ts.map +1 -0
  76. package/lib/base-components/display/components/r-button/resolve-no-container.d.ts +8 -0
  77. package/lib/base-components/display/components/r-button/resolve-no-container.d.ts.map +1 -0
  78. package/lib/base-components/display/components/r-button/resolve-renders-own-element.d.ts +6 -0
  79. package/lib/base-components/display/components/r-button/resolve-renders-own-element.d.ts.map +1 -0
  80. package/lib/base-components/display/components/r-divider/attribute-edit-mode-configs.d.ts +10 -0
  81. package/lib/base-components/display/components/r-divider/attribute-edit-mode-configs.d.ts.map +1 -0
  82. package/lib/base-components/display/components/r-divider/default-attribute-values.d.ts +11 -0
  83. package/lib/base-components/display/components/r-divider/default-attribute-values.d.ts.map +1 -0
  84. package/lib/base-components/display/components/r-divider/display-card-entry.d.ts +3 -0
  85. package/lib/base-components/display/components/r-divider/display-card-entry.d.ts.map +1 -0
  86. package/lib/base-components/display/components/r-divider/divider-attributes.d.ts +41 -0
  87. package/lib/base-components/display/components/r-divider/divider-attributes.d.ts.map +1 -0
  88. package/lib/base-components/display/components/r-divider/divider-styles.d.ts +14 -0
  89. package/lib/base-components/display/components/r-divider/divider-styles.d.ts.map +1 -0
  90. package/lib/base-components/display/components/r-divider/r-divider.component.d.ts +16 -0
  91. package/lib/base-components/display/components/r-divider/r-divider.component.d.ts.map +1 -0
  92. package/lib/base-components/display/components/r-divider/resolve-no-container.d.ts +7 -0
  93. package/lib/base-components/display/components/r-divider/resolve-no-container.d.ts.map +1 -0
  94. package/lib/base-components/display/components/r-divider/resolve-renders-own-element.d.ts +6 -0
  95. package/lib/base-components/display/components/r-divider/resolve-renders-own-element.d.ts.map +1 -0
  96. package/lib/base-components/display/components/r-embed/attribute-edit-mode-configs.d.ts +8 -0
  97. package/lib/base-components/display/components/r-embed/attribute-edit-mode-configs.d.ts.map +1 -0
  98. package/lib/base-components/display/components/r-embed/default-attribute-values.d.ts +14 -0
  99. package/lib/base-components/display/components/r-embed/default-attribute-values.d.ts.map +1 -0
  100. package/lib/base-components/display/components/r-embed/display-card-entry.d.ts +3 -0
  101. package/lib/base-components/display/components/r-embed/display-card-entry.d.ts.map +1 -0
  102. package/lib/base-components/display/components/r-embed/embed-attributes.d.ts +71 -0
  103. package/lib/base-components/display/components/r-embed/embed-attributes.d.ts.map +1 -0
  104. package/lib/base-components/display/components/r-embed/embed-providers.d.ts +78 -0
  105. package/lib/base-components/display/components/r-embed/embed-providers.d.ts.map +1 -0
  106. package/lib/base-components/display/components/r-embed/r-embed.component.d.ts +21 -0
  107. package/lib/base-components/display/components/r-embed/r-embed.component.d.ts.map +1 -0
  108. package/lib/base-components/display/components/r-embed/resolve-embed.d.ts +41 -0
  109. package/lib/base-components/display/components/r-embed/resolve-embed.d.ts.map +1 -0
  110. package/lib/base-components/display/components/r-embed/resolve-no-container.d.ts +13 -0
  111. package/lib/base-components/display/components/r-embed/resolve-no-container.d.ts.map +1 -0
  112. package/lib/base-components/display/components/r-embed/resolve-renders-own-element.d.ts +9 -0
  113. package/lib/base-components/display/components/r-embed/resolve-renders-own-element.d.ts.map +1 -0
  114. package/lib/base-components/display/components/r-icon/attribute-edit-mode-configs.d.ts +3 -0
  115. package/lib/base-components/display/components/r-icon/attribute-edit-mode-configs.d.ts.map +1 -0
  116. package/lib/base-components/display/components/r-icon/default-attribute-values.d.ts +3 -0
  117. package/lib/base-components/display/components/r-icon/default-attribute-values.d.ts.map +1 -0
  118. package/lib/base-components/display/components/r-icon/display-card-entry.d.ts +3 -0
  119. package/lib/base-components/display/components/r-icon/display-card-entry.d.ts.map +1 -0
  120. package/lib/base-components/display/components/r-icon/icon-attribute-value.d.ts +26 -0
  121. package/lib/base-components/display/components/r-icon/icon-attribute-value.d.ts.map +1 -0
  122. package/lib/base-components/display/components/r-icon/r-icon.component.d.ts +16 -0
  123. package/lib/base-components/display/components/r-icon/r-icon.component.d.ts.map +1 -0
  124. package/lib/base-components/display/components/r-icon/resolve-no-container.d.ts +7 -0
  125. package/lib/base-components/display/components/r-icon/resolve-no-container.d.ts.map +1 -0
  126. package/lib/base-components/display/components/r-icon/resolve-renders-own-element.d.ts +7 -0
  127. package/lib/base-components/display/components/r-icon/resolve-renders-own-element.d.ts.map +1 -0
  128. package/lib/base-components/display/components/r-list/attribute-edit-mode-configs.d.ts +10 -0
  129. package/lib/base-components/display/components/r-list/attribute-edit-mode-configs.d.ts.map +1 -0
  130. package/lib/base-components/display/components/r-list/default-attribute-values.d.ts +3 -0
  131. package/lib/base-components/display/components/r-list/default-attribute-values.d.ts.map +1 -0
  132. package/lib/base-components/display/components/r-list/display-card-entry.d.ts +3 -0
  133. package/lib/base-components/display/components/r-list/display-card-entry.d.ts.map +1 -0
  134. package/lib/base-components/display/components/r-list/list-attributes.d.ts +72 -0
  135. package/lib/base-components/display/components/r-list/list-attributes.d.ts.map +1 -0
  136. package/lib/base-components/display/components/r-list/r-list.component.d.ts +22 -0
  137. package/lib/base-components/display/components/r-list/r-list.component.d.ts.map +1 -0
  138. package/lib/base-components/display/components/r-list/resolve-no-container.d.ts +8 -0
  139. package/lib/base-components/display/components/r-list/resolve-no-container.d.ts.map +1 -0
  140. package/lib/base-components/display/components/r-list/resolve-renders-own-element.d.ts +7 -0
  141. package/lib/base-components/display/components/r-list/resolve-renders-own-element.d.ts.map +1 -0
  142. package/lib/base-components/display/components/r-text/attribute-edit-mode-configs.d.ts +3 -0
  143. package/lib/base-components/display/components/r-text/attribute-edit-mode-configs.d.ts.map +1 -0
  144. package/lib/base-components/display/components/r-text/default-attribute-values.d.ts +4 -0
  145. package/lib/base-components/display/components/r-text/default-attribute-values.d.ts.map +1 -0
  146. package/lib/base-components/display/components/r-text/display-card-entry.d.ts +3 -0
  147. package/lib/base-components/display/components/r-text/display-card-entry.d.ts.map +1 -0
  148. package/lib/base-components/display/components/r-text/enclosing-tags.d.ts +48 -0
  149. package/lib/base-components/display/components/r-text/enclosing-tags.d.ts.map +1 -0
  150. package/lib/base-components/display/components/r-text/r-text.component.d.ts +16 -0
  151. package/lib/base-components/display/components/r-text/r-text.component.d.ts.map +1 -0
  152. package/lib/base-components/display/components/r-text/resolve-enclosing-tag.d.ts +18 -0
  153. package/lib/base-components/display/components/r-text/resolve-enclosing-tag.d.ts.map +1 -0
  154. package/lib/base-components/display/components/r-text/resolve-no-container.d.ts +13 -0
  155. package/lib/base-components/display/components/r-text/resolve-no-container.d.ts.map +1 -0
  156. package/lib/base-components/display/components/r-text/resolve-renders-own-element.d.ts +11 -0
  157. package/lib/base-components/display/components/r-text/resolve-renders-own-element.d.ts.map +1 -0
  158. package/lib/base-components/display/components/r-video/attribute-edit-mode-configs.d.ts +3 -0
  159. package/lib/base-components/display/components/r-video/attribute-edit-mode-configs.d.ts.map +1 -0
  160. package/lib/base-components/display/components/r-video/build-video-delivery-urls.d.ts +7 -0
  161. package/lib/base-components/display/components/r-video/build-video-delivery-urls.d.ts.map +1 -0
  162. package/lib/base-components/display/components/r-video/default-attribute-values.d.ts +3 -0
  163. package/lib/base-components/display/components/r-video/default-attribute-values.d.ts.map +1 -0
  164. package/lib/base-components/display/components/r-video/display-card-entry.d.ts +3 -0
  165. package/lib/base-components/display/components/r-video/display-card-entry.d.ts.map +1 -0
  166. package/lib/base-components/display/components/r-video/r-video.component.d.ts +26 -0
  167. package/lib/base-components/display/components/r-video/r-video.component.d.ts.map +1 -0
  168. package/lib/base-components/display/components/r-video/resolve-no-container.d.ts +8 -0
  169. package/lib/base-components/display/components/r-video/resolve-no-container.d.ts.map +1 -0
  170. package/lib/base-components/display/components/r-video/resolve-renders-own-element.d.ts +6 -0
  171. package/lib/base-components/display/components/r-video/resolve-renders-own-element.d.ts.map +1 -0
  172. package/lib/base-components/editor-preview/card-entries/card-entries.d.ts +3 -0
  173. package/lib/base-components/editor-preview/card-entries/card-entries.d.ts.map +1 -0
  174. package/lib/base-components/editor-preview/card-groups.d.ts +3 -0
  175. package/lib/base-components/editor-preview/card-groups.d.ts.map +1 -0
  176. package/lib/component-identifiers.d.ts +4 -0
  177. package/lib/component-identifiers.d.ts.map +1 -0
  178. package/lib/component-library-module.d.ts +25 -0
  179. package/lib/component-library-module.d.ts.map +1 -0
  180. package/lib/get-component-metadata-for-editing.d.ts +3 -0
  181. package/lib/get-component-metadata-for-editing.d.ts.map +1 -0
  182. package/lib/get-preview-components.d.ts +12 -0
  183. package/lib/get-preview-components.d.ts.map +1 -0
  184. package/lib/get-source-components-plugin.d.ts +9 -0
  185. package/lib/get-source-components-plugin.d.ts.map +1 -0
  186. package/lib/shared/html-tag.component.d.ts +22 -0
  187. package/lib/shared/html-tag.component.d.ts.map +1 -0
  188. package/lib/shared/icons/append-icon-children.d.ts +5 -0
  189. package/lib/shared/icons/append-icon-children.d.ts.map +1 -0
  190. package/lib/shared/icons/svg-icon.component.d.ts +21 -0
  191. package/lib/shared/icons/svg-icon.component.d.ts.map +1 -0
  192. package/lib/shared/inline-markup/index.d.ts +5 -0
  193. package/lib/shared/inline-markup/index.d.ts.map +1 -0
  194. package/lib/shared/inline-markup/inline-markup.component.d.ts +16 -0
  195. package/lib/shared/inline-markup/inline-markup.component.d.ts.map +1 -0
  196. package/lib/shared/inline-markup/parse-inline-markers.d.ts +45 -0
  197. package/lib/shared/inline-markup/parse-inline-markers.d.ts.map +1 -0
  198. package/lib/shared/inline-markup/serialize-inline-markers.d.ts +14 -0
  199. package/lib/shared/inline-markup/serialize-inline-markers.d.ts.map +1 -0
  200. package/lib/shared/links/index.d.ts +8 -0
  201. package/lib/shared/links/index.d.ts.map +1 -0
  202. package/lib/shared/links/is-safe-href.d.ts +20 -0
  203. package/lib/shared/links/is-safe-href.d.ts.map +1 -0
  204. package/lib/shared/links/link-item.d.ts +39 -0
  205. package/lib/shared/links/link-item.d.ts.map +1 -0
  206. package/lib/shared/links/parse-inline-link-markers.d.ts +25 -0
  207. package/lib/shared/links/parse-inline-link-markers.d.ts.map +1 -0
  208. package/lib/shared/links/resolve-link-attributes.d.ts +13 -0
  209. package/lib/shared/links/resolve-link-attributes.d.ts.map +1 -0
  210. package/lib/types.d.ts +113 -0
  211. package/lib/types.d.ts.map +1 -0
  212. package/package.json +56 -0
  213. package/public-api.d.ts +30 -0
  214. package/public-api.d.ts.map +1 -0
  215. package/rulecms-source-components-angular.d.ts.map +1 -0
@@ -0,0 +1,3979 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Input, Component, ViewChild } from '@angular/core';
3
+ import { NgStyle } from '@angular/common';
4
+ import * as i1 from '@angular/platform-browser';
5
+
6
+ const DEFAULT_CONTENT_TEXT = 'This is a text block. Click to edit and add your own content.';
7
+ const defaultAttributeValues$8 = [
8
+ {
9
+ name: 'content-text-3-resolutions',
10
+ defaultValues: {
11
+ all: DEFAULT_CONTENT_TEXT,
12
+ },
13
+ },
14
+ {
15
+ name: 'enclosing-tag-3-resolutions',
16
+ defaultValues: {},
17
+ },
18
+ {
19
+ name: 'links-3-resolutions',
20
+ defaultValues: {},
21
+ },
22
+ ];
23
+
24
+ /**
25
+ * HTML tags the RText component is allowed to enclose its text content in,
26
+ * ordered from most common to least common.
27
+ *
28
+ * Every entry is a standard HTML element that can directly contain text
29
+ * without changing how that text behaves. Deliberately excluded:
30
+ * - Interactive / behavior-changing tags: a, button, label, summary, option,
31
+ * textarea, select, details, dialog
32
+ * - Void elements that cannot enclose text: br, wbr, hr, img, input
33
+ * - Tags only valid inside a specific parent: li, dt, dd, td, th, caption,
34
+ * legend, figcaption, rt, rp, optgroup
35
+ * - Raw-text / metadata tags: script, style, title, template
36
+ * - Live-region / form-output tags that alter assistive-technology behavior:
37
+ * output
38
+ */
39
+ const ENCLOSING_TAGS = [
40
+ // Common block-level text containers
41
+ 'p',
42
+ 'div',
43
+ // Generic inline container
44
+ 'span',
45
+ // Headings
46
+ 'h1',
47
+ 'h2',
48
+ 'h3',
49
+ 'h4',
50
+ 'h5',
51
+ 'h6',
52
+ // Common inline emphasis and importance
53
+ 'strong',
54
+ 'em',
55
+ 'b',
56
+ 'i',
57
+ 'u',
58
+ // Common text presentation
59
+ 'small',
60
+ 'mark',
61
+ // Preformatted and code text
62
+ 'pre',
63
+ 'code',
64
+ // Quotations
65
+ 'blockquote',
66
+ 'q',
67
+ // Subscript and superscript
68
+ 'sub',
69
+ 'sup',
70
+ // Edits and strikethrough
71
+ 's',
72
+ 'del',
73
+ 'ins',
74
+ // Citations, definitions and abbreviations
75
+ 'cite',
76
+ 'abbr',
77
+ 'dfn',
78
+ // Computer I/O text
79
+ 'kbd',
80
+ 'samp',
81
+ 'var',
82
+ // Machine-readable text
83
+ 'time',
84
+ 'data',
85
+ // Contact information
86
+ 'address',
87
+ // Bidirectional text
88
+ 'bdi',
89
+ 'bdo',
90
+ // Sectioning and grouping containers
91
+ 'figure',
92
+ 'article',
93
+ 'section',
94
+ 'aside',
95
+ 'header',
96
+ 'footer',
97
+ 'main',
98
+ 'nav',
99
+ ];
100
+ /**
101
+ * The tags above whose default CSS display is block-level.
102
+ *
103
+ * Box styles — margin, padding, width, border — behave as authored on these
104
+ * elements, so RText can carry the column's styles on the tag itself and
105
+ * needs no wrapper div (see `resolve-no-container.ts`).
106
+ */
107
+ const BLOCK_ENCLOSING_TAGS = [
108
+ 'p',
109
+ 'div',
110
+ 'h1',
111
+ 'h2',
112
+ 'h3',
113
+ 'h4',
114
+ 'h5',
115
+ 'h6',
116
+ 'pre',
117
+ 'blockquote',
118
+ 'address',
119
+ 'figure',
120
+ 'article',
121
+ 'section',
122
+ 'aside',
123
+ 'header',
124
+ 'footer',
125
+ 'main',
126
+ 'nav',
127
+ ];
128
+ /**
129
+ * The tags above that default to `display: inline`, where vertical
130
+ * margin/padding and width/height are ignored by the layout engine. Column
131
+ * box styles must go on a wrapper element instead of on the tag.
132
+ *
133
+ * Kept as an explicit list rather than derived from BLOCK_ENCLOSING_TAGS so
134
+ * adding a tag to ENCLOSING_TAGS forces a deliberate block-or-inline call;
135
+ * the coverage test in `__tests__/enclosing-tags.test.ts` fails otherwise.
136
+ */
137
+ const INLINE_ENCLOSING_TAGS = [
138
+ 'span',
139
+ 'strong',
140
+ 'em',
141
+ 'b',
142
+ 'i',
143
+ 'u',
144
+ 'small',
145
+ 'mark',
146
+ 'code',
147
+ 'q',
148
+ 'sub',
149
+ 'sup',
150
+ 's',
151
+ 'del',
152
+ 'ins',
153
+ 'cite',
154
+ 'abbr',
155
+ 'dfn',
156
+ 'kbd',
157
+ 'samp',
158
+ 'var',
159
+ 'time',
160
+ 'data',
161
+ 'bdi',
162
+ 'bdo',
163
+ ];
164
+ /**
165
+ * Runtime guard for values arriving through componentAttributes (untyped
166
+ * editor-authored data). Unknown values are rejected so a bad persisted value
167
+ * can never render an unexpected element.
168
+ */
169
+ function isEnclosingTag(value) {
170
+ return (typeof value === 'string' &&
171
+ ENCLOSING_TAGS.includes(value));
172
+ }
173
+ /**
174
+ * True when the tag renders as a block-level box. Unknown values count as
175
+ * non-block: the safe answer, because it routes box styles to a wrapper
176
+ * element where they are guaranteed to apply.
177
+ */
178
+ function isBlockEnclosingTag(value) {
179
+ return (isEnclosingTag(value) &&
180
+ BLOCK_ENCLOSING_TAGS.includes(value));
181
+ }
182
+
183
+ /** Editor-authored attribute holding the tag, flattened per device resolution. */
184
+ const ENCLOSING_TAG_ATTRIBUTE = 'enclosing-tag-3-resolutions';
185
+ /**
186
+ * The single source of truth for which tag RText will render.
187
+ *
188
+ * Both the component and `resolveNoContainer` go through here: if they
189
+ * disagreed, widget-react would wrap a block element in a redundant div or
190
+ * hand box styles to an inline element that ignores them.
191
+ *
192
+ * The editor-authored attribute wins over the caller's prop. Attribute values
193
+ * are untyped, so an unrecognised value falls back to the prop, then to no
194
+ * enclosing tag at all.
195
+ */
196
+ function resolveEnclosingTag(componentAttributes, fallback) {
197
+ const userAssignedTag = componentAttributes?.[ENCLOSING_TAG_ATTRIBUTE];
198
+ return isEnclosingTag(userAssignedTag) ? userAssignedTag : fallback;
199
+ }
200
+
201
+ /**
202
+ * Runtime guard for entries arriving through componentAttributes (untyped,
203
+ * editor-authored data).
204
+ *
205
+ * Only object shape is checked. Field types are not: a single bad field must
206
+ * cost that field, not the whole entry, so per-field validation belongs to
207
+ * `resolveLinkAttributes` where each value is turned into an attribute.
208
+ */
209
+ function isLinkItem(value) {
210
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
211
+ }
212
+ /**
213
+ * True for a cleared slot. Clearing a link stores `{}` rather than removing
214
+ * the array element, because marker indexes are 1-based positions: dropping an
215
+ * element would silently repoint every later marker.
216
+ */
217
+ function isEmptyLinkItem(item) {
218
+ if (!isLinkItem(item)) {
219
+ return true;
220
+ }
221
+ return Object.values(item).every((value) => value === undefined ||
222
+ value === null ||
223
+ (typeof value === 'string' && value.trim() === ''));
224
+ }
225
+ /**
226
+ * The entry a `{n|…}` marker points at, by 1-based position. Tolerant of bad
227
+ * data on every axis — a non-array list, a fractional or out-of-range index,
228
+ * a hole in the array — because none of it may throw during a render.
229
+ */
230
+ function getLinkItemAt(links, oneBasedIndex) {
231
+ if (!Array.isArray(links)) {
232
+ return undefined;
233
+ }
234
+ if (!Number.isInteger(oneBasedIndex) || oneBasedIndex < 1) {
235
+ return undefined;
236
+ }
237
+ const entry = links[oneBasedIndex - 1];
238
+ return isLinkItem(entry) ? entry : undefined;
239
+ }
240
+
241
+ /**
242
+ * Schemes that can execute or smuggle a payload. Mirrors the denylist in
243
+ * RuleCMS `text-safety/validate-text-content.ts`; the two must stay in step,
244
+ * because that one warns at save and this one is the guarantee at render.
245
+ */
246
+ const REJECTED_HREF_SCHEMES = [
247
+ 'javascript',
248
+ // Netscape-era aliases for javascript:. No browser in use today runs them,
249
+ // but RuleCMS refuses them in prose, and an href accepting what the text
250
+ // field rejects is the kind of gap that outlives the reason for it.
251
+ 'livescript',
252
+ 'mocha',
253
+ 'vbscript',
254
+ 'data',
255
+ 'file',
256
+ 'blob',
257
+ ];
258
+ const SCHEME = /^([a-z][a-z0-9+.-]*):/;
259
+ /**
260
+ * Characters a browser ignores while reading a scheme. Stripped before the
261
+ * scheme is matched so `java\tscript:` and a leading NUL cannot hide one.
262
+ */
263
+ const isIgnoredWhenReadingAScheme = (charCode) => charCode <= 0x20 || charCode === 0x7f;
264
+ const schemeProbe = (value) => {
265
+ let probe = '';
266
+ for (let index = 0; index < value.length; index += 1) {
267
+ if (!isIgnoredWhenReadingAScheme(value.charCodeAt(index))) {
268
+ probe += value[index];
269
+ }
270
+ }
271
+ return probe.toLowerCase();
272
+ };
273
+ /**
274
+ * Whether a value is safe to put in an `href`.
275
+ *
276
+ * A denylist, not an allowlist. Customers link with git and ssh URLs, mobile
277
+ * deep links, `mailto:`, `tel:`, `sms:`, custom app schemes, protocol-relative
278
+ * `//host/path`, and bare `/path`, `#anchor`, `?q=1`, `./rel` — all of which
279
+ * are accepted. A host is not required and neither is http/https. Only the
280
+ * schemes that turn a link into code or an opaque payload are turned away.
281
+ *
282
+ * Rejection happens at render, never at save: the composer may use this for a
283
+ * non-blocking warning, but stored content is not trusted content.
284
+ */
285
+ function isSafeHref(value) {
286
+ if (typeof value !== 'string' || value.trim() === '') {
287
+ return false;
288
+ }
289
+ const match = SCHEME.exec(schemeProbe(value));
290
+ // No scheme at all: relative, fragment, query, or protocol-relative.
291
+ if (!match) {
292
+ return true;
293
+ }
294
+ return !REJECTED_HREF_SCHEMES.includes(match[1]);
295
+ }
296
+
297
+ const BLANK_TARGET = '_blank';
298
+ const IMPLIED_REL_FOR_BLANK_TARGET = ['noopener', 'noreferrer'];
299
+ const cleanString = (value) => {
300
+ if (typeof value !== 'string') {
301
+ return undefined;
302
+ }
303
+ const trimmed = value.trim();
304
+ return trimmed === '' ? undefined : trimmed;
305
+ };
306
+ const resolveRel = (authorRel, target) => {
307
+ const tokens = authorRel ? authorRel.split(/\s+/) : [];
308
+ if (target?.toLowerCase() !== BLANK_TARGET) {
309
+ return tokens.length > 0 ? tokens.join(' ') : undefined;
310
+ }
311
+ const present = new Set(tokens.map((token) => token.toLowerCase()));
312
+ const merged = [...tokens];
313
+ IMPLIED_REL_FOR_BLANK_TARGET.forEach((token) => {
314
+ if (!present.has(token)) {
315
+ merged.push(token);
316
+ }
317
+ });
318
+ return merged.join(' ');
319
+ };
320
+ const resolveDownload = (download) => {
321
+ if (download === true) {
322
+ return true;
323
+ }
324
+ return cleanString(download);
325
+ };
326
+ function resolveLinkAttributes(item) {
327
+ if (!isLinkItem(item)) {
328
+ return undefined;
329
+ }
330
+ const href = cleanString(item.href);
331
+ if (!isSafeHref(href)) {
332
+ return undefined;
333
+ }
334
+ const target = cleanString(item.target);
335
+ const rel = resolveRel(cleanString(item.rel), target);
336
+ const title = cleanString(item.title);
337
+ const ariaLabel = cleanString(item.ariaLabel);
338
+ const download = resolveDownload(item.download);
339
+ const hrefLang = cleanString(item.hreflang);
340
+ const referrerPolicy = cleanString(item.referrerPolicy);
341
+ return {
342
+ href: href,
343
+ ...(target !== undefined ? { target } : {}),
344
+ ...(rel !== undefined ? { rel } : {}),
345
+ ...(title !== undefined ? { title } : {}),
346
+ ...(ariaLabel !== undefined ? { 'aria-label': ariaLabel } : {}),
347
+ ...(download !== undefined ? { download } : {}),
348
+ ...(hrefLang !== undefined ? { hrefLang } : {}),
349
+ ...(referrerPolicy !== undefined ? { referrerPolicy } : {}),
350
+ };
351
+ }
352
+
353
+ /**
354
+ * `{` + optional mark letters + optional index + `|` + text + `}`.
355
+ *
356
+ * Sticky, so it can be tried at one position without slicing the string. The
357
+ * text is everything up to the first `}`, which is what makes markers
358
+ * non-nesting.
359
+ */
360
+ const MARKER = /\{([a-z]*)(\d*)\|([^}]*)\}/y;
361
+ /**
362
+ * One letter per mark. Order is not significant (`{bi1|x}` and `{ib1|x}` are
363
+ * the same span) and repeats are harmless, because the letters resolve to a
364
+ * set.
365
+ *
366
+ * **`r` must never become a mark letter.** `br` is the line-break token below,
367
+ * and it is only unambiguous while `r` cannot appear in a mark sequence.
368
+ */
369
+ const MARK_LETTERS$1 = {
370
+ b: 'bold',
371
+ i: 'italic',
372
+ s: 'strike',
373
+ };
374
+ /** The one marker that carries no text: `{br|}`. */
375
+ const BREAK_TOKEN = 'br';
376
+ const NO_MARKS = { bold: false, italic: false, strike: false };
377
+ /**
378
+ * Letters to a mark set, or `undefined` when any letter is not a mark. An
379
+ * unknown letter makes the whole marker literal text rather than a span with
380
+ * the bad letter ignored: an author who mistypes should see it.
381
+ */
382
+ const resolveMarks = (letters) => {
383
+ if (letters === '') {
384
+ return { ...NO_MARKS };
385
+ }
386
+ const marks = { ...NO_MARKS };
387
+ for (const letter of letters) {
388
+ const mark = MARK_LETTERS$1[letter];
389
+ if (mark === undefined) {
390
+ return undefined;
391
+ }
392
+ marks[mark] = true;
393
+ }
394
+ return marks;
395
+ };
396
+ /**
397
+ * Splits authored text into plain runs, marked/linked spans, and line breaks.
398
+ *
399
+ * The grammar, and everything it deliberately leaves as literal text, is
400
+ * specified in `rulecms/__docs__/__ARCHITECTURE__/INLINE_MARKUP_ARCHITECTURE.md`.
401
+ * In short: a marker is `{`, then lowercase mark letters, then a link index,
402
+ * then `|`, then its text, then `}`. At least one of the letters and the index
403
+ * must be present, and everything that does not match that shape stays
404
+ * verbatim — `{name}`, `{99}`, a lone `{`, JSON fragments, an unterminated
405
+ * marker, an unknown mark letter, and empty span text.
406
+ *
407
+ * Indexes are 1-based and normalize through a base-10 parse, so `{01|x}` and
408
+ * `{1|x}` point at the same entry. Resolving an index to an entry is the
409
+ * caller's job; this stays pure and free of React.
410
+ */
411
+ function parseInlineMarkers(value) {
412
+ if (typeof value !== 'string' || value === '') {
413
+ return [];
414
+ }
415
+ const runs = [];
416
+ let pendingText = '';
417
+ let cursor = 0;
418
+ const flushPendingText = () => {
419
+ if (pendingText !== '') {
420
+ runs.push({ kind: 'text', text: pendingText });
421
+ pendingText = '';
422
+ }
423
+ };
424
+ while (cursor < value.length) {
425
+ const open = value.indexOf('{', cursor);
426
+ if (open === -1) {
427
+ pendingText += value.slice(cursor);
428
+ break;
429
+ }
430
+ MARKER.lastIndex = open;
431
+ const match = MARKER.exec(value);
432
+ const run = match ? resolveRun(match[1], match[2], match[3]) : undefined;
433
+ if (run === undefined) {
434
+ // Not a marker: keep the brace as text and look for the next one.
435
+ pendingText += value.slice(cursor, open + 1);
436
+ cursor = open + 1;
437
+ continue;
438
+ }
439
+ pendingText += value.slice(cursor, open);
440
+ flushPendingText();
441
+ runs.push(run);
442
+ cursor = open + match[0].length;
443
+ }
444
+ flushPendingText();
445
+ return runs;
446
+ }
447
+ /**
448
+ * One matched `{…|…}` to the run it means, or `undefined` when the match is
449
+ * not a marker after all and its opening brace is literal text.
450
+ */
451
+ const resolveRun = (letters, digits, text) => {
452
+ if (letters === BREAK_TOKEN) {
453
+ // A break carries nothing. `{br|x}` is a mistake, not a break with text.
454
+ return digits === '' && text === '' ? { kind: 'break' } : undefined;
455
+ }
456
+ if (letters === '' && digits === '') {
457
+ return undefined;
458
+ }
459
+ // Empty text has no words to mark or link, and staying literal is what shows
460
+ // the author the mistake instead of swallowing the marker.
461
+ if (text === '') {
462
+ return undefined;
463
+ }
464
+ const marks = resolveMarks(letters);
465
+ if (marks === undefined) {
466
+ return undefined;
467
+ }
468
+ return {
469
+ kind: 'span',
470
+ text,
471
+ ...(digits === '' ? {} : { index: Number.parseInt(digits, 10) }),
472
+ marks,
473
+ };
474
+ };
475
+
476
+ const MARK_TAGS = [
477
+ ['bold', 'strong'],
478
+ ['italic', 'em'],
479
+ ['strike', 's'],
480
+ ];
481
+ const hasMark = (marks) => marks.bold || marks.italic || marks.strike;
482
+ const plainTextOf = (run) => (run.kind === 'break' ? '' : run.text);
483
+ class InlineMarkupComponent {
484
+ constructor(host, renderer) {
485
+ this.host = host;
486
+ this.renderer = renderer;
487
+ this.text = '';
488
+ }
489
+ ngOnChanges() {
490
+ this.clear();
491
+ const runs = parseInlineMarkers(this.text);
492
+ const anchors = runs.map((run) => run.kind === 'span' && run.index !== undefined
493
+ ? resolveLinkAttributes(getLinkItemAt(this.links, run.index))
494
+ : undefined);
495
+ const needsElement = runs.some((run, position) => run.kind === 'break' ||
496
+ (run.kind === 'span' && (anchors[position] !== undefined || hasMark(run.marks))));
497
+ if (!needsElement) {
498
+ this.renderer.appendChild(this.host.nativeElement, this.renderer.createText(runs.map(plainTextOf).join('')));
499
+ return;
500
+ }
501
+ runs.forEach((run, position) => {
502
+ if (run.kind === 'text') {
503
+ this.renderer.appendChild(this.host.nativeElement, this.renderer.createText(run.text));
504
+ return;
505
+ }
506
+ if (run.kind === 'break') {
507
+ this.renderer.appendChild(this.host.nativeElement, this.renderer.createElement('br'));
508
+ return;
509
+ }
510
+ this.renderer.appendChild(this.host.nativeElement, this.renderSpan(run, anchors[position]));
511
+ });
512
+ }
513
+ renderSpan(run, anchor) {
514
+ let node = anchor === undefined
515
+ ? this.renderer.createText(run.text)
516
+ : this.createAnchor(anchor, run.text);
517
+ for (let position = MARK_TAGS.length - 1; position >= 0; position -= 1) {
518
+ const [mark, tag] = MARK_TAGS[position];
519
+ if (run.marks[mark]) {
520
+ const wrapper = this.renderer.createElement(tag);
521
+ this.renderer.appendChild(wrapper, node);
522
+ node = wrapper;
523
+ }
524
+ }
525
+ return node;
526
+ }
527
+ createAnchor(anchor, text) {
528
+ const el = this.renderer.createElement('a');
529
+ this.renderer.setAttribute(el, 'href', anchor.href);
530
+ if (anchor.target)
531
+ this.renderer.setAttribute(el, 'target', anchor.target);
532
+ if (anchor.rel)
533
+ this.renderer.setAttribute(el, 'rel', anchor.rel);
534
+ if (anchor.title)
535
+ this.renderer.setAttribute(el, 'title', anchor.title);
536
+ if (anchor['aria-label'])
537
+ this.renderer.setAttribute(el, 'aria-label', anchor['aria-label']);
538
+ if (anchor.download === true)
539
+ this.renderer.setAttribute(el, 'download', '');
540
+ else if (typeof anchor.download === 'string')
541
+ this.renderer.setAttribute(el, 'download', anchor.download);
542
+ if (anchor.hrefLang)
543
+ this.renderer.setAttribute(el, 'hreflang', anchor.hrefLang);
544
+ if (anchor.referrerPolicy)
545
+ this.renderer.setAttribute(el, 'referrerpolicy', anchor.referrerPolicy);
546
+ this.renderer.appendChild(el, this.renderer.createText(text));
547
+ return el;
548
+ }
549
+ clear() {
550
+ const el = this.host.nativeElement;
551
+ while (el.firstChild) {
552
+ this.renderer.removeChild(el, el.firstChild);
553
+ }
554
+ }
555
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: InlineMarkupComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component }); }
556
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: InlineMarkupComponent, isStandalone: true, selector: "rulecms-inline-markup", inputs: { text: "text", links: "links" }, host: { styleAttribute: "display: contents" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true }); }
557
+ }
558
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: InlineMarkupComponent, decorators: [{
559
+ type: Component,
560
+ args: [{
561
+ selector: 'rulecms-inline-markup',
562
+ standalone: true,
563
+ template: '',
564
+ host: { style: 'display: contents' },
565
+ }]
566
+ }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { text: [{
567
+ type: Input
568
+ }], links: [{
569
+ type: Input
570
+ }] } });
571
+
572
+ class HtmlTagComponent {
573
+ constructor(host, renderer, vcr) {
574
+ this.host = host;
575
+ this.renderer = renderer;
576
+ this.vcr = vcr;
577
+ this.created = null;
578
+ this.viewReady = false;
579
+ }
580
+ ngAfterViewInit() {
581
+ this.viewReady = true;
582
+ this.render();
583
+ }
584
+ ngOnChanges() {
585
+ if (this.viewReady) {
586
+ this.render();
587
+ }
588
+ }
589
+ render() {
590
+ if (this.created) {
591
+ this.renderer.removeChild(this.host.nativeElement, this.created);
592
+ this.created = null;
593
+ }
594
+ const el = this.renderer.createElement(this.tag);
595
+ if (this.className !== undefined) {
596
+ this.renderer.setAttribute(el, 'class', this.className);
597
+ }
598
+ if (this.styleProps) {
599
+ Object.entries(this.styleProps).forEach(([key, value]) => {
600
+ if (value !== undefined && value !== null && value !== '') {
601
+ this.renderer.setStyle(el, key, String(value));
602
+ }
603
+ });
604
+ }
605
+ const view = this.vcr.createEmbeddedView(this.projected);
606
+ view.rootNodes.forEach((node) => this.renderer.appendChild(el, node));
607
+ this.renderer.appendChild(this.host.nativeElement, el);
608
+ this.created = el;
609
+ }
610
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HtmlTagComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Component }); }
611
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: HtmlTagComponent, isStandalone: true, selector: "rulecms-html-tag", inputs: { tag: "tag", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, viewQueries: [{ propertyName: "projected", first: true, predicate: ["projected"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
612
+ <ng-template #projected>
613
+ <ng-content />
614
+ </ng-template>
615
+ `, isInline: true }); }
616
+ }
617
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HtmlTagComponent, decorators: [{
618
+ type: Component,
619
+ args: [{
620
+ selector: 'rulecms-html-tag',
621
+ standalone: true,
622
+ template: `
623
+ <ng-template #projected>
624
+ <ng-content />
625
+ </ng-template>
626
+ `,
627
+ host: { style: 'display: contents' },
628
+ }]
629
+ }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.ViewContainerRef }], propDecorators: { tag: [{
630
+ type: Input,
631
+ args: [{ required: true }]
632
+ }], styleProps: [{
633
+ type: Input
634
+ }], className: [{
635
+ type: Input
636
+ }], projected: [{
637
+ type: ViewChild,
638
+ args: ['projected', { static: true }]
639
+ }] } });
640
+
641
+ class RTextComponent {
642
+ get content() {
643
+ const contentVal = this.componentAttributes?.['content-text-3-resolutions'] ?? DEFAULT_CONTENT_TEXT;
644
+ return typeof contentVal === 'string' ? contentVal : String(contentVal ?? '');
645
+ }
646
+ get links() {
647
+ return this.componentAttributes?.['links-3-resolutions'];
648
+ }
649
+ get tag() {
650
+ return resolveEnclosingTag(this.componentAttributes);
651
+ }
652
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RTextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
653
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RTextComponent, isStandalone: true, selector: "rulecms-r-text", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
654
+ @if (tag) {
655
+ <rulecms-html-tag [tag]="tag" [styleProps]="styleProps" [className]="className">
656
+ <rulecms-inline-markup [text]="content" [links]="links" />
657
+ </rulecms-html-tag>
658
+ } @else {
659
+ <rulecms-inline-markup [text]="content" [links]="links" />
660
+ }
661
+ `, isInline: true, dependencies: [{ kind: "component", type: InlineMarkupComponent, selector: "rulecms-inline-markup", inputs: ["text", "links"] }, { kind: "component", type: HtmlTagComponent, selector: "rulecms-html-tag", inputs: ["tag", "styleProps", "className"] }] }); }
662
+ }
663
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RTextComponent, decorators: [{
664
+ type: Component,
665
+ args: [{
666
+ selector: 'rulecms-r-text',
667
+ standalone: true,
668
+ imports: [InlineMarkupComponent, HtmlTagComponent],
669
+ template: `
670
+ @if (tag) {
671
+ <rulecms-html-tag [tag]="tag" [styleProps]="styleProps" [className]="className">
672
+ <rulecms-inline-markup [text]="content" [links]="links" />
673
+ </rulecms-html-tag>
674
+ } @else {
675
+ <rulecms-inline-markup [text]="content" [links]="links" />
676
+ }
677
+ `,
678
+ host: { style: 'display: contents' },
679
+ }]
680
+ }], propDecorators: { componentAttributes: [{
681
+ type: Input
682
+ }], styleProps: [{
683
+ type: Input
684
+ }], className: [{
685
+ type: Input
686
+ }] } });
687
+
688
+ /**
689
+ * RText needs a wrapper div for the column's box styles unless it is about to
690
+ * render a block-level element that can hold those styles itself.
691
+ *
692
+ * - No enclosing tag: bare text with no element to style → wrapper needed.
693
+ * - Inline tag (`span`, `em`, `code`, …): vertical margin/padding and width
694
+ * are ignored on inline boxes → wrapper needed.
695
+ * - Block tag (`p`, `div`, `h1`, `section`, …): RText puts styleProps on the
696
+ * tag itself → no wrapper, one less element in the DOM.
697
+ */
698
+ const resolveNoContainer$8 = ({ componentAttributes, }) => !isBlockEnclosingTag(resolveEnclosingTag(componentAttributes));
699
+
700
+ /**
701
+ * True when RText is about to render an element that can carry a className —
702
+ * any enclosing tag, block or inline. Bare text has no element, so false.
703
+ *
704
+ * Shares `resolveEnclosingTag` with RText and `resolveNoContainer`: if this
705
+ * disagreed with what the component renders, a class would either land on
706
+ * nothing or force a redundant wrapper.
707
+ */
708
+ const resolveRendersOwnElement$8 = ({ componentAttributes, }) => !!resolveEnclosingTag(componentAttributes);
709
+
710
+ const attributeEditModeConfigs$8 = [
711
+ {
712
+ name: 'content-text-3-resolutions',
713
+ defaultValues: {},
714
+ },
715
+ {
716
+ name: 'enclosing-tag-3-resolutions',
717
+ heading: 'Enclosing Tag',
718
+ subHeading: 'HTML tag to enclose the text in (e.g. p, h1, span); see ENCLOSING_TAGS for the allowed values, for 3 different device resolutions',
719
+ defaultValues: {},
720
+ },
721
+ {
722
+ name: 'links-3-resolutions',
723
+ heading: 'Links',
724
+ subHeading: 'Links this text can point at. To link part of the text, wrap those words in {1|linked text}, where the number is the position of the entry in this list counting from 1; clearing an entry leaves its position in place so the other numbers keep pointing where they did, for 3 different device resolutions',
725
+ defaultValues: {},
726
+ },
727
+ ];
728
+
729
+ const ComponentIdentifiers = {
730
+ rText: 'r-text',
731
+ somethingElse: 'something-else',
732
+ cloudinaryAdvancedImage: 'cloudinary-advanced-image',
733
+ rVideo: 'r-video',
734
+ rIcon: 'r-icon',
735
+ rButton: 'r-button',
736
+ rDivider: 'r-divider',
737
+ rEmbed: 'r-embed',
738
+ rList: 'r-list',
739
+ rAccordion: 'r-accordion',
740
+ };
741
+
742
+ const displayCardEntry$8 = {
743
+ id: ComponentIdentifiers.rText,
744
+ resolveNoContainer: resolveNoContainer$8,
745
+ resolveRendersOwnElement: resolveRendersOwnElement$8,
746
+ card: RTextComponent,
747
+ defaultAttributeValues: defaultAttributeValues$8,
748
+ attributeEditModeConfigs: attributeEditModeConfigs$8,
749
+ };
750
+
751
+ // Builds Cloudinary delivery URLs directly so the component renders a complete
752
+ // <img src/srcSet> during server rendering (no browser-only Cloudinary SDK).
753
+ // `f_auto,q_auto` lets Cloudinary pick format/quality; `c_limit` scales down
754
+ // but never upscales beyond the original asset.
755
+ // Covers phone through large desktop displays, including 2x DPR on common
756
+ // viewport widths. Upper bound keeps multi-thousand-pixel originals in check.
757
+ const cloudinarySrcSetWidths = [480, 768, 1080, 1600, 2400];
758
+ // Mid-size fallback for browsers/tools that ignore srcset.
759
+ const fallbackSrcWidth = 1080;
760
+ const defaultTransformations = {
761
+ f: 'auto',
762
+ q: 'auto',
763
+ c: 'limit',
764
+ };
765
+ function serializeTransformations$1(transformations) {
766
+ return Object.entries(transformations)
767
+ .map(([key, value]) => `${key}_${value}`)
768
+ .join(',');
769
+ }
770
+ // Accepts the raw Cloudinary string form ("c_fill,g_auto,ar_4:3") and parses
771
+ // it into an object so it merges over the defaults the same way object input
772
+ // does (e.g. a user c_fill cleanly replaces the default c_limit)
773
+ function parseTransformationString$1(value) {
774
+ const result = {};
775
+ value.split(',').forEach((entry) => {
776
+ const trimmed = entry.trim();
777
+ if (!trimmed) {
778
+ return;
779
+ }
780
+ const separatorIndex = trimmed.indexOf('_');
781
+ if (separatorIndex <= 0) {
782
+ return;
783
+ }
784
+ result[trimmed.slice(0, separatorIndex)] = trimmed.slice(separatorIndex + 1);
785
+ });
786
+ return result;
787
+ }
788
+ function buildCloudinaryImageUrl(cloudName, publicId, width, transformations) {
789
+ const userTransformations = typeof transformations === 'string'
790
+ ? parseTransformationString$1(transformations)
791
+ : transformations;
792
+ // User keys override defaults (object spread = "split the object onto the
793
+ // configuration"). `w` is always component-managed so srcset width variants
794
+ // stay correct; combined with user ar/c/g values Cloudinary produces
795
+ // consistent crops at every width.
796
+ const merged = {
797
+ ...defaultTransformations,
798
+ ...userTransformations,
799
+ w: width,
800
+ };
801
+ // encodeURI preserves the slashes that are part of Cloudinary public IDs
802
+ // (e.g. "images/<uuid>/<id>") while escaping unsafe characters
803
+ return encodeURI(`https://res.cloudinary.com/${cloudName}/image/upload/${serializeTransformations$1(merged)}/${publicId}`);
804
+ }
805
+ function buildCloudinarySrc(cloudName, publicId, transformations) {
806
+ return buildCloudinaryImageUrl(cloudName, publicId, fallbackSrcWidth, transformations);
807
+ }
808
+ function buildCloudinarySrcSet(cloudName, publicId, transformations) {
809
+ return cloudinarySrcSetWidths
810
+ .map((width) => `${buildCloudinaryImageUrl(cloudName, publicId, width, transformations)} ${width}w`)
811
+ .join(', ');
812
+ }
813
+
814
+ class CloudinaryAdvancedImageComponent {
815
+ constructor() {
816
+ this.cloudName = 'rulecms';
817
+ this.publicId = 'main-sample';
818
+ this.alt = 'RuleCMS Component';
819
+ }
820
+ get finalCloudName() {
821
+ return this.componentAttributes?.['cloudinary-cloud-name'] || this.cloudName;
822
+ }
823
+ get finalPublicId() {
824
+ return (this.componentAttributes?.['media-selector-3-resolutions'] ||
825
+ this.componentAttributes?.['cloudinary-public-id'] ||
826
+ this.publicId);
827
+ }
828
+ get finalTransformations() {
829
+ return this.componentAttributes?.['cloudinary-transformations-3-resolutions'] || this.transformations;
830
+ }
831
+ get src() {
832
+ return buildCloudinarySrc(this.finalCloudName, this.finalPublicId, this.finalTransformations);
833
+ }
834
+ get srcSet() {
835
+ return buildCloudinarySrcSet(this.finalCloudName, this.finalPublicId, this.finalTransformations);
836
+ }
837
+ get imageAttributes() {
838
+ const imageAttributes = {
839
+ alt: this.alt,
840
+ decoding: 'async',
841
+ sizes: '100vw',
842
+ };
843
+ const imageStyles = { height: 'auto' };
844
+ if (this.componentAttributes) {
845
+ Object.keys(this.componentAttributes).forEach((keyWithDimensionSuffix) => {
846
+ const value = this.componentAttributes[keyWithDimensionSuffix];
847
+ if (value === undefined || value === null) {
848
+ return;
849
+ }
850
+ const key = keyWithDimensionSuffix.endsWith('-3-resolutions')
851
+ ? keyWithDimensionSuffix.slice(0, -'-3-resolutions'.length)
852
+ : keyWithDimensionSuffix;
853
+ if (key.startsWith('pass-through-image-attributes-style-')) {
854
+ imageStyles[key.slice('pass-through-image-attributes-style-'.length)] = value;
855
+ }
856
+ else if (key.startsWith('pass-through-image-attributes-')) {
857
+ imageAttributes[key.slice('pass-through-image-attributes-'.length)] = value;
858
+ }
859
+ });
860
+ }
861
+ imageAttributes.__styles = imageStyles;
862
+ return imageAttributes;
863
+ }
864
+ get mergedClassName() {
865
+ const { className: passThroughClassName } = this.imageAttributes;
866
+ return [passThroughClassName, this.className].filter(Boolean).join(' ') || undefined;
867
+ }
868
+ get mergedStyles() {
869
+ return { ...this.imageAttributes.__styles, ...this.styleProps };
870
+ }
871
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: CloudinaryAdvancedImageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
872
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: CloudinaryAdvancedImageComponent, isStandalone: true, selector: "rulecms-cloudinary-image", inputs: { cloudName: "cloudName", publicId: "publicId", alt: "alt", transformations: "transformations", componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
873
+ <img
874
+ [src]="src"
875
+ [srcset]="srcSet"
876
+ [attr.alt]="imageAttributes['alt']"
877
+ [attr.decoding]="imageAttributes['decoding']"
878
+ [attr.sizes]="imageAttributes['sizes']"
879
+ [attr.loading]="imageAttributes['loading']"
880
+ [attr.fetchpriority]="imageAttributes['fetchpriority']"
881
+ [attr.width]="imageAttributes['width']"
882
+ [attr.height]="imageAttributes['height']"
883
+ [class]="mergedClassName"
884
+ [ngStyle]="mergedStyles"
885
+ />
886
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
887
+ }
888
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: CloudinaryAdvancedImageComponent, decorators: [{
889
+ type: Component,
890
+ args: [{
891
+ selector: 'rulecms-cloudinary-image',
892
+ standalone: true,
893
+ imports: [NgStyle],
894
+ template: `
895
+ <img
896
+ [src]="src"
897
+ [srcset]="srcSet"
898
+ [attr.alt]="imageAttributes['alt']"
899
+ [attr.decoding]="imageAttributes['decoding']"
900
+ [attr.sizes]="imageAttributes['sizes']"
901
+ [attr.loading]="imageAttributes['loading']"
902
+ [attr.fetchpriority]="imageAttributes['fetchpriority']"
903
+ [attr.width]="imageAttributes['width']"
904
+ [attr.height]="imageAttributes['height']"
905
+ [class]="mergedClassName"
906
+ [ngStyle]="mergedStyles"
907
+ />
908
+ `,
909
+ host: { style: 'display: contents' },
910
+ }]
911
+ }], propDecorators: { cloudName: [{
912
+ type: Input
913
+ }], publicId: [{
914
+ type: Input
915
+ }], alt: [{
916
+ type: Input
917
+ }], transformations: [{
918
+ type: Input
919
+ }], componentAttributes: [{
920
+ type: Input
921
+ }], styleProps: [{
922
+ type: Input
923
+ }], className: [{
924
+ type: Input
925
+ }] } });
926
+
927
+ /**
928
+ * The component spreads styleProps onto its `<img>`, a replaced element that
929
+ * honors min/max sizing, margin and padding directly — a wrapper div would
930
+ * only add a redundant element. There is no configuration that changes this,
931
+ * so the answer is the same for every render.
932
+ */
933
+ const resolveNoContainer$7 = () => false;
934
+
935
+ /**
936
+ * Always renders an `<img>`, which can carry a className. There is no
937
+ * configuration that changes this.
938
+ */
939
+ const resolveRendersOwnElement$7 = () => true;
940
+
941
+ const defaultAttributeValues$7 = [
942
+ {
943
+ name: 'cloudinary-cloud-name',
944
+ defaultValues: {},
945
+ },
946
+ {
947
+ name: 'cloudinary-public-id',
948
+ defaultValues: {},
949
+ },
950
+ {
951
+ name: 'media-selector-3-resolutions',
952
+ defaultValues: {
953
+ all: 'main-sample',
954
+ },
955
+ },
956
+ {
957
+ name: 'pass-through-image-attributes-alt-3-resolutions',
958
+ defaultValues: {},
959
+ },
960
+ {
961
+ name: 'pass-through-image-attributes-width-3-resolutions',
962
+ defaultValues: {},
963
+ },
964
+ {
965
+ name: 'pass-through-image-attributes-height-3-resolutions',
966
+ defaultValues: {},
967
+ },
968
+ {
969
+ name: 'pass-through-image-attributes-sizes-3-resolutions',
970
+ defaultValues: {},
971
+ },
972
+ {
973
+ name: 'pass-through-image-attributes-loading-3-resolutions',
974
+ defaultValues: {},
975
+ },
976
+ {
977
+ name: 'pass-through-image-attributes-fetchpriority-3-resolutions',
978
+ defaultValues: {},
979
+ },
980
+ {
981
+ name: 'cloudinary-transformations-3-resolutions',
982
+ defaultValues: {},
983
+ },
984
+ ];
985
+
986
+ const attributeEditModeConfigs$7 = [
987
+ {
988
+ name: 'cloudinary-cloud-name',
989
+ heading: 'Cloudinary Cloud Name',
990
+ subHeading: 'Set the Cloudinary cloud name for 3 different device resolutions',
991
+ defaultValues: {},
992
+ // Filled by Media Selector / team defaults — not something marketers edit by hand.
993
+ hiddenToUser: true,
994
+ },
995
+ {
996
+ name: 'cloudinary-public-id',
997
+ heading: 'Image Public ID',
998
+ subHeading: 'Set the Cloudinary image public ID for 3 different device resolutions',
999
+ defaultValues: {},
1000
+ // Filled by Media Selector — not something marketers edit by hand.
1001
+ hiddenToUser: true,
1002
+ },
1003
+ {
1004
+ name: 'media-selector-3-resolutions',
1005
+ heading: 'Media Selector',
1006
+ subHeading: 'Select media for 3 different device resolutions',
1007
+ defaultValues: {},
1008
+ },
1009
+ {
1010
+ name: 'pass-through-image-attributes-alt-3-resolutions',
1011
+ heading: 'Image Alt Text',
1012
+ subHeading: 'Set the image alt text for 3 different device resolutions',
1013
+ defaultValues: {},
1014
+ },
1015
+ {
1016
+ name: 'pass-through-image-attributes-width-3-resolutions',
1017
+ heading: 'Image Intrinsic Width',
1018
+ subHeading: 'Set the intrinsic pixel width (with height, reserves layout space before the image loads) for 3 different device resolutions',
1019
+ defaultValues: {},
1020
+ },
1021
+ {
1022
+ name: 'pass-through-image-attributes-height-3-resolutions',
1023
+ heading: 'Image Intrinsic Height',
1024
+ subHeading: 'Set the intrinsic pixel height (with width, reserves layout space before the image loads) for 3 different device resolutions',
1025
+ defaultValues: {},
1026
+ },
1027
+ {
1028
+ name: 'pass-through-image-attributes-sizes-3-resolutions',
1029
+ heading: 'Image Sizes',
1030
+ subHeading: 'Set the sizes attribute (e.g. "(max-width: 600px) 100vw, 600px") so the browser downloads the right srcset variant, for 3 different device resolutions',
1031
+ defaultValues: {},
1032
+ },
1033
+ {
1034
+ name: 'pass-through-image-attributes-loading-3-resolutions',
1035
+ heading: 'Image Loading',
1036
+ subHeading: 'Set to "lazy" for below-the-fold images (never for the main hero image), for 3 different device resolutions',
1037
+ defaultValues: {},
1038
+ },
1039
+ {
1040
+ name: 'pass-through-image-attributes-fetchpriority-3-resolutions',
1041
+ heading: 'Image Fetch Priority',
1042
+ subHeading: 'Set to "high" for the most important image on the page (e.g. the hero), for 3 different device resolutions',
1043
+ defaultValues: {},
1044
+ },
1045
+ {
1046
+ name: 'cloudinary-transformations-3-resolutions',
1047
+ heading: 'Cloudinary Transformations',
1048
+ subHeading: 'Set extra Cloudinary transformations (e.g. "c_fill,g_auto,ar_4:3"); merged over the defaults f_auto,q_auto,c_limit, for 3 different device resolutions',
1049
+ defaultValues: {},
1050
+ },
1051
+ ];
1052
+
1053
+ const displayCardEntry$7 = {
1054
+ id: ComponentIdentifiers.cloudinaryAdvancedImage,
1055
+ resolveNoContainer: resolveNoContainer$7,
1056
+ resolveRendersOwnElement: resolveRendersOwnElement$7,
1057
+ card: CloudinaryAdvancedImageComponent,
1058
+ defaultAttributeValues: defaultAttributeValues$7,
1059
+ attributeEditModeConfigs: attributeEditModeConfigs$7,
1060
+ };
1061
+
1062
+ // Builds media-host delivery URLs directly so the component renders a complete
1063
+ // <video src/poster> during server rendering (no browser-only SDK).
1064
+ //
1065
+ // Delivery choices for web video:
1066
+ // - `.mp4` extension: every uploaded source format (webm, mov, …) is
1067
+ // transcoded to progressive MP4, which plays in every browser.
1068
+ // - `vc_auto,q_auto`: the host picks the best codec and quality for the
1069
+ // requested container.
1070
+ // - `c_limit,w_1920`: scales oversized originals down to a web-appropriate
1071
+ // width but never upscales.
1072
+ const defaultVideoTransformations = {
1073
+ q: 'auto',
1074
+ vc: 'auto',
1075
+ c: 'limit',
1076
+ w: 1920,
1077
+ };
1078
+ // Poster is a single still frame: take the first frame (so_0), same
1079
+ // downscale cap as the video itself. Format comes from the .jpg extension.
1080
+ const defaultPosterTransformations = {
1081
+ so: 0,
1082
+ q: 'auto',
1083
+ c: 'limit',
1084
+ w: 1920,
1085
+ };
1086
+ // Custom posters are uploaded images, so they get the image pipeline's
1087
+ // automatic format/quality.
1088
+ const defaultCustomPosterTransformations = {
1089
+ f: 'auto',
1090
+ q: 'auto',
1091
+ c: 'limit',
1092
+ w: 1920,
1093
+ };
1094
+ function serializeTransformations(transformations) {
1095
+ return Object.entries(transformations)
1096
+ .map(([key, value]) => (value === '' ? key : `${key}_${value}`))
1097
+ .join(',');
1098
+ }
1099
+ // Accepts the raw string form ("c_fill,ar_16:9") and parses it into an object
1100
+ // so it merges over the defaults the same way object input does.
1101
+ function parseTransformationString(value) {
1102
+ const result = {};
1103
+ value.split(',').forEach((entry) => {
1104
+ const trimmed = entry.trim();
1105
+ if (!trimmed) {
1106
+ return;
1107
+ }
1108
+ const separatorIndex = trimmed.indexOf('_');
1109
+ if (separatorIndex <= 0) {
1110
+ // Flag-style component with no value (e.g. "so_0" handled above; keep
1111
+ // unknown flags as bare keys so they serialize unchanged).
1112
+ result[trimmed] = '';
1113
+ return;
1114
+ }
1115
+ result[trimmed.slice(0, separatorIndex)] = trimmed.slice(separatorIndex + 1);
1116
+ });
1117
+ return result;
1118
+ }
1119
+ function mergeTransformations(defaults, transformations) {
1120
+ const userTransformations = typeof transformations === 'string'
1121
+ ? parseTransformationString(transformations)
1122
+ : transformations;
1123
+ return { ...defaults, ...userTransformations };
1124
+ }
1125
+ function buildVideoSrc(cloudName, publicId, transformations) {
1126
+ const merged = mergeTransformations(defaultVideoTransformations, transformations);
1127
+ // encodeURI preserves the slashes that are part of public IDs
1128
+ // (e.g. "videos/<uuid>/<id>") while escaping unsafe characters
1129
+ return encodeURI(`https://res.cloudinary.com/${cloudName}/video/upload/${serializeTransformations(merged)}/${publicId}.mp4`);
1130
+ }
1131
+ // First frame of the video itself — the zero-configuration poster.
1132
+ function buildVideoPosterSrc(cloudName, publicId) {
1133
+ return encodeURI(`https://res.cloudinary.com/${cloudName}/video/upload/${serializeTransformations(defaultPosterTransformations)}/${publicId}.jpg`);
1134
+ }
1135
+ // A separately uploaded image chosen as the poster.
1136
+ function buildCustomPosterSrc(cloudName, posterPublicId) {
1137
+ return encodeURI(`https://res.cloudinary.com/${cloudName}/image/upload/${serializeTransformations(defaultCustomPosterTransformations)}/${posterPublicId}`);
1138
+ }
1139
+
1140
+ const BOOLEAN_VIDEO_ATTRIBUTES = new Set(['controls', 'autoplay', 'loop', 'muted', 'playsinline']);
1141
+ const coerceBoolean = (value) => value === true || value === 'true';
1142
+ class RVideoComponent {
1143
+ constructor() {
1144
+ this.cloudName = 'rulecms';
1145
+ this.publicId = 'main-sample-video';
1146
+ }
1147
+ get finalCloudName() {
1148
+ return this.componentAttributes?.['video-host-cloud-name'] || this.cloudName;
1149
+ }
1150
+ get finalPublicId() {
1151
+ return (this.componentAttributes?.['video-selector-3-resolutions'] ||
1152
+ this.componentAttributes?.['video-public-id'] ||
1153
+ this.publicId);
1154
+ }
1155
+ get finalTransformations() {
1156
+ return this.componentAttributes?.['video-transformations-3-resolutions'] || this.transformations;
1157
+ }
1158
+ get src() {
1159
+ return buildVideoSrc(this.finalCloudName, this.finalPublicId, this.finalTransformations);
1160
+ }
1161
+ get videoAttributes() {
1162
+ const posterPublicId = this.componentAttributes?.['video-poster-selector-3-resolutions'];
1163
+ const videoAttributes = {
1164
+ controls: true,
1165
+ playsinline: true,
1166
+ preload: 'metadata',
1167
+ poster: posterPublicId
1168
+ ? buildCustomPosterSrc(this.finalCloudName, posterPublicId)
1169
+ : buildVideoPosterSrc(this.finalCloudName, this.finalPublicId),
1170
+ };
1171
+ const videoStyles = {
1172
+ maxWidth: '100%',
1173
+ height: 'auto',
1174
+ };
1175
+ if (this.componentAttributes) {
1176
+ Object.keys(this.componentAttributes).forEach((keyWithDimensionSuffix) => {
1177
+ const value = this.componentAttributes[keyWithDimensionSuffix];
1178
+ if (value === undefined || value === null) {
1179
+ return;
1180
+ }
1181
+ const key = keyWithDimensionSuffix.endsWith('-3-resolutions')
1182
+ ? keyWithDimensionSuffix.slice(0, -'-3-resolutions'.length)
1183
+ : keyWithDimensionSuffix;
1184
+ if (key.startsWith('pass-through-video-attributes-style-')) {
1185
+ videoStyles[key.slice('pass-through-video-attributes-style-'.length)] = value;
1186
+ }
1187
+ else if (key.startsWith('pass-through-video-attributes-')) {
1188
+ const attributeName = key.slice('pass-through-video-attributes-'.length);
1189
+ if (BOOLEAN_VIDEO_ATTRIBUTES.has(attributeName)) {
1190
+ if (coerceBoolean(value)) {
1191
+ videoAttributes[attributeName] = true;
1192
+ }
1193
+ else {
1194
+ delete videoAttributes[attributeName];
1195
+ }
1196
+ return;
1197
+ }
1198
+ videoAttributes[attributeName] = value;
1199
+ }
1200
+ });
1201
+ }
1202
+ videoAttributes.__styles = videoStyles;
1203
+ return videoAttributes;
1204
+ }
1205
+ get mergedClassName() {
1206
+ const { className: passThroughClassName } = this.videoAttributes;
1207
+ return [passThroughClassName, this.className].filter(Boolean).join(' ') || undefined;
1208
+ }
1209
+ get mergedStyles() {
1210
+ return { ...this.videoAttributes.__styles, ...this.styleProps };
1211
+ }
1212
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1213
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: RVideoComponent, isStandalone: true, selector: "rulecms-r-video", inputs: { cloudName: "cloudName", publicId: "publicId", transformations: "transformations", componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
1214
+ <video
1215
+ [src]="src"
1216
+ [attr.poster]="videoAttributes['poster']"
1217
+ [attr.controls]="videoAttributes['controls'] ? true : null"
1218
+ [attr.autoplay]="videoAttributes['autoplay'] ? true : null"
1219
+ [attr.loop]="videoAttributes['loop'] ? true : null"
1220
+ [attr.muted]="videoAttributes['muted'] ? true : null"
1221
+ [attr.playsinline]="videoAttributes['playsinline'] ? true : null"
1222
+ [attr.preload]="videoAttributes['preload']"
1223
+ [attr.width]="videoAttributes['width']"
1224
+ [attr.height]="videoAttributes['height']"
1225
+ [class]="mergedClassName"
1226
+ [ngStyle]="mergedStyles"
1227
+ ></video>
1228
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
1229
+ }
1230
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RVideoComponent, decorators: [{
1231
+ type: Component,
1232
+ args: [{
1233
+ selector: 'rulecms-r-video',
1234
+ standalone: true,
1235
+ imports: [NgStyle],
1236
+ template: `
1237
+ <video
1238
+ [src]="src"
1239
+ [attr.poster]="videoAttributes['poster']"
1240
+ [attr.controls]="videoAttributes['controls'] ? true : null"
1241
+ [attr.autoplay]="videoAttributes['autoplay'] ? true : null"
1242
+ [attr.loop]="videoAttributes['loop'] ? true : null"
1243
+ [attr.muted]="videoAttributes['muted'] ? true : null"
1244
+ [attr.playsinline]="videoAttributes['playsinline'] ? true : null"
1245
+ [attr.preload]="videoAttributes['preload']"
1246
+ [attr.width]="videoAttributes['width']"
1247
+ [attr.height]="videoAttributes['height']"
1248
+ [class]="mergedClassName"
1249
+ [ngStyle]="mergedStyles"
1250
+ ></video>
1251
+ `,
1252
+ host: { style: 'display: contents' },
1253
+ }]
1254
+ }], propDecorators: { cloudName: [{
1255
+ type: Input
1256
+ }], publicId: [{
1257
+ type: Input
1258
+ }], transformations: [{
1259
+ type: Input
1260
+ }], componentAttributes: [{
1261
+ type: Input
1262
+ }], styleProps: [{
1263
+ type: Input
1264
+ }], className: [{
1265
+ type: Input
1266
+ }] } });
1267
+
1268
+ /**
1269
+ * The component spreads styleProps onto its `<video>`, a replaced element that
1270
+ * honors min/max sizing, margin and padding directly — a wrapper div would
1271
+ * only add a redundant element. There is no configuration that changes this,
1272
+ * so the answer is the same for every render.
1273
+ */
1274
+ const resolveNoContainer$6 = () => false;
1275
+
1276
+ /**
1277
+ * Always renders a `<video>`, which can carry a className. There is no
1278
+ * configuration that changes this.
1279
+ */
1280
+ const resolveRendersOwnElement$6 = () => true;
1281
+
1282
+ const defaultAttributeValues$6 = [
1283
+ {
1284
+ name: 'video-host-cloud-name',
1285
+ defaultValues: {},
1286
+ },
1287
+ {
1288
+ name: 'video-public-id',
1289
+ defaultValues: {},
1290
+ },
1291
+ {
1292
+ name: 'video-selector-3-resolutions',
1293
+ defaultValues: {
1294
+ all: 'main-sample-video',
1295
+ },
1296
+ },
1297
+ {
1298
+ name: 'video-poster-selector-3-resolutions',
1299
+ defaultValues: {},
1300
+ },
1301
+ {
1302
+ name: 'pass-through-video-attributes-controls-3-resolutions',
1303
+ defaultValues: {},
1304
+ },
1305
+ {
1306
+ name: 'pass-through-video-attributes-autoplay-3-resolutions',
1307
+ defaultValues: {},
1308
+ },
1309
+ {
1310
+ name: 'pass-through-video-attributes-loop-3-resolutions',
1311
+ defaultValues: {},
1312
+ },
1313
+ {
1314
+ name: 'pass-through-video-attributes-muted-3-resolutions',
1315
+ defaultValues: {},
1316
+ },
1317
+ {
1318
+ name: 'pass-through-video-attributes-playsinline-3-resolutions',
1319
+ defaultValues: {},
1320
+ },
1321
+ {
1322
+ name: 'pass-through-video-attributes-preload-3-resolutions',
1323
+ defaultValues: {},
1324
+ },
1325
+ {
1326
+ name: 'pass-through-video-attributes-width-3-resolutions',
1327
+ defaultValues: {},
1328
+ },
1329
+ {
1330
+ name: 'pass-through-video-attributes-height-3-resolutions',
1331
+ defaultValues: {},
1332
+ },
1333
+ {
1334
+ name: 'video-transformations-3-resolutions',
1335
+ defaultValues: {},
1336
+ },
1337
+ ];
1338
+
1339
+ const attributeEditModeConfigs$6 = [
1340
+ {
1341
+ name: 'video-host-cloud-name',
1342
+ heading: 'Video Host Cloud Name',
1343
+ subHeading: 'Set the media host cloud name for 3 different device resolutions',
1344
+ defaultValues: {},
1345
+ // Filled by Video Selector / team defaults — not something marketers edit by hand.
1346
+ hiddenToUser: true,
1347
+ },
1348
+ {
1349
+ name: 'video-public-id',
1350
+ heading: 'Video Public ID',
1351
+ subHeading: 'Set the video public ID for 3 different device resolutions',
1352
+ defaultValues: {},
1353
+ // Filled by Video Selector — not something marketers edit by hand.
1354
+ hiddenToUser: true,
1355
+ },
1356
+ {
1357
+ name: 'video-selector-3-resolutions',
1358
+ heading: 'Video Selector',
1359
+ subHeading: 'Select a video for 3 different device resolutions',
1360
+ defaultValues: {},
1361
+ },
1362
+ {
1363
+ name: 'video-poster-selector-3-resolutions',
1364
+ heading: 'Poster Image',
1365
+ subHeading: 'Select an uploaded image shown before playback; defaults to the video\'s first frame, for 3 different device resolutions',
1366
+ defaultValues: {},
1367
+ },
1368
+ {
1369
+ name: 'pass-through-video-attributes-controls-3-resolutions',
1370
+ heading: 'Video Controls',
1371
+ subHeading: 'Show the browser playback controls (default "true"; set "false" for background/ambient video), for 3 different device resolutions',
1372
+ defaultValues: {},
1373
+ },
1374
+ {
1375
+ name: 'pass-through-video-attributes-autoplay-3-resolutions',
1376
+ heading: 'Video Autoplay',
1377
+ subHeading: 'Start playback automatically (browsers require muted "true" for autoplay to work), for 3 different device resolutions',
1378
+ defaultValues: {},
1379
+ },
1380
+ {
1381
+ name: 'pass-through-video-attributes-loop-3-resolutions',
1382
+ heading: 'Video Loop',
1383
+ subHeading: 'Restart the video automatically when it ends, for 3 different device resolutions',
1384
+ defaultValues: {},
1385
+ },
1386
+ {
1387
+ name: 'pass-through-video-attributes-muted-3-resolutions',
1388
+ heading: 'Video Muted',
1389
+ subHeading: 'Start with the audio muted (required by browsers for autoplay), for 3 different device resolutions',
1390
+ defaultValues: {},
1391
+ },
1392
+ {
1393
+ name: 'pass-through-video-attributes-playsinline-3-resolutions',
1394
+ heading: 'Video Plays Inline',
1395
+ subHeading: 'Play within the page on mobile instead of forcing fullscreen (default "true"), for 3 different device resolutions',
1396
+ defaultValues: {},
1397
+ },
1398
+ {
1399
+ name: 'pass-through-video-attributes-preload-3-resolutions',
1400
+ heading: 'Video Preload',
1401
+ subHeading: 'How much to download before playback: "none", "metadata" (default) or "auto", for 3 different device resolutions',
1402
+ defaultValues: {},
1403
+ },
1404
+ {
1405
+ name: 'pass-through-video-attributes-width-3-resolutions',
1406
+ heading: 'Video Intrinsic Width',
1407
+ subHeading: 'Set the intrinsic pixel width (with height, reserves layout space before the video loads) for 3 different device resolutions',
1408
+ defaultValues: {},
1409
+ },
1410
+ {
1411
+ name: 'pass-through-video-attributes-height-3-resolutions',
1412
+ heading: 'Video Intrinsic Height',
1413
+ subHeading: 'Set the intrinsic pixel height (with width, reserves layout space before the video loads) for 3 different device resolutions',
1414
+ defaultValues: {},
1415
+ },
1416
+ {
1417
+ name: 'video-transformations-3-resolutions',
1418
+ heading: 'Video Transformations',
1419
+ subHeading: 'Set extra delivery transformations (e.g. "c_fill,ar_16:9"); merged over the defaults q_auto,vc_auto,c_limit,w_1920, for 3 different device resolutions',
1420
+ defaultValues: {},
1421
+ },
1422
+ ];
1423
+
1424
+ const displayCardEntry$6 = {
1425
+ id: ComponentIdentifiers.rVideo,
1426
+ resolveNoContainer: resolveNoContainer$6,
1427
+ resolveRendersOwnElement: resolveRendersOwnElement$6,
1428
+ card: RVideoComponent,
1429
+ defaultAttributeValues: defaultAttributeValues$6,
1430
+ attributeEditModeConfigs: attributeEditModeConfigs$6,
1431
+ };
1432
+
1433
+ /**
1434
+ * The placeholder a freshly dropped icon shows before the author picks one:
1435
+ * Lucide's `shapes`, inlined as a literal.
1436
+ *
1437
+ * It is a literal rather than a registry lookup on purpose. Nothing on the
1438
+ * render path may import `lucide-static` — the default library is registered
1439
+ * eagerly by every consumer, so its bundle reaches every customer page.
1440
+ * `__tests__/default-attribute-values.test.ts` checks this literal still
1441
+ * matches the registry, so a drift shows up as a test failure rather than a
1442
+ * placeholder that quietly stops matching the real icon.
1443
+ */
1444
+ const PLACEHOLDER_ICON_NAME = 'shapes';
1445
+ const PLACEHOLDER_ICON_NODE = [
1446
+ [
1447
+ 'path',
1448
+ { d: 'M8.3 10a.7.7 0 0 1-.626-1.079L11.4 3a.7.7 0 0 1 1.198-.043L16.3 8.9a.7.7 0 0 1-.572 1.1Z' },
1449
+ ],
1450
+ ['rect', { x: '3', y: '14', width: '7', height: '7', rx: '1' }],
1451
+ ['circle', { cx: '17.5', cy: '17.5', r: '3.5' }],
1452
+ ];
1453
+ const PLACEHOLDER_ICON = {
1454
+ name: PLACEHOLDER_ICON_NAME,
1455
+ iconNode: PLACEHOLDER_ICON_NODE,
1456
+ };
1457
+ const isIconNode = (value) => Array.isArray(value) &&
1458
+ value.length > 0 &&
1459
+ value.every((entry) => Array.isArray(entry) &&
1460
+ entry.length >= 2 &&
1461
+ typeof entry[0] === 'string' &&
1462
+ entry[1] != null &&
1463
+ typeof entry[1] === 'object' &&
1464
+ !Array.isArray(entry[1]));
1465
+ /**
1466
+ * Read geometry out of a persisted icon attribute.
1467
+ *
1468
+ * The composer writes `{ name, iconNode }` when the author picks an icon, so
1469
+ * every surface renders from the value alone and never needs the registry.
1470
+ * Anything else — a bare name string from a hand-edited config, a half-written
1471
+ * value — yields null and renders nothing, which is the specified behaviour
1472
+ * for an unresolvable icon on a published page.
1473
+ */
1474
+ const resolveIconAttributeValue = (value) => {
1475
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1476
+ return null;
1477
+ }
1478
+ const { iconNode } = value;
1479
+ return isIconNode(iconNode) ? iconNode : null;
1480
+ };
1481
+
1482
+ /** Turns persisted Lucide geometry into SVG children on `svg`. */
1483
+ function appendIconChildren(renderer, svg, iconNode) {
1484
+ iconNode.forEach(([tag, attributes]) => {
1485
+ const child = renderer.createElement(tag, 'svg');
1486
+ Object.entries(attributes).forEach(([name, value]) => {
1487
+ if (name !== 'key') {
1488
+ renderer.setAttribute(child, name, value);
1489
+ }
1490
+ });
1491
+ renderer.appendChild(svg, child);
1492
+ });
1493
+ }
1494
+
1495
+ class SvgIconComponent {
1496
+ constructor(host, renderer) {
1497
+ this.host = host;
1498
+ this.renderer = renderer;
1499
+ this.width = 24;
1500
+ this.height = 24;
1501
+ this.ariaHidden = true;
1502
+ this.a11yProps = {};
1503
+ }
1504
+ ngOnChanges() {
1505
+ const el = this.host.nativeElement;
1506
+ while (el.firstChild) {
1507
+ this.renderer.removeChild(el, el.firstChild);
1508
+ }
1509
+ const svg = this.renderer.createElement('svg', 'svg');
1510
+ this.renderer.setAttribute(svg, 'xmlns', 'http://www.w3.org/2000/svg');
1511
+ this.renderer.setAttribute(svg, 'width', String(this.width));
1512
+ this.renderer.setAttribute(svg, 'height', String(this.height));
1513
+ this.renderer.setAttribute(svg, 'viewBox', '0 0 24 24');
1514
+ this.renderer.setAttribute(svg, 'fill', 'none');
1515
+ this.renderer.setAttribute(svg, 'stroke', 'currentColor');
1516
+ this.renderer.setAttribute(svg, 'stroke-width', '2');
1517
+ this.renderer.setAttribute(svg, 'stroke-linecap', 'round');
1518
+ this.renderer.setAttribute(svg, 'stroke-linejoin', 'round');
1519
+ if (this.className !== undefined) {
1520
+ this.renderer.setAttribute(svg, 'class', this.className);
1521
+ }
1522
+ if (this.styleProps) {
1523
+ Object.entries(this.styleProps).forEach(([key, value]) => {
1524
+ if (value !== undefined && value !== null && value !== '') {
1525
+ this.renderer.setStyle(svg, key, String(value));
1526
+ }
1527
+ });
1528
+ }
1529
+ Object.entries(this.a11yProps).forEach(([key, value]) => {
1530
+ this.renderer.setAttribute(svg, key, value);
1531
+ });
1532
+ if (this.ariaHidden && Object.keys(this.a11yProps).length === 0) {
1533
+ this.renderer.setAttribute(svg, 'aria-hidden', 'true');
1534
+ }
1535
+ appendIconChildren(this.renderer, svg, this.iconNode);
1536
+ this.renderer.appendChild(el, svg);
1537
+ }
1538
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: SvgIconComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component }); }
1539
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: SvgIconComponent, isStandalone: true, selector: "rulecms-svg-icon", inputs: { iconNode: "iconNode", width: "width", height: "height", className: "className", styleProps: "styleProps", ariaHidden: "ariaHidden", a11yProps: "a11yProps" }, host: { styleAttribute: "display: contents" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true }); }
1540
+ }
1541
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: SvgIconComponent, decorators: [{
1542
+ type: Component,
1543
+ args: [{
1544
+ selector: 'rulecms-svg-icon',
1545
+ standalone: true,
1546
+ template: '',
1547
+ host: { style: 'display: contents' },
1548
+ }]
1549
+ }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { iconNode: [{
1550
+ type: Input,
1551
+ args: [{ required: true }]
1552
+ }], width: [{
1553
+ type: Input
1554
+ }], height: [{
1555
+ type: Input
1556
+ }], className: [{
1557
+ type: Input
1558
+ }], styleProps: [{
1559
+ type: Input
1560
+ }], ariaHidden: [{
1561
+ type: Input
1562
+ }], a11yProps: [{
1563
+ type: Input
1564
+ }] } });
1565
+
1566
+ const A11Y_ATTR_KEYS = [
1567
+ 'aria-label',
1568
+ 'aria-labelledby',
1569
+ 'aria-describedby',
1570
+ 'role',
1571
+ 'title',
1572
+ ];
1573
+ class RIconComponent {
1574
+ get iconNode() {
1575
+ const value = this.componentAttributes?.['icon-name-3-resolutions'];
1576
+ return value == null ? PLACEHOLDER_ICON_NODE : resolveIconAttributeValue(value);
1577
+ }
1578
+ get a11yProps() {
1579
+ if (!this.componentAttributes)
1580
+ return {};
1581
+ const a11yProps = {};
1582
+ for (const key of A11Y_ATTR_KEYS) {
1583
+ const value = this.componentAttributes[key];
1584
+ if (typeof value === 'string' && value.length > 0) {
1585
+ a11yProps[key] = value;
1586
+ }
1587
+ }
1588
+ return a11yProps;
1589
+ }
1590
+ get hasA11yProp() {
1591
+ return Object.keys(this.a11yProps).length > 0;
1592
+ }
1593
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1594
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RIconComponent, isStandalone: true, selector: "rulecms-r-icon", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
1595
+ @if (iconNode) {
1596
+ <rulecms-svg-icon
1597
+ [iconNode]="iconNode"
1598
+ [className]="className"
1599
+ [styleProps]="styleProps"
1600
+ [a11yProps]="a11yProps"
1601
+ [ariaHidden]="!hasA11yProp"
1602
+ />
1603
+ }
1604
+ `, isInline: true, dependencies: [{ kind: "component", type: SvgIconComponent, selector: "rulecms-svg-icon", inputs: ["iconNode", "width", "height", "className", "styleProps", "ariaHidden", "a11yProps"] }] }); }
1605
+ }
1606
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RIconComponent, decorators: [{
1607
+ type: Component,
1608
+ args: [{
1609
+ selector: 'rulecms-r-icon',
1610
+ standalone: true,
1611
+ imports: [SvgIconComponent],
1612
+ template: `
1613
+ @if (iconNode) {
1614
+ <rulecms-svg-icon
1615
+ [iconNode]="iconNode"
1616
+ [className]="className"
1617
+ [styleProps]="styleProps"
1618
+ [a11yProps]="a11yProps"
1619
+ [ariaHidden]="!hasA11yProp"
1620
+ />
1621
+ }
1622
+ `,
1623
+ host: { style: 'display: contents' },
1624
+ }]
1625
+ }], propDecorators: { componentAttributes: [{
1626
+ type: Input
1627
+ }], styleProps: [{
1628
+ type: Input
1629
+ }], className: [{
1630
+ type: Input
1631
+ }] } });
1632
+
1633
+ /**
1634
+ * Always renders an `<svg>`, a replaced element that honours sizing, margin
1635
+ * and colour (via currentColor) directly, so a wrapper div would only add a
1636
+ * redundant element. No configuration changes that.
1637
+ */
1638
+ const resolveNoContainer$5 = () => false;
1639
+
1640
+ /**
1641
+ * The `<svg>` carries the className. An unresolvable attribute renders no
1642
+ * element at all, but that is a data error rather than a different root type,
1643
+ * so the configured happy path is what this answers for.
1644
+ */
1645
+ const resolveRendersOwnElement$5 = () => true;
1646
+
1647
+ const defaultAttributeValues$5 = [
1648
+ {
1649
+ // Holds `{ name, iconNode }`, not a bare name: the composer stamps the
1650
+ // geometry when the author picks the icon so the render path never needs
1651
+ // the icon registry.
1652
+ name: 'icon-name-3-resolutions',
1653
+ defaultValues: {
1654
+ all: PLACEHOLDER_ICON,
1655
+ },
1656
+ },
1657
+ ];
1658
+
1659
+ const attributeEditModeConfigs$5 = [
1660
+ {
1661
+ name: 'icon-name-3-resolutions',
1662
+ heading: 'Icon',
1663
+ subHeading: 'Search by name or by what the icon depicts (e.g. "launch" finds the rocket), for 3 different device resolutions',
1664
+ defaultValues: {
1665
+ all: PLACEHOLDER_ICON,
1666
+ },
1667
+ },
1668
+ ];
1669
+
1670
+ const displayCardEntry$5 = {
1671
+ id: ComponentIdentifiers.rIcon,
1672
+ resolveNoContainer: resolveNoContainer$5,
1673
+ resolveRendersOwnElement: resolveRendersOwnElement$5,
1674
+ card: RIconComponent,
1675
+ defaultAttributeValues: defaultAttributeValues$5,
1676
+ attributeEditModeConfigs: attributeEditModeConfigs$5,
1677
+ };
1678
+
1679
+ /**
1680
+ * @deprecated Use `parseInlineMarkers` from `shared/inline-markup`.
1681
+ *
1682
+ * The link-only view of the grammar, kept because it is public API. Markers now
1683
+ * also carry marks (`{b|word}`) and line breaks (`{br|}`), which this shape
1684
+ * cannot express, so anything that is not a linked span is flattened into text:
1685
+ * marked spans contribute their words without the marks, and a break
1686
+ * contributes a newline. That is exactly what a caller stripping markers out of
1687
+ * a string wants — `resolveAccordionSummary` is the one in this package — and
1688
+ * it is wrong for anything that means to render them.
1689
+ *
1690
+ * Adjacent text is merged, so text with no linked spans is still one run.
1691
+ */
1692
+ function parseInlineLinkMarkers(value) {
1693
+ const runs = [];
1694
+ let pendingText = '';
1695
+ const flushPendingText = () => {
1696
+ if (pendingText !== '') {
1697
+ runs.push({ kind: 'text', text: pendingText });
1698
+ pendingText = '';
1699
+ }
1700
+ };
1701
+ for (const run of parseInlineMarkers(value)) {
1702
+ if (run.kind === 'span' && run.index !== undefined) {
1703
+ flushPendingText();
1704
+ runs.push({ kind: 'link', index: run.index, text: run.text });
1705
+ continue;
1706
+ }
1707
+ pendingText += run.kind === 'break' ? '\n' : run.text;
1708
+ }
1709
+ flushPendingText();
1710
+ return runs;
1711
+ }
1712
+
1713
+ /**
1714
+ * The preset vocabulary for `r-button`.
1715
+ *
1716
+ * Three independent axes rather than one flat list of named buttons: five
1717
+ * variants times four tones times five sizes is a hundred looks from three
1718
+ * short tables, and an author picks each axis without us having to have
1719
+ * anticipated the combination.
1720
+ *
1721
+ * Every preset name here is persisted customer data, exactly like an icon
1722
+ * name. A name that has shipped is never deleted or repurposed — add a new
1723
+ * one and alias the old one, or published pages change appearance on upgrade.
1724
+ */
1725
+ /** How the button is filled and bordered. */
1726
+ const BUTTON_VARIANTS = ['solid', 'soft', 'outline', 'ghost', 'link'];
1727
+ /** Which colour family the variant draws from. */
1728
+ const BUTTON_TONES = ['brand', 'neutral', 'danger', 'success'];
1729
+ /** Type scale, padding and control height, smallest to largest. */
1730
+ const BUTTON_SIZES = ['xs', 'sm', 'md', 'lg', 'xl'];
1731
+ /**
1732
+ * What an unconfigured button looks like. Dropping the component on a canvas
1733
+ * and touching nothing must already produce something shippable.
1734
+ */
1735
+ const DEFAULT_BUTTON_PRESET = {
1736
+ variant: 'solid',
1737
+ tone: 'brand',
1738
+ size: 'md',
1739
+ };
1740
+ const isButtonVariant = (value) => BUTTON_VARIANTS.includes(value);
1741
+ const isButtonTone = (value) => BUTTON_TONES.includes(value);
1742
+ const isButtonSize = (value) => BUTTON_SIZES.includes(value);
1743
+ /**
1744
+ * Defaults only. Every one of these reaches the page as the fallback half of a
1745
+ * `var(--rulecms-button-…, default)`, so a customer rebrands every button on
1746
+ * their site by defining the custom property once, with no per-button edits
1747
+ * and no composer access. See `button-css-variables.ts`.
1748
+ */
1749
+ const TONE_PALETTES = {
1750
+ brand: {
1751
+ base: '#2563eb',
1752
+ hover: '#1d4ed8',
1753
+ active: '#1e40af',
1754
+ contrast: '#ffffff',
1755
+ border: '#2563eb',
1756
+ soft: '#eff6ff',
1757
+ softHover: '#dbeafe',
1758
+ softContrast: '#1d4ed8',
1759
+ },
1760
+ neutral: {
1761
+ base: '#334155',
1762
+ hover: '#1e293b',
1763
+ active: '#0f172a',
1764
+ contrast: '#ffffff',
1765
+ // Deliberately lighter than `base`: a secondary button whose border matches
1766
+ // its dark label text reads as heavy as a primary one.
1767
+ border: '#cbd5e1',
1768
+ soft: '#f1f5f9',
1769
+ softHover: '#e2e8f0',
1770
+ softContrast: '#334155',
1771
+ },
1772
+ danger: {
1773
+ base: '#dc2626',
1774
+ hover: '#b91c1c',
1775
+ active: '#991b1b',
1776
+ contrast: '#ffffff',
1777
+ border: '#dc2626',
1778
+ soft: '#fef2f2',
1779
+ softHover: '#fee2e2',
1780
+ softContrast: '#b91c1c',
1781
+ },
1782
+ success: {
1783
+ base: '#16a34a',
1784
+ hover: '#15803d',
1785
+ active: '#166534',
1786
+ contrast: '#ffffff',
1787
+ border: '#16a34a',
1788
+ soft: '#f0fdf4',
1789
+ softHover: '#dcfce7',
1790
+ softContrast: '#15803d',
1791
+ },
1792
+ };
1793
+ const SIZE_SCALES = {
1794
+ xs: {
1795
+ fontSize: '12px',
1796
+ minHeight: '28px',
1797
+ paddingInline: '10px',
1798
+ paddingBlock: '4px',
1799
+ borderRadius: '4px',
1800
+ gap: '4px',
1801
+ },
1802
+ sm: {
1803
+ fontSize: '14px',
1804
+ minHeight: '34px',
1805
+ paddingInline: '12px',
1806
+ paddingBlock: '6px',
1807
+ borderRadius: '6px',
1808
+ gap: '6px',
1809
+ },
1810
+ md: {
1811
+ fontSize: '16px',
1812
+ minHeight: '40px',
1813
+ paddingInline: '16px',
1814
+ paddingBlock: '8px',
1815
+ borderRadius: '8px',
1816
+ gap: '8px',
1817
+ },
1818
+ lg: {
1819
+ fontSize: '18px',
1820
+ minHeight: '48px',
1821
+ paddingInline: '20px',
1822
+ paddingBlock: '10px',
1823
+ borderRadius: '10px',
1824
+ gap: '8px',
1825
+ },
1826
+ xl: {
1827
+ fontSize: '20px',
1828
+ minHeight: '56px',
1829
+ paddingInline: '24px',
1830
+ paddingBlock: '12px',
1831
+ borderRadius: '12px',
1832
+ gap: '10px',
1833
+ },
1834
+ };
1835
+
1836
+ /**
1837
+ * Every colour a preset emits goes out as `var(--rulecms-button-…, default)`.
1838
+ *
1839
+ * This is the whole white-label story for buttons. A customer who sets
1840
+ *
1841
+ * :root { --rulecms-button-brand: #c8102e; }
1842
+ *
1843
+ * once in their own stylesheet rebrands every RuleCMS button on the site,
1844
+ * without touching a widget, without composer access, and without us shipping
1845
+ * a theme provider or a design system to their page. Authors who want one
1846
+ * button to differ still override it per column, as before.
1847
+ */
1848
+ const BUTTON_CSS_VARIABLE_PREFIX = '--rulecms-button';
1849
+ const toneCssVariable = (tone, slot = '') => slot === ''
1850
+ ? `${BUTTON_CSS_VARIABLE_PREFIX}-${tone}`
1851
+ : `${BUTTON_CSS_VARIABLE_PREFIX}-${tone}-${slot}`;
1852
+ /**
1853
+ * A `var()` reference carrying our default as the fallback, so the button is
1854
+ * fully styled whether or not the host defines anything.
1855
+ */
1856
+ const cssVarWithFallback = (variableName, fallback) => `var(${variableName}, ${fallback})`;
1857
+ /** Shorthand for the common case: one tone slot with its palette default. */
1858
+ const toneColor = (tone, slot, fallback) => cssVarWithFallback(toneCssVariable(tone, slot), fallback);
1859
+
1860
+ /**
1861
+ * The rules that stop a native `<button>` looking like a native `<button>`.
1862
+ *
1863
+ * Two of these do most of the work. `appearance: none` removes the platform
1864
+ * chrome iOS Safari still paints on form controls, and `fontFamily: inherit`
1865
+ * opts out of the browser's refusal to inherit typography into form controls —
1866
+ * which is the single biggest reason an unstyled button looks foreign on a
1867
+ * designed page. Inheriting means the button picks up the customer's brand
1868
+ * typeface with no configuration at all.
1869
+ */
1870
+ const structuralStyles = {
1871
+ appearance: 'none',
1872
+ WebkitAppearance: 'none',
1873
+ boxSizing: 'border-box',
1874
+ display: 'inline-flex',
1875
+ alignItems: 'center',
1876
+ justifyContent: 'center',
1877
+ textAlign: 'center',
1878
+ textDecoration: 'none',
1879
+ cursor: 'pointer',
1880
+ userSelect: 'none',
1881
+ WebkitUserSelect: 'none',
1882
+ verticalAlign: 'middle',
1883
+ borderStyle: 'solid',
1884
+ fontFamily: 'inherit',
1885
+ fontWeight: 600,
1886
+ lineHeight: 1.25,
1887
+ // Inert until something changes state. Kept because the host's own `:hover`
1888
+ // rule — the only hover mechanism available to a widget today — then eases
1889
+ // for free instead of snapping.
1890
+ transition: 'background-color 150ms ease, border-color 150ms ease, color 150ms ease',
1891
+ };
1892
+ const sizeStyles = (preset) => {
1893
+ const scale = SIZE_SCALES[preset.size];
1894
+ return {
1895
+ fontSize: scale.fontSize,
1896
+ minHeight: scale.minHeight,
1897
+ // Logical properties so a right-to-left page mirrors correctly.
1898
+ paddingInline: scale.paddingInline,
1899
+ paddingBlock: scale.paddingBlock,
1900
+ borderRadius: scale.borderRadius,
1901
+ gap: scale.gap,
1902
+ };
1903
+ };
1904
+ /**
1905
+ * Every variant keeps a 1px border, transparent where it is not wanted, so
1906
+ * switching preset never changes the button's box and never nudges the layout
1907
+ * around it.
1908
+ */
1909
+ const variantStyles = (preset) => {
1910
+ const palette = TONE_PALETTES[preset.tone];
1911
+ const tone = preset.tone;
1912
+ switch (preset.variant) {
1913
+ case 'solid':
1914
+ return {
1915
+ borderWidth: '1px',
1916
+ backgroundColor: toneColor(tone, '', palette.base),
1917
+ borderColor: toneColor(tone, '', palette.base),
1918
+ color: toneColor(tone, 'contrast', palette.contrast),
1919
+ };
1920
+ case 'soft':
1921
+ return {
1922
+ borderWidth: '1px',
1923
+ backgroundColor: toneColor(tone, 'soft', palette.soft),
1924
+ borderColor: 'transparent',
1925
+ color: toneColor(tone, 'soft-contrast', palette.softContrast),
1926
+ };
1927
+ case 'outline':
1928
+ return {
1929
+ borderWidth: '1px',
1930
+ backgroundColor: 'transparent',
1931
+ borderColor: toneColor(tone, 'border', palette.border),
1932
+ color: toneColor(tone, '', palette.base),
1933
+ };
1934
+ case 'ghost':
1935
+ return {
1936
+ borderWidth: '1px',
1937
+ backgroundColor: 'transparent',
1938
+ borderColor: 'transparent',
1939
+ color: toneColor(tone, '', palette.base),
1940
+ };
1941
+ case 'link':
1942
+ default:
1943
+ // Reads as prose, not as a control: the size scale's box is stripped
1944
+ // back off so it can sit in a sentence without a padded hit area.
1945
+ return {
1946
+ borderWidth: 0,
1947
+ backgroundColor: 'transparent',
1948
+ borderColor: 'transparent',
1949
+ color: toneColor(tone, '', palette.base),
1950
+ minHeight: 0,
1951
+ paddingInline: 0,
1952
+ paddingBlock: 0,
1953
+ borderRadius: 0,
1954
+ textDecoration: 'underline',
1955
+ textUnderlineOffset: '0.2em',
1956
+ };
1957
+ }
1958
+ };
1959
+ /**
1960
+ * The preset's contribution to the button's inline style.
1961
+ *
1962
+ * Callers spread column `styleProps` *after* this, which is what makes the
1963
+ * preset a set of defaults rather than a set of rules: anything the author
1964
+ * touched in Column Styles lands later in the object and wins.
1965
+ */
1966
+ function buildButtonPresetStyles(preset) {
1967
+ return {
1968
+ ...structuralStyles,
1969
+ ...sizeStyles(preset),
1970
+ ...variantStyles(preset),
1971
+ };
1972
+ }
1973
+ /** Applied on top when the button is disabled, before column styles. */
1974
+ const disabledStyles = {
1975
+ opacity: 0.5,
1976
+ cursor: 'not-allowed',
1977
+ };
1978
+
1979
+ /** The only key `r-button` reads out of the host's per-instance props. */
1980
+ const BUTTON_CLICK_HANDLER_PROP = 'onClick';
1981
+ /**
1982
+ * The host's click handler for this instance, or `undefined` when there is
1983
+ * nothing to attach.
1984
+ *
1985
+ * Two reasons to answer `undefined` even when the host supplied something:
1986
+ *
1987
+ * - **Disabled wins.** A disabled plain `<button>` is inert natively, but a
1988
+ * disabled *linked* button renders `<a role="link" aria-disabled="true">`,
1989
+ * which has no native inertness at all — a handler left on it would fire on
1990
+ * every click and make "disabled" a lie.
1991
+ * - **The bag is untyped.** It crosses a package boundary as
1992
+ * `{ [key: string]: any }`, so a host that passes a string here should get a
1993
+ * button that does nothing rather than a React crash on the customer's page.
1994
+ */
1995
+ const resolveButtonClickHandler = ({ widgetInstanceProps, disabled, }) => {
1996
+ if (disabled) {
1997
+ return undefined;
1998
+ }
1999
+ const handler = widgetInstanceProps?.[BUTTON_CLICK_HANDLER_PROP];
2000
+ return typeof handler === 'function' ? handler : undefined;
2001
+ };
2002
+
2003
+ /**
2004
+ * Turns whatever the composer stored into a complete preset.
2005
+ *
2006
+ * Tolerant on every axis independently: a value saved by a newer composer, a
2007
+ * half-written object, or an outright wrong type costs that one axis and
2008
+ * nothing else. A published page must never fail to render a button because
2009
+ * one field went stale.
2010
+ */
2011
+ function resolveButtonPreset(value) {
2012
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
2013
+ return DEFAULT_BUTTON_PRESET;
2014
+ }
2015
+ const candidate = value;
2016
+ return {
2017
+ variant: isButtonVariant(candidate.variant)
2018
+ ? candidate.variant
2019
+ : DEFAULT_BUTTON_PRESET.variant,
2020
+ tone: isButtonTone(candidate.tone) ? candidate.tone : DEFAULT_BUTTON_PRESET.tone,
2021
+ size: isButtonSize(candidate.size) ? candidate.size : DEFAULT_BUTTON_PRESET.size,
2022
+ };
2023
+ }
2024
+
2025
+ /**
2026
+ * The attribute names `r-button` reads.
2027
+ *
2028
+ * Every one is button-specific, including the label and the icon, which are
2029
+ * conceptually the same thing `r-text` and `r-icon` already offer. Sharing
2030
+ * their attribute names would have saved two lines of editor wiring and cost
2031
+ * the documentation contract: RuleCMS publishes one page per component-scope
2032
+ * attribute and requires exactly one owning component, so a shared name has
2033
+ * no single page to link to from the drawer.
2034
+ *
2035
+ * Reuse happens a level down instead. The composer resolves an attribute to
2036
+ * an *editor* by name, and several names already share one editor, so these
2037
+ * point at the existing text field and the existing Lucide picker without
2038
+ * either component having to give up ownership of its own settings.
2039
+ *
2040
+ * Nothing here duplicates a column style attribute. Colour, border, radius,
2041
+ * spacing and width already have editors that write into the same bag, and a
2042
+ * second way to set the same CSS property would only create a question about
2043
+ * which one wins.
2044
+ */
2045
+ const BUTTON_PRESET_ATTRIBUTE = 'button-preset-3-resolutions';
2046
+ const BUTTON_LABEL_ATTRIBUTE = 'button-label-3-resolutions';
2047
+ const BUTTON_LINK_ATTRIBUTE = 'button-link-3-resolutions';
2048
+ const BUTTON_DISABLED_ATTRIBUTE = 'button-disabled-3-resolutions';
2049
+ const BUTTON_ICON_ATTRIBUTE = 'button-icon-3-resolutions';
2050
+ const BUTTON_ICON_POSITION_ATTRIBUTE = 'button-icon-position-3-resolutions';
2051
+ const BUTTON_ARIA_LABEL_ATTRIBUTE = 'aria-label';
2052
+ /** What a freshly dropped button says before the author types anything. */
2053
+ const DEFAULT_BUTTON_LABEL = 'Button';
2054
+ const ICON_POSITIONS = ['leading', 'trailing'];
2055
+ const DEFAULT_ICON_POSITION = 'leading';
2056
+ /**
2057
+ * An unset label falls back to the placeholder, but a label the author
2058
+ * deliberately cleared stays cleared — that is how an icon-only button is
2059
+ * made, so an empty string cannot be treated as "nothing set".
2060
+ */
2061
+ const resolveButtonLabel = (componentAttributes) => {
2062
+ const value = componentAttributes?.[BUTTON_LABEL_ATTRIBUTE];
2063
+ if (value === undefined || value === null) {
2064
+ return DEFAULT_BUTTON_LABEL;
2065
+ }
2066
+ return typeof value === 'string' ? value : String(value);
2067
+ };
2068
+ /**
2069
+ * Select editors in the composer store `'true'` / `'false'` as strings, while
2070
+ * a hand-written or migrated config may carry a real boolean. Both mean the
2071
+ * same thing here.
2072
+ */
2073
+ const resolveButtonDisabled = (componentAttributes) => {
2074
+ const value = componentAttributes?.[BUTTON_DISABLED_ATTRIBUTE];
2075
+ if (typeof value === 'boolean') {
2076
+ return value;
2077
+ }
2078
+ return typeof value === 'string' && value.trim().toLowerCase() === 'true';
2079
+ };
2080
+ const resolveIconPosition = (componentAttributes) => {
2081
+ const value = componentAttributes?.[BUTTON_ICON_POSITION_ATTRIBUTE];
2082
+ return value === 'trailing' ? 'trailing' : DEFAULT_ICON_POSITION;
2083
+ };
2084
+ const resolveButtonAriaLabel = (componentAttributes) => {
2085
+ const value = componentAttributes?.[BUTTON_ARIA_LABEL_ATTRIBUTE];
2086
+ if (typeof value !== 'string') {
2087
+ return undefined;
2088
+ }
2089
+ const trimmed = value.trim();
2090
+ return trimmed === '' ? undefined : trimmed;
2091
+ };
2092
+
2093
+ class RButtonComponent {
2094
+ get preset() {
2095
+ return resolveButtonPreset(this.componentAttributes?.[BUTTON_PRESET_ATTRIBUTE]);
2096
+ }
2097
+ get disabled() {
2098
+ return resolveButtonDisabled(this.componentAttributes);
2099
+ }
2100
+ get style() {
2101
+ return {
2102
+ ...buildButtonPresetStyles(this.preset),
2103
+ ...(this.disabled ? disabledStyles : {}),
2104
+ ...this.styleProps,
2105
+ };
2106
+ }
2107
+ get label() {
2108
+ return resolveButtonLabel(this.componentAttributes);
2109
+ }
2110
+ get ariaLabel() {
2111
+ return resolveButtonAriaLabel(this.componentAttributes);
2112
+ }
2113
+ get link() {
2114
+ return resolveLinkAttributes(this.componentAttributes?.[BUTTON_LINK_ATTRIBUTE]);
2115
+ }
2116
+ get iconNode() {
2117
+ return resolveIconAttributeValue(this.componentAttributes?.[BUTTON_ICON_ATTRIBUTE]);
2118
+ }
2119
+ get iconPosition() {
2120
+ return resolveIconPosition(this.componentAttributes);
2121
+ }
2122
+ handleClick(event) {
2123
+ const onClick = resolveButtonClickHandler({
2124
+ widgetInstanceProps: this.widgetInstanceProps,
2125
+ disabled: this.disabled,
2126
+ });
2127
+ onClick?.(event);
2128
+ }
2129
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2130
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RButtonComponent, isStandalone: true, selector: "rulecms-r-button", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className", widgetInstanceProps: "widgetInstanceProps" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
2131
+ @if (link && disabled) {
2132
+ <a [ngStyle]="style" [class]="className || undefined" role="link" aria-disabled="true" [attr.aria-label]="ariaLabel">
2133
+ @if (iconNode && iconPosition !== 'trailing') {
2134
+ <rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
2135
+ }
2136
+ {{ label }}
2137
+ @if (iconNode && iconPosition === 'trailing') {
2138
+ <rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
2139
+ }
2140
+ </a>
2141
+ } @else if (link) {
2142
+ <a
2143
+ [attr.href]="link.href"
2144
+ [attr.target]="link.target"
2145
+ [attr.rel]="link.rel"
2146
+ [attr.title]="link.title"
2147
+ [attr.aria-label]="link['aria-label'] ?? ariaLabel"
2148
+ [attr.download]="link.download === true ? '' : link.download"
2149
+ [attr.hreflang]="link.hrefLang"
2150
+ [attr.referrerpolicy]="link.referrerPolicy"
2151
+ [ngStyle]="style"
2152
+ [class]="className || undefined"
2153
+ (click)="handleClick($event)"
2154
+ >
2155
+ @if (iconNode && iconPosition !== 'trailing') {
2156
+ <rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
2157
+ }
2158
+ {{ label }}
2159
+ @if (iconNode && iconPosition === 'trailing') {
2160
+ <rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
2161
+ }
2162
+ </a>
2163
+ } @else {
2164
+ <button
2165
+ type="button"
2166
+ [disabled]="disabled"
2167
+ [ngStyle]="style"
2168
+ [class]="className || undefined"
2169
+ [attr.aria-label]="ariaLabel"
2170
+ (click)="handleClick($event)"
2171
+ >
2172
+ @if (iconNode && iconPosition !== 'trailing') {
2173
+ <rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
2174
+ }
2175
+ {{ label }}
2176
+ @if (iconNode && iconPosition === 'trailing') {
2177
+ <rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
2178
+ }
2179
+ </button>
2180
+ }
2181
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SvgIconComponent, selector: "rulecms-svg-icon", inputs: ["iconNode", "width", "height", "className", "styleProps", "ariaHidden", "a11yProps"] }] }); }
2182
+ }
2183
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RButtonComponent, decorators: [{
2184
+ type: Component,
2185
+ args: [{
2186
+ selector: 'rulecms-r-button',
2187
+ standalone: true,
2188
+ imports: [NgStyle, SvgIconComponent],
2189
+ template: `
2190
+ @if (link && disabled) {
2191
+ <a [ngStyle]="style" [class]="className || undefined" role="link" aria-disabled="true" [attr.aria-label]="ariaLabel">
2192
+ @if (iconNode && iconPosition !== 'trailing') {
2193
+ <rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
2194
+ }
2195
+ {{ label }}
2196
+ @if (iconNode && iconPosition === 'trailing') {
2197
+ <rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
2198
+ }
2199
+ </a>
2200
+ } @else if (link) {
2201
+ <a
2202
+ [attr.href]="link.href"
2203
+ [attr.target]="link.target"
2204
+ [attr.rel]="link.rel"
2205
+ [attr.title]="link.title"
2206
+ [attr.aria-label]="link['aria-label'] ?? ariaLabel"
2207
+ [attr.download]="link.download === true ? '' : link.download"
2208
+ [attr.hreflang]="link.hrefLang"
2209
+ [attr.referrerpolicy]="link.referrerPolicy"
2210
+ [ngStyle]="style"
2211
+ [class]="className || undefined"
2212
+ (click)="handleClick($event)"
2213
+ >
2214
+ @if (iconNode && iconPosition !== 'trailing') {
2215
+ <rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
2216
+ }
2217
+ {{ label }}
2218
+ @if (iconNode && iconPosition === 'trailing') {
2219
+ <rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
2220
+ }
2221
+ </a>
2222
+ } @else {
2223
+ <button
2224
+ type="button"
2225
+ [disabled]="disabled"
2226
+ [ngStyle]="style"
2227
+ [class]="className || undefined"
2228
+ [attr.aria-label]="ariaLabel"
2229
+ (click)="handleClick($event)"
2230
+ >
2231
+ @if (iconNode && iconPosition !== 'trailing') {
2232
+ <rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
2233
+ }
2234
+ {{ label }}
2235
+ @if (iconNode && iconPosition === 'trailing') {
2236
+ <rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
2237
+ }
2238
+ </button>
2239
+ }
2240
+ `,
2241
+ host: { style: 'display: contents' },
2242
+ }]
2243
+ }], propDecorators: { componentAttributes: [{
2244
+ type: Input
2245
+ }], styleProps: [{
2246
+ type: Input
2247
+ }], className: [{
2248
+ type: Input
2249
+ }], widgetInstanceProps: [{
2250
+ type: Input
2251
+ }] } });
2252
+
2253
+ /**
2254
+ * Always renders a `<button>` or an `<a>`, both of which take padding, border,
2255
+ * background and sizing directly. A wrapper div would add an element that does
2256
+ * nothing, and would put the column's background behind the button rather than
2257
+ * on it. No configuration changes that.
2258
+ */
2259
+ const resolveNoContainer$4 = () => false;
2260
+
2261
+ /**
2262
+ * There is always a root element to carry the class: the button is the
2263
+ * component, so no configuration leaves it without one.
2264
+ */
2265
+ const resolveRendersOwnElement$4 = () => true;
2266
+
2267
+ /**
2268
+ * Seeds the button's own semantics only.
2269
+ *
2270
+ * No style attribute may be seeded here, and the omission is load-bearing
2271
+ * rather than an oversight. Defaults are merged into the column's attribute
2272
+ * bag before styles are computed, so a seeded `border-radius-3-resolutions`
2273
+ * would be present on every button and would override the preset's radius
2274
+ * forever — the preset would appear to have no effect on anything a default
2275
+ * happened to cover. Leaving the bag empty is what lets "unset means preset"
2276
+ * be true.
2277
+ */
2278
+ const defaultAttributeValues$4 = [
2279
+ {
2280
+ name: BUTTON_PRESET_ATTRIBUTE,
2281
+ defaultValues: {
2282
+ all: DEFAULT_BUTTON_PRESET,
2283
+ },
2284
+ },
2285
+ {
2286
+ name: BUTTON_LABEL_ATTRIBUTE,
2287
+ defaultValues: {
2288
+ all: DEFAULT_BUTTON_LABEL,
2289
+ },
2290
+ },
2291
+ ];
2292
+
2293
+ /**
2294
+ * Every setting the button offers, in the order the Modify drawer shows them:
2295
+ * how it looks, what it says, where it goes, then the two refinements most
2296
+ * buttons never need.
2297
+ *
2298
+ * Order is not cosmetic. The drawer renders these top to bottom and the
2299
+ * documentation registry asserts its pages match that sequence, so moving an
2300
+ * entry here moves it in both places at once.
2301
+ */
2302
+ const attributeEditModeConfigs$4 = [
2303
+ {
2304
+ name: BUTTON_PRESET_ATTRIBUTE,
2305
+ heading: 'Button Style',
2306
+ subHeading: 'Variant, tone and size. Sets how the button looks when Column Styles leave it alone — anything set there wins, for 3 different device resolutions',
2307
+ defaultValues: { all: DEFAULT_BUTTON_PRESET },
2308
+ },
2309
+ {
2310
+ name: BUTTON_LABEL_ATTRIBUTE,
2311
+ heading: 'Label',
2312
+ subHeading: 'Text shown on the button. Clear it for an icon-only button',
2313
+ defaultValues: {},
2314
+ },
2315
+ {
2316
+ name: BUTTON_LINK_ATTRIBUTE,
2317
+ heading: 'Link',
2318
+ subHeading: 'Where the button goes. Setting it renders an anchor styled as a button; leaving it empty renders a plain button, for 3 different device resolutions',
2319
+ defaultValues: {},
2320
+ },
2321
+ {
2322
+ name: BUTTON_ICON_ATTRIBUTE,
2323
+ heading: 'Icon',
2324
+ subHeading: 'Optional icon shown beside the label. Search by name or by what the icon depicts (e.g. "launch" finds the rocket), for 3 different device resolutions',
2325
+ defaultValues: {},
2326
+ },
2327
+ {
2328
+ name: BUTTON_ICON_POSITION_ATTRIBUTE,
2329
+ heading: 'Icon Position',
2330
+ subHeading: 'Whether the icon sits before or after the label',
2331
+ defaultValues: { all: DEFAULT_ICON_POSITION },
2332
+ },
2333
+ {
2334
+ name: BUTTON_DISABLED_ATTRIBUTE,
2335
+ heading: 'Disabled',
2336
+ subHeading: 'Renders the button dimmed and non-interactive',
2337
+ defaultValues: {},
2338
+ },
2339
+ ];
2340
+
2341
+ const displayCardEntry$4 = {
2342
+ id: ComponentIdentifiers.rButton,
2343
+ resolveNoContainer: resolveNoContainer$4,
2344
+ resolveRendersOwnElement: resolveRendersOwnElement$4,
2345
+ card: RButtonComponent,
2346
+ defaultAttributeValues: defaultAttributeValues$4,
2347
+ attributeEditModeConfigs: attributeEditModeConfigs$4,
2348
+ };
2349
+
2350
+ /**
2351
+ * The attribute names `r-divider` reads.
2352
+ *
2353
+ * There are two, and the list is short on purpose. A divider is a line, and a
2354
+ * line has four things about it worth setting: how thick it is, whether it is
2355
+ * broken up, what colour it is, and how far across the column it runs. Only
2356
+ * the first two are here.
2357
+ *
2358
+ * Colour and width are missing because they already exist. The line is drawn
2359
+ * in `currentColor`, so Text Color on the column sets it the same way it sets
2360
+ * the colour of an icon, and the element is the column's own box, so Width on
2361
+ * the column shortens it. Offering a second control for either would only
2362
+ * raise the question of which one wins — the same reasoning that keeps colour
2363
+ * and border off `r-button`.
2364
+ *
2365
+ * Both remaining attributes are closed sets rather than free text, which is
2366
+ * what lets them go through the composer's shared select and lets the render
2367
+ * path treat anything unrecognised as unset. A stored value reaches a CSS
2368
+ * property here, so an allowlist is the boundary that keeps it from being
2369
+ * whatever a request happened to write.
2370
+ */
2371
+ const DIVIDER_STYLE_ATTRIBUTE = 'divider-style-3-resolutions';
2372
+ const DIVIDER_THICKNESS_ATTRIBUTE = 'divider-thickness-3-resolutions';
2373
+ const DIVIDER_STYLES = ['solid', 'dashed', 'dotted'];
2374
+ const DEFAULT_DIVIDER_STYLE = 'solid';
2375
+ /**
2376
+ * Weights rather than a free measurement. Anything under a pixel disappears on
2377
+ * a low-density screen and anything much over eight reads as a band rather
2378
+ * than a rule, so the useful range is small enough to offer whole.
2379
+ */
2380
+ const DIVIDER_THICKNESSES = ['1px', '2px', '3px', '4px', '8px'];
2381
+ const DEFAULT_DIVIDER_THICKNESS = '1px';
2382
+ const resolveDividerStyle = (componentAttributes) => {
2383
+ const value = componentAttributes?.[DIVIDER_STYLE_ATTRIBUTE];
2384
+ return DIVIDER_STYLES.includes(value) ? value : DEFAULT_DIVIDER_STYLE;
2385
+ };
2386
+ const resolveDividerThickness = (componentAttributes) => {
2387
+ const value = componentAttributes?.[DIVIDER_THICKNESS_ATTRIBUTE];
2388
+ return DIVIDER_THICKNESSES.includes(value) ? value : DEFAULT_DIVIDER_THICKNESS;
2389
+ };
2390
+
2391
+ /**
2392
+ * The rules that stop a host page's CSS reset having an opinion about the line.
2393
+ *
2394
+ * Every popular reset has already decided what an `<hr>` looks like, and they
2395
+ * disagree: Bootstrap draws one at a quarter opacity, older versions paint it
2396
+ * as a background bar with no border at all, browsers with no reset at all
2397
+ * still paint the inset 3D groove from the days of Netscape. A widget is
2398
+ * dropped into a page whose stylesheet it has never seen, so the only line
2399
+ * that renders the same everywhere is one whose box is stated outright.
2400
+ *
2401
+ * These are inline styles, so they win against anything the host declares —
2402
+ * and column styles are spread over them afterwards, so they lose to anything
2403
+ * the author sets.
2404
+ */
2405
+ const hostResetStyles = {
2406
+ boxSizing: 'border-box',
2407
+ // The line is the top border. The element itself has no content, and the
2408
+ // other three sides are what the browser's own inset groove is made of.
2409
+ height: 0,
2410
+ borderRightStyle: 'none',
2411
+ borderBottomStyle: 'none',
2412
+ borderLeftStyle: 'none',
2413
+ backgroundColor: 'transparent',
2414
+ opacity: 1,
2415
+ // Spacing around a divider belongs to the row's gap and the column's margin,
2416
+ // the same as it does for every other component. Zero here is a decision,
2417
+ // not an omission: the alternative is spacing that depends on which reset
2418
+ // the host page happens to load.
2419
+ marginBlock: 0,
2420
+ // Only visible once the author narrows the column: an inset rule centres
2421
+ // rather than hugging the left edge, which is what a shortened divider is
2422
+ // almost always for.
2423
+ marginInline: 'auto',
2424
+ };
2425
+ /**
2426
+ * The divider's contribution to its own inline style.
2427
+ *
2428
+ * Written entirely in per-side longhands. Column styles arrive as the
2429
+ * all-sides `borderWidth` / `borderStyle` / `borderColor` — different keys,
2430
+ * which is what matters: spread later they are appended after these rather
2431
+ * than replacing them in place, so an author who sets Border on the column
2432
+ * gets a border and not a silent no-op.
2433
+ */
2434
+ function buildDividerStyles(componentAttributes) {
2435
+ return {
2436
+ ...hostResetStyles,
2437
+ borderTopStyle: resolveDividerStyle(componentAttributes),
2438
+ borderTopWidth: resolveDividerThickness(componentAttributes),
2439
+ // Inherited, so Text Color on the column colours the line — and a column
2440
+ // that says nothing gets a rule in the same ink as the words around it.
2441
+ borderTopColor: 'currentColor',
2442
+ };
2443
+ }
2444
+
2445
+ class RDividerComponent {
2446
+ get style() {
2447
+ return {
2448
+ ...buildDividerStyles(this.componentAttributes),
2449
+ ...this.styleProps,
2450
+ };
2451
+ }
2452
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RDividerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2453
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: RDividerComponent, isStandalone: true, selector: "rulecms-r-divider", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `<hr [ngStyle]="style" [class]="className || undefined" />`, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
2454
+ }
2455
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RDividerComponent, decorators: [{
2456
+ type: Component,
2457
+ args: [{
2458
+ selector: 'rulecms-r-divider',
2459
+ standalone: true,
2460
+ imports: [NgStyle],
2461
+ template: `<hr [ngStyle]="style" [class]="className || undefined" />`,
2462
+ host: { style: 'display: contents' },
2463
+ }]
2464
+ }], propDecorators: { componentAttributes: [{
2465
+ type: Input
2466
+ }], styleProps: [{
2467
+ type: Input
2468
+ }], className: [{
2469
+ type: Input
2470
+ }] } });
2471
+
2472
+ /**
2473
+ * Always renders an `<hr>`, a block-level element that holds margin, padding,
2474
+ * width and border on itself, so a wrapper div would only add a redundant
2475
+ * element. No configuration changes that.
2476
+ */
2477
+ const resolveNoContainer$3 = () => false;
2478
+
2479
+ /**
2480
+ * The `<hr>` carries the className. There is always exactly one element and it
2481
+ * is always the root, so there is nothing for this to depend on.
2482
+ */
2483
+ const resolveRendersOwnElement$3 = () => true;
2484
+
2485
+ /**
2486
+ * Seeds the divider's own two settings so the drawer opens on the values a
2487
+ * fresh divider is already drawn with, rather than on two empty pickers.
2488
+ *
2489
+ * Seeding them is safe here in a way it would not be for `r-button`: these
2490
+ * names are the divider's own, so nothing in Column Styles writes to them and
2491
+ * a seeded value can never shadow a style the author set.
2492
+ */
2493
+ const defaultAttributeValues$3 = [
2494
+ {
2495
+ name: DIVIDER_STYLE_ATTRIBUTE,
2496
+ defaultValues: {
2497
+ all: DEFAULT_DIVIDER_STYLE,
2498
+ },
2499
+ },
2500
+ {
2501
+ name: DIVIDER_THICKNESS_ATTRIBUTE,
2502
+ defaultValues: {
2503
+ all: DEFAULT_DIVIDER_THICKNESS,
2504
+ },
2505
+ },
2506
+ ];
2507
+
2508
+ /**
2509
+ * Both settings the divider offers, in the order the Modify drawer shows them.
2510
+ *
2511
+ * Order is not cosmetic. The drawer renders these top to bottom and the
2512
+ * documentation registry asserts its pages match that sequence, so moving an
2513
+ * entry here moves it in both places at once.
2514
+ */
2515
+ const attributeEditModeConfigs$3 = [
2516
+ {
2517
+ name: DIVIDER_STYLE_ATTRIBUTE,
2518
+ heading: 'Line Style',
2519
+ subHeading: 'Whether the rule is unbroken or broken up. Colour comes from Text Color on the column, for 3 different device resolutions',
2520
+ defaultValues: { all: DEFAULT_DIVIDER_STYLE },
2521
+ },
2522
+ {
2523
+ name: DIVIDER_THICKNESS_ATTRIBUTE,
2524
+ heading: 'Thickness',
2525
+ subHeading: 'How heavy the rule is, from a hairline to a band',
2526
+ defaultValues: { all: DEFAULT_DIVIDER_THICKNESS },
2527
+ },
2528
+ ];
2529
+
2530
+ const displayCardEntry$3 = {
2531
+ id: ComponentIdentifiers.rDivider,
2532
+ resolveNoContainer: resolveNoContainer$3,
2533
+ resolveRendersOwnElement: resolveRendersOwnElement$3,
2534
+ card: RDividerComponent,
2535
+ defaultAttributeValues: defaultAttributeValues$3,
2536
+ attributeEditModeConfigs: attributeEditModeConfigs$3,
2537
+ };
2538
+
2539
+ /**
2540
+ * The attribute names `r-embed` reads.
2541
+ *
2542
+ * Four, and the shortest list that still produces a usable embed: what to
2543
+ * frame, what to call it, what shape it is, and whether it loads before the
2544
+ * visitor scrolls to it.
2545
+ *
2546
+ * There is no provider attribute. The provider is derived from the URL, so
2547
+ * pasting is the whole interaction and an author cannot pick YouTube and then
2548
+ * paste a Vimeo link. There is no width, colour, border or radius either —
2549
+ * those are column settings, and a second route to the same CSS property only
2550
+ * raises the question of which one is in force.
2551
+ *
2552
+ * Nothing here can influence the frame's privileges. `sandbox`, `allow` and
2553
+ * `referrerpolicy` are fixed per provider in `embed-providers.ts` and are not
2554
+ * attributes at all, which is also why this component deliberately has no
2555
+ * `pass-through-embed-attributes-*` convention.
2556
+ */
2557
+ const EMBED_URL_ATTRIBUTE = 'embed-url-3-resolutions';
2558
+ const EMBED_TITLE_ATTRIBUTE = 'embed-title-3-resolutions';
2559
+ const EMBED_ASPECT_RATIO_ATTRIBUTE = 'embed-aspect-ratio-3-resolutions';
2560
+ const EMBED_LOADING_ATTRIBUTE = 'embed-loading-3-resolutions';
2561
+ /**
2562
+ * Shapes rather than dimensions. An iframe has no intrinsic size to preserve,
2563
+ * so something has to state the proportion or the box collapses to the
2564
+ * browser's 300×150 default — and a ratio does it without the author having to
2565
+ * work out a height for every screen width.
2566
+ *
2567
+ * `none` is the escape hatch for content that has no natural shape, a booking
2568
+ * form being the obvious one. It is the only value that hands sizing back to
2569
+ * the column's Height setting.
2570
+ */
2571
+ const EMBED_ASPECT_RATIOS = ['16:9', '4:3', '1:1', '9:16', '21:9', 'none'];
2572
+ /**
2573
+ * What an unshaped embed is tall by default. Applied by the component rather
2574
+ * than seeded onto the column, so the column's Height still overrides it and
2575
+ * clearing that returns the embed to a height that works — the same
2576
+ * "unset means the component's default" arrangement `r-button` uses for its
2577
+ * preset. Sized for a Calendly month view, which is the content that needs it.
2578
+ */
2579
+ const EMBED_UNSHAPED_HEIGHT = '700px';
2580
+ const EMBED_LOADING_VALUES = ['lazy', 'eager'];
2581
+ /**
2582
+ * Lazy by default. An embed is a whole third-party document — for YouTube,
2583
+ * comfortably more than the rest of the page around it — and most of them sit
2584
+ * below the fold. `eager` exists for the ones that do not.
2585
+ */
2586
+ const DEFAULT_EMBED_LOADING = 'lazy';
2587
+ /**
2588
+ * The aspect ratio, or `undefined` when the author has not chosen one and the
2589
+ * provider's own default should apply. Anything unrecognised is treated as
2590
+ * unset rather than passed through: this value reaches a CSS property.
2591
+ */
2592
+ const resolveEmbedAspectRatio = (componentAttributes) => {
2593
+ const value = componentAttributes?.[EMBED_ASPECT_RATIO_ATTRIBUTE];
2594
+ return EMBED_ASPECT_RATIOS.includes(value) ? value : undefined;
2595
+ };
2596
+ const resolveEmbedLoading = (componentAttributes) => {
2597
+ const value = componentAttributes?.[EMBED_LOADING_ATTRIBUTE];
2598
+ return value === 'eager' ? 'eager' : DEFAULT_EMBED_LOADING;
2599
+ };
2600
+ /**
2601
+ * An iframe's accessible name. A screen reader reaching an untitled frame
2602
+ * announces it as "frame" and nothing else, so this never returns empty — an
2603
+ * author who clears the field gets the provider's name back rather than a
2604
+ * silent frame.
2605
+ */
2606
+ const resolveEmbedTitle = (componentAttributes, fallback) => {
2607
+ const value = componentAttributes?.[EMBED_TITLE_ATTRIBUTE];
2608
+ if (typeof value !== 'string') {
2609
+ return fallback;
2610
+ }
2611
+ const trimmed = value.trim();
2612
+ return trimmed === '' ? fallback : trimmed;
2613
+ };
2614
+ /** CSS `aspect-ratio` wants `16 / 9`; the attribute stores the familiar `16:9`. */
2615
+ const toCssAspectRatio = (ratio) => ratio === 'none' ? undefined : ratio.replace(':', ' / ');
2616
+
2617
+ /**
2618
+ * `allow-scripts` with `allow-same-origin` looks like it cancels the sandbox,
2619
+ * and for a same-origin frame it would. Here the framed document is
2620
+ * cross-origin, so "same origin" means youtube.com's own origin, not ours: the
2621
+ * frame gets its own storage and cookies and still cannot reach into the
2622
+ * embedding page. Dropping `allow-same-origin` does not harden anything — it
2623
+ * gives the frame an opaque origin, which breaks every one of these providers
2624
+ * outright.
2625
+ */
2626
+ const FRAMED_APP_SANDBOX = ['allow-scripts', 'allow-same-origin'];
2627
+ const YOUTUBE_ID = /^[A-Za-z0-9_-]{11}$/;
2628
+ const VIMEO_ID = /^[0-9]+$/;
2629
+ const VIMEO_PRIVACY_HASH = /^[A-Za-z0-9]+$/;
2630
+ const CALENDLY_SEGMENT = /^[A-Za-z0-9_-]+$/;
2631
+ const GOOGLE_MAPS_PB = /^[A-Za-z0-9!*'().~_-]+$/;
2632
+ /**
2633
+ * Calendly paths that are the scheduling app rather than someone's booking
2634
+ * page. Framing one gets a login screen, so they fail closed here instead.
2635
+ */
2636
+ const CALENDLY_RESERVED_SEGMENTS = new Set([
2637
+ 'app',
2638
+ 'event_types',
2639
+ 'login',
2640
+ 'signup',
2641
+ 'pages',
2642
+ 'api',
2643
+ 'help',
2644
+ 'blog',
2645
+ 'pricing',
2646
+ ]);
2647
+ /**
2648
+ * Calendly's own appearance and prefill options, which authors legitimately
2649
+ * copy along with the booking URL. Anything else in the query is dropped
2650
+ * rather than forwarded — the point of rebuilding the URL is that only things
2651
+ * on a list get through.
2652
+ */
2653
+ const CALENDLY_ALLOWED_PARAMS = [
2654
+ 'hide_gdpr_banner',
2655
+ 'hide_event_type_details',
2656
+ 'hide_landing_page_details',
2657
+ 'primary_color',
2658
+ 'background_color',
2659
+ 'text_color',
2660
+ 'month',
2661
+ 'date',
2662
+ ];
2663
+ /** Lowercase and drop a leading `www.`, so host matching is not a trick question. */
2664
+ const normaliseHost = (hostname) => hostname.toLowerCase().replace(/^www\./, '');
2665
+ const pathSegments = (url) => url.pathname.split('/').filter((segment) => segment !== '');
2666
+ /**
2667
+ * Every YouTube URL an author is likely to have in the clipboard — the watch
2668
+ * page, the share link, a Short, a live stream, and an embed URL someone
2669
+ * copied out of another site's markup — reduced to the video id.
2670
+ */
2671
+ const youTubeVideoId = (url) => {
2672
+ const segments = pathSegments(url);
2673
+ const host = normaliseHost(url.hostname);
2674
+ if (host === 'youtu.be') {
2675
+ return segments[0] ?? null;
2676
+ }
2677
+ if (segments[0] === 'watch') {
2678
+ return url.searchParams.get('v');
2679
+ }
2680
+ if (segments.length === 2 && ['embed', 'shorts', 'live', 'v'].includes(segments[0])) {
2681
+ return segments[1];
2682
+ }
2683
+ return null;
2684
+ };
2685
+ const EMBED_PROVIDERS = [
2686
+ {
2687
+ id: 'youtube',
2688
+ label: 'YouTube',
2689
+ inputHosts: ['youtube.com', 'm.youtube.com', 'youtu.be', 'youtube-nocookie.com'],
2690
+ toEmbedUrl: (url) => {
2691
+ const videoId = youTubeVideoId(url);
2692
+ if (!videoId || !YOUTUBE_ID.test(videoId)) {
2693
+ return null;
2694
+ }
2695
+ // youtube-nocookie.com is the same player without the tracking cookies
2696
+ // it would otherwise set on a visitor who never pressed play. Nothing
2697
+ // about the embed needs them, and a widget that quietly adds third-party
2698
+ // cookies to a customer's site is a consent problem we would be handing
2699
+ // them.
2700
+ return `https://www.youtube-nocookie.com/embed/${videoId}`;
2701
+ },
2702
+ sandbox: [...FRAMED_APP_SANDBOX, 'allow-presentation', 'allow-popups', 'allow-popups-to-escape-sandbox'],
2703
+ allow: ['accelerometer', 'clipboard-write', 'encrypted-media', 'gyroscope', 'picture-in-picture'],
2704
+ allowFullScreen: true,
2705
+ defaultAspectRatio: '16:9',
2706
+ defaultTitle: 'YouTube video player',
2707
+ },
2708
+ {
2709
+ id: 'vimeo',
2710
+ label: 'Vimeo',
2711
+ inputHosts: ['vimeo.com', 'player.vimeo.com'],
2712
+ toEmbedUrl: (url) => {
2713
+ const segments = pathSegments(url);
2714
+ // player.vimeo.com/video/123, vimeo.com/123, vimeo.com/channels/staffpicks/123
2715
+ const withoutPrefix = segments[0] === 'video' ? segments.slice(1) : segments;
2716
+ const numeric = withoutPrefix.filter((segment) => VIMEO_ID.test(segment));
2717
+ const videoId = numeric[0];
2718
+ if (!videoId) {
2719
+ return null;
2720
+ }
2721
+ // An unlisted video carries a hash, either as a trailing path segment
2722
+ // (vimeo.com/123/abc123) or as ?h=. Without it the player refuses to
2723
+ // load, so it is the one piece of the input besides the id that is
2724
+ // carried across.
2725
+ const trailing = withoutPrefix[withoutPrefix.indexOf(videoId) + 1];
2726
+ const hash = url.searchParams.get('h') ?? trailing;
2727
+ const privacyHash = hash && VIMEO_PRIVACY_HASH.test(hash) ? hash : null;
2728
+ return privacyHash
2729
+ ? `https://player.vimeo.com/video/${videoId}?h=${privacyHash}`
2730
+ : `https://player.vimeo.com/video/${videoId}`;
2731
+ },
2732
+ sandbox: [...FRAMED_APP_SANDBOX, 'allow-presentation', 'allow-popups', 'allow-popups-to-escape-sandbox'],
2733
+ allow: ['autoplay', 'clipboard-write', 'encrypted-media', 'picture-in-picture'],
2734
+ allowFullScreen: true,
2735
+ defaultAspectRatio: '16:9',
2736
+ defaultTitle: 'Vimeo video player',
2737
+ },
2738
+ {
2739
+ id: 'google-maps',
2740
+ label: 'Google Maps',
2741
+ inputHosts: ['google.com', 'maps.google.com'],
2742
+ toEmbedUrl: (url) => {
2743
+ // Only the URL from "Share → Embed a map" is accepted, because it is the
2744
+ // only one that can be framed at all: Google serves an ordinary maps
2745
+ // link with X-Frame-Options and the visitor gets a blank box. Failing
2746
+ // here means the author is told to copy the other URL, rather than
2747
+ // shipping a page with a hole in it.
2748
+ if (pathSegments(url).join('/') !== 'maps/embed') {
2749
+ return null;
2750
+ }
2751
+ const pb = url.searchParams.get('pb');
2752
+ // `pb` is Google's own opaque camera-and-place blob. It is carried
2753
+ // across verbatim rather than re-encoded, because it is the one input
2754
+ // this file cannot regenerate from an id — so instead it is held to a
2755
+ // charset with no way out of a query string. Everything that could end
2756
+ // one, or start an attribute, is absent from this list.
2757
+ if (!pb || !GOOGLE_MAPS_PB.test(pb)) {
2758
+ return null;
2759
+ }
2760
+ return `https://www.google.com/maps/embed?pb=${pb}`;
2761
+ },
2762
+ sandbox: [...FRAMED_APP_SANDBOX, 'allow-popups', 'allow-popups-to-escape-sandbox'],
2763
+ allow: [],
2764
+ allowFullScreen: true,
2765
+ defaultAspectRatio: '4:3',
2766
+ defaultTitle: 'Map',
2767
+ },
2768
+ {
2769
+ id: 'calendly',
2770
+ label: 'Calendly',
2771
+ inputHosts: ['calendly.com'],
2772
+ toEmbedUrl: (url) => {
2773
+ const segments = pathSegments(url);
2774
+ if (segments.length === 0 || segments.length > 2) {
2775
+ return null;
2776
+ }
2777
+ if (!segments.every((segment) => CALENDLY_SEGMENT.test(segment))) {
2778
+ return null;
2779
+ }
2780
+ if (CALENDLY_RESERVED_SEGMENTS.has(segments[0].toLowerCase())) {
2781
+ return null;
2782
+ }
2783
+ const options = new URLSearchParams();
2784
+ for (const parameter of CALENDLY_ALLOWED_PARAMS) {
2785
+ const value = url.searchParams.get(parameter);
2786
+ if (value !== null) {
2787
+ options.set(parameter, value);
2788
+ }
2789
+ }
2790
+ const query = options.toString();
2791
+ const path = segments.join('/');
2792
+ return query ? `https://calendly.com/${path}?${query}` : `https://calendly.com/${path}`;
2793
+ },
2794
+ sandbox: [...FRAMED_APP_SANDBOX, 'allow-forms', 'allow-popups', 'allow-popups-to-escape-sandbox'],
2795
+ allow: [],
2796
+ allowFullScreen: false,
2797
+ // A booking widget is a form of unpredictable length, not a picture with a
2798
+ // shape. It gets a height instead of a ratio; see `embed-attributes.ts`.
2799
+ defaultAspectRatio: 'none',
2800
+ defaultTitle: 'Booking calendar',
2801
+ },
2802
+ ];
2803
+ /**
2804
+ * The single place an embed URL is checked against the allowlist.
2805
+ *
2806
+ * Every caller — the component, both container resolvers, and the composer's
2807
+ * URL editor through the package's public exports — comes through here rather
2808
+ * than reading `EMBED_PROVIDERS` and matching for itself. That is what keeps
2809
+ * a per-app allowlist a change to one function's argument later on, instead of
2810
+ * a hunt for call sites: see
2811
+ * `rulecms/__docs__/__TODO__/__COMPONENTS__/TODO_PLATFORM_EMBED_ALLOWLIST_PER_APP.md`.
2812
+ *
2813
+ * Returns `null` for anything it cannot vouch for, which is the fail-closed
2814
+ * behaviour the component depends on. It never throws: a malformed URL in a
2815
+ * stored widget must not take down the page it was published to.
2816
+ */
2817
+ function resolveEmbedUrl(value, providers = EMBED_PROVIDERS) {
2818
+ if (typeof value !== 'string' || value.trim() === '') {
2819
+ return null;
2820
+ }
2821
+ let url;
2822
+ try {
2823
+ url = new URL(value.trim());
2824
+ }
2825
+ catch {
2826
+ return null;
2827
+ }
2828
+ // https only. Every provider here serves it, and requiring it disposes of
2829
+ // javascript:, data:, blob: and file: as a class rather than one at a time.
2830
+ if (url.protocol !== 'https:') {
2831
+ return null;
2832
+ }
2833
+ // `https://youtube.com@evil.example/` parses with `evil.example` as the host
2834
+ // and the rest as credentials. The host check below is not fooled by it, but
2835
+ // a URL carrying credentials at all is not one an author pasted from a
2836
+ // provider, so it is refused outright.
2837
+ if (url.username !== '' || url.password !== '') {
2838
+ return null;
2839
+ }
2840
+ const host = normaliseHost(url.hostname);
2841
+ const provider = providers.find((candidate) => candidate.inputHosts.includes(host));
2842
+ if (!provider) {
2843
+ return null;
2844
+ }
2845
+ const src = provider.toEmbedUrl(url);
2846
+ return src ? { provider, src } : null;
2847
+ }
2848
+ /** Whether a URL would render. The composer's editor uses this for its hint. */
2849
+ const isAllowedEmbedUrl = (value, providers = EMBED_PROVIDERS) => resolveEmbedUrl(value, providers) !== null;
2850
+
2851
+ /**
2852
+ * The single resolution path, shared by `REmbed` and by both container
2853
+ * resolvers, in the way `r-text` shares `resolveEnclosingTag`.
2854
+ *
2855
+ * Sharing it is not tidiness. `resolveNoContainer` and
2856
+ * `resolveRendersOwnElement` both answer questions about an element that only
2857
+ * exists when a URL passed the allowlist, so if they decided that
2858
+ * independently of the component, a rejected URL would have the renderer
2859
+ * either handing column styles to an element that was never rendered or
2860
+ * wrapping nothing in a styled div. Neither would look wrong in the drawer, in
2861
+ * Redux, or in the stored JSON.
2862
+ *
2863
+ * `null` means nothing will be rendered: no URL, or one the allowlist refused.
2864
+ */
2865
+ function resolveEmbed(componentAttributes) {
2866
+ const resolved = resolveEmbedUrl(componentAttributes?.[EMBED_URL_ATTRIBUTE]);
2867
+ if (!resolved) {
2868
+ return null;
2869
+ }
2870
+ const { provider, src } = resolved;
2871
+ return {
2872
+ provider,
2873
+ src,
2874
+ title: resolveEmbedTitle(componentAttributes, provider.defaultTitle),
2875
+ // An author's choice wins; otherwise the provider's own shape, which is
2876
+ // what makes a pasted map 4:3 and a pasted video 16:9 without anyone
2877
+ // having to know that is what they wanted.
2878
+ aspectRatio: resolveEmbedAspectRatio(componentAttributes) ?? provider.defaultAspectRatio,
2879
+ loading: resolveEmbedLoading(componentAttributes),
2880
+ };
2881
+ }
2882
+ /**
2883
+ * The frame's own appearance, before column styles are laid over it.
2884
+ *
2885
+ * `display: block` removes the descender gap an inline replaced element leaves
2886
+ * under itself, and the zero border removes the 2px inset ridge every browser
2887
+ * still draws around an iframe by default.
2888
+ */
2889
+ const buildEmbedStyles = (aspectRatio) => ({
2890
+ display: 'block',
2891
+ width: '100%',
2892
+ border: 0,
2893
+ ...(aspectRatio === 'none'
2894
+ ? { height: EMBED_UNSHAPED_HEIGHT }
2895
+ : { aspectRatio: toCssAspectRatio(aspectRatio) }),
2896
+ });
2897
+
2898
+ class REmbedComponent {
2899
+ constructor(sanitizer) {
2900
+ this.sanitizer = sanitizer;
2901
+ }
2902
+ get embed() {
2903
+ return resolveEmbed(this.componentAttributes);
2904
+ }
2905
+ get safeSrc() {
2906
+ const embed = this.embed;
2907
+ return embed ? this.sanitizer.bypassSecurityTrustResourceUrl(embed.src) : null;
2908
+ }
2909
+ get style() {
2910
+ const embed = this.embed;
2911
+ if (!embed) {
2912
+ return this.styleProps;
2913
+ }
2914
+ return { ...buildEmbedStyles(embed.aspectRatio), ...this.styleProps };
2915
+ }
2916
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: REmbedComponent, deps: [{ token: i1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Component }); }
2917
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: REmbedComponent, isStandalone: true, selector: "rulecms-r-embed", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
2918
+ @if (embed) {
2919
+ <iframe
2920
+ [src]="safeSrc"
2921
+ [title]="embed.title"
2922
+ [ngStyle]="style"
2923
+ [attr.sandbox]="embed.provider.sandbox.join(' ')"
2924
+ [attr.allow]="embed.provider.allow.length > 0 ? embed.provider.allow.join('; ') : null"
2925
+ [attr.allowfullscreen]="embed.provider.allowFullScreen ? true : null"
2926
+ referrerpolicy="strict-origin-when-cross-origin"
2927
+ [attr.loading]="embed.loading"
2928
+ [class]="className || undefined"
2929
+ ></iframe>
2930
+ }
2931
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
2932
+ }
2933
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: REmbedComponent, decorators: [{
2934
+ type: Component,
2935
+ args: [{
2936
+ selector: 'rulecms-r-embed',
2937
+ standalone: true,
2938
+ imports: [NgStyle],
2939
+ template: `
2940
+ @if (embed) {
2941
+ <iframe
2942
+ [src]="safeSrc"
2943
+ [title]="embed.title"
2944
+ [ngStyle]="style"
2945
+ [attr.sandbox]="embed.provider.sandbox.join(' ')"
2946
+ [attr.allow]="embed.provider.allow.length > 0 ? embed.provider.allow.join('; ') : null"
2947
+ [attr.allowfullscreen]="embed.provider.allowFullScreen ? true : null"
2948
+ referrerpolicy="strict-origin-when-cross-origin"
2949
+ [attr.loading]="embed.loading"
2950
+ [class]="className || undefined"
2951
+ ></iframe>
2952
+ }
2953
+ `,
2954
+ host: { style: 'display: contents' },
2955
+ }]
2956
+ }], ctorParameters: () => [{ type: i1.DomSanitizer }], propDecorators: { componentAttributes: [{
2957
+ type: Input
2958
+ }], styleProps: [{
2959
+ type: Input
2960
+ }], className: [{
2961
+ type: Input
2962
+ }] } });
2963
+
2964
+ /**
2965
+ * `false` whenever an iframe is rendered: it is a replaced element that holds
2966
+ * margin, padding, width and height on itself, so a wrapper div would only add
2967
+ * an element that does nothing.
2968
+ *
2969
+ * `true` when the URL is missing or the allowlist refused it, because then
2970
+ * there is no element at all — the same answer `r-text` gives for bare text.
2971
+ * Deciding this from `resolveEmbed`, rather than from the fact that this
2972
+ * component "is an iframe component", is what keeps the two in step.
2973
+ */
2974
+ const resolveNoContainer$2 = ({ componentAttributes }) => resolveEmbed(componentAttributes) === null;
2975
+
2976
+ /**
2977
+ * The iframe carries the className — when there is one. A rejected or unset
2978
+ * URL renders nothing, and a class handed to a component that renders nothing
2979
+ * disappears without trace, so this answers from the same resolution the
2980
+ * component uses.
2981
+ */
2982
+ const resolveRendersOwnElement$2 = ({ componentAttributes, }) => resolveEmbed(componentAttributes) !== null;
2983
+
2984
+ /**
2985
+ * Nothing is seeded, including the URL.
2986
+ *
2987
+ * A freshly dropped embed renders nothing until an author pastes something,
2988
+ * which is the honest result: the alternative is shipping a placeholder video
2989
+ * that a customer's page would publish if anyone forgot to replace it. The
2990
+ * palette card and the empty drawer say what to do instead.
2991
+ *
2992
+ * Aspect ratio is left unset on purpose too, so it resolves from the provider
2993
+ * — a map is 4:3 and a video is 16:9 without either being written down here.
2994
+ */
2995
+ const defaultAttributeValues$2 = [
2996
+ {
2997
+ name: 'embed-url-3-resolutions',
2998
+ defaultValues: {},
2999
+ },
3000
+ {
3001
+ name: 'embed-title-3-resolutions',
3002
+ defaultValues: {},
3003
+ },
3004
+ {
3005
+ name: 'embed-aspect-ratio-3-resolutions',
3006
+ defaultValues: {},
3007
+ },
3008
+ {
3009
+ name: 'embed-loading-3-resolutions',
3010
+ defaultValues: {},
3011
+ },
3012
+ ];
3013
+
3014
+ /**
3015
+ * Drawer order, which is also the order the documentation pages are listed in:
3016
+ * the URL first because nothing renders without it, then the three settings
3017
+ * that only matter once something does.
3018
+ */
3019
+ const attributeEditModeConfigs$2 = [
3020
+ {
3021
+ name: 'embed-url-3-resolutions',
3022
+ heading: 'Embed URL',
3023
+ subHeading: 'Paste the address of a YouTube or Vimeo video, a Google Maps "Embed a map" link, or a Calendly booking page, for 3 different device resolutions',
3024
+ defaultValues: {},
3025
+ },
3026
+ {
3027
+ name: 'embed-title-3-resolutions',
3028
+ heading: 'Embed Title',
3029
+ subHeading: 'Describe what is being embedded, for screen readers; defaults to the provider name, for 3 different device resolutions',
3030
+ defaultValues: {},
3031
+ },
3032
+ {
3033
+ name: 'embed-aspect-ratio-3-resolutions',
3034
+ heading: 'Embed Aspect Ratio',
3035
+ subHeading: 'The shape of the embed — "16:9" for video, "4:3" for maps, or "none" to set the height on the column instead, for 3 different device resolutions',
3036
+ defaultValues: {},
3037
+ },
3038
+ {
3039
+ name: 'embed-loading-3-resolutions',
3040
+ heading: 'Embed Loading',
3041
+ subHeading: 'Whether the embed loads with the page ("eager") or waits until the visitor scrolls near it ("lazy", the default), for 3 different device resolutions',
3042
+ defaultValues: {},
3043
+ },
3044
+ ];
3045
+
3046
+ const displayCardEntry$2 = {
3047
+ id: ComponentIdentifiers.rEmbed,
3048
+ resolveNoContainer: resolveNoContainer$2,
3049
+ resolveRendersOwnElement: resolveRendersOwnElement$2,
3050
+ card: REmbedComponent,
3051
+ defaultAttributeValues: defaultAttributeValues$2,
3052
+ attributeEditModeConfigs: attributeEditModeConfigs$2,
3053
+ };
3054
+
3055
+ /**
3056
+ * The attribute names `r-list` reads.
3057
+ *
3058
+ * Every one is list-specific, including the links, which are conceptually the
3059
+ * same numbered list `r-text` already offers. Sharing that attribute name
3060
+ * would have saved one line of editor wiring and cost the documentation
3061
+ * contract: RuleCMS publishes one page per component-scope attribute and
3062
+ * requires exactly one owning component, so a shared name has no single page
3063
+ * to link to from the drawer. Reuse happens a level down instead — the
3064
+ * composer resolves an attribute to an *editor* by name, and `list-links`
3065
+ * points at the editor `links-3-resolutions` already resolves to.
3066
+ *
3067
+ * Nothing here duplicates a column style attribute. Spacing, colour and
3068
+ * typography come from Column Styles on the root element and inherit into
3069
+ * the items; the attributes below are only what is specific to a list —
3070
+ * which element it is, what the markers look like, and where numbering
3071
+ * starts. Kind and marker are closed sets rather than free text, which is
3072
+ * what lets them go through the composer's shared select and lets the render
3073
+ * path treat anything unrecognised as unset.
3074
+ */
3075
+ const LIST_ITEMS_ATTRIBUTE = 'list-items-3-resolutions';
3076
+ const LIST_KIND_ATTRIBUTE = 'list-kind-3-resolutions';
3077
+ const LIST_MARKER_ATTRIBUTE = 'list-marker-3-resolutions';
3078
+ const LIST_START_ATTRIBUTE = 'list-start-3-resolutions';
3079
+ const LIST_REVERSED_ATTRIBUTE = 'list-reversed-3-resolutions';
3080
+ const LIST_LINKS_ATTRIBUTE = 'list-links-3-resolutions';
3081
+ /** What a freshly dropped list shows before the author types anything. */
3082
+ const DEFAULT_LIST_ITEMS = 'First item\nSecond item\nThird item';
3083
+ const LIST_KINDS = ['unordered', 'ordered'];
3084
+ const DEFAULT_LIST_KIND = 'unordered';
3085
+ /**
3086
+ * `list-style-type` values worth offering. Every entry is valid CSS on both
3087
+ * `<ul>` and `<ol>`, so the marker survives the author switching kind rather
3088
+ * than resetting. Unset means the browser default — disc for unordered,
3089
+ * decimal for ordered — which is why there is no default constant here: not
3090
+ * setting the property at all is the default.
3091
+ */
3092
+ const LIST_MARKERS = [
3093
+ 'disc',
3094
+ 'circle',
3095
+ 'square',
3096
+ 'decimal',
3097
+ 'lower-alpha',
3098
+ 'upper-alpha',
3099
+ 'lower-roman',
3100
+ 'upper-roman',
3101
+ 'none',
3102
+ ];
3103
+ const resolveListKind = (componentAttributes) => {
3104
+ const value = componentAttributes?.[LIST_KIND_ATTRIBUTE];
3105
+ return LIST_KINDS.includes(value) ? value : DEFAULT_LIST_KIND;
3106
+ };
3107
+ /**
3108
+ * A stored value reaches a CSS property here, so the allowlist is the
3109
+ * boundary that keeps it from being whatever a request happened to write.
3110
+ * Unrecognised values mean "unset" and the browser default applies.
3111
+ */
3112
+ const resolveListMarker = (componentAttributes) => {
3113
+ const value = componentAttributes?.[LIST_MARKER_ATTRIBUTE];
3114
+ return LIST_MARKERS.includes(value) ? value : undefined;
3115
+ };
3116
+ /**
3117
+ * One item per line of the authored text, blank lines dropped. An unset
3118
+ * attribute falls back to the placeholder items, but text the author
3119
+ * deliberately cleared stays cleared — an empty list element still carries
3120
+ * the column's styles and stays selectable in the composer.
3121
+ */
3122
+ const resolveListItems = (componentAttributes) => {
3123
+ const value = componentAttributes?.[LIST_ITEMS_ATTRIBUTE];
3124
+ const text = value === undefined || value === null
3125
+ ? DEFAULT_LIST_ITEMS
3126
+ : typeof value === 'string'
3127
+ ? value
3128
+ : String(value);
3129
+ return text
3130
+ .split(/\r?\n/)
3131
+ .map((line) => line.trim())
3132
+ .filter((line) => line !== '');
3133
+ };
3134
+ /**
3135
+ * HTML's `start` takes any integer, including negative ones. The editor
3136
+ * stores a number, but a hand-written or migrated config may carry a numeric
3137
+ * string; both mean the same thing here. Anything else is unset, and only an
3138
+ * ordered list ever reads it.
3139
+ */
3140
+ const resolveListStart = (componentAttributes) => {
3141
+ const value = componentAttributes?.[LIST_START_ATTRIBUTE];
3142
+ if (typeof value === 'number' && Number.isInteger(value)) {
3143
+ return value;
3144
+ }
3145
+ if (typeof value === 'string' && value.trim() !== '') {
3146
+ const parsed = Number(value.trim());
3147
+ if (Number.isInteger(parsed)) {
3148
+ return parsed;
3149
+ }
3150
+ }
3151
+ return undefined;
3152
+ };
3153
+ /**
3154
+ * Select editors in the composer store `'true'` / `'false'` as strings, while
3155
+ * a hand-written or migrated config may carry a real boolean. Both mean the
3156
+ * same thing here. Only an ordered list ever reads it.
3157
+ */
3158
+ const resolveListReversed = (componentAttributes) => {
3159
+ const value = componentAttributes?.[LIST_REVERSED_ATTRIBUTE];
3160
+ if (typeof value === 'boolean') {
3161
+ return value;
3162
+ }
3163
+ return typeof value === 'string' && value.trim().toLowerCase() === 'true';
3164
+ };
3165
+
3166
+ class RListComponent {
3167
+ get marker() {
3168
+ return resolveListMarker(this.componentAttributes);
3169
+ }
3170
+ get links() {
3171
+ return this.componentAttributes?.[LIST_LINKS_ATTRIBUTE];
3172
+ }
3173
+ get style() {
3174
+ return {
3175
+ ...(this.marker !== undefined ? { listStyleType: this.marker } : {}),
3176
+ ...this.styleProps,
3177
+ };
3178
+ }
3179
+ get items() {
3180
+ return resolveListItems(this.componentAttributes);
3181
+ }
3182
+ get kind() {
3183
+ return resolveListKind(this.componentAttributes);
3184
+ }
3185
+ get start() {
3186
+ return resolveListStart(this.componentAttributes);
3187
+ }
3188
+ get reversed() {
3189
+ return resolveListReversed(this.componentAttributes);
3190
+ }
3191
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3192
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RListComponent, isStandalone: true, selector: "rulecms-r-list", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
3193
+ @if (kind === 'ordered') {
3194
+ <ol [ngStyle]="style" [class]="className || undefined" [attr.start]="start" [attr.reversed]="reversed ? true : null">
3195
+ @for (item of items; track $index) {
3196
+ <li>
3197
+ <rulecms-inline-markup [text]="item" [links]="links" />
3198
+ </li>
3199
+ }
3200
+ </ol>
3201
+ } @else {
3202
+ <ul [ngStyle]="style" [class]="className || undefined">
3203
+ @for (item of items; track $index) {
3204
+ <li>
3205
+ <rulecms-inline-markup [text]="item" [links]="links" />
3206
+ </li>
3207
+ }
3208
+ </ul>
3209
+ }
3210
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: InlineMarkupComponent, selector: "rulecms-inline-markup", inputs: ["text", "links"] }] }); }
3211
+ }
3212
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RListComponent, decorators: [{
3213
+ type: Component,
3214
+ args: [{
3215
+ selector: 'rulecms-r-list',
3216
+ standalone: true,
3217
+ imports: [NgStyle, InlineMarkupComponent],
3218
+ template: `
3219
+ @if (kind === 'ordered') {
3220
+ <ol [ngStyle]="style" [class]="className || undefined" [attr.start]="start" [attr.reversed]="reversed ? true : null">
3221
+ @for (item of items; track $index) {
3222
+ <li>
3223
+ <rulecms-inline-markup [text]="item" [links]="links" />
3224
+ </li>
3225
+ }
3226
+ </ol>
3227
+ } @else {
3228
+ <ul [ngStyle]="style" [class]="className || undefined">
3229
+ @for (item of items; track $index) {
3230
+ <li>
3231
+ <rulecms-inline-markup [text]="item" [links]="links" />
3232
+ </li>
3233
+ }
3234
+ </ul>
3235
+ }
3236
+ `,
3237
+ host: { style: 'display: contents' },
3238
+ }]
3239
+ }], propDecorators: { componentAttributes: [{
3240
+ type: Input
3241
+ }], styleProps: [{
3242
+ type: Input
3243
+ }], className: [{
3244
+ type: Input
3245
+ }] } });
3246
+
3247
+ /**
3248
+ * RList always renders a `<ul>` or `<ol>` — both block-level elements that
3249
+ * hold margin, padding, width and border as authored — and puts styleProps on
3250
+ * that element itself. No configuration changes the answer, so no wrapper
3251
+ * div, ever.
3252
+ */
3253
+ const resolveNoContainer$1 = () => false;
3254
+
3255
+ /**
3256
+ * RList's root is always a real element — `<ul>` or `<ol>` — so it can always
3257
+ * carry the column's className, and the component applies the one it
3258
+ * receives.
3259
+ */
3260
+ const resolveRendersOwnElement$1 = () => true;
3261
+
3262
+ const defaultAttributeValues$1 = [
3263
+ {
3264
+ name: LIST_ITEMS_ATTRIBUTE,
3265
+ defaultValues: {
3266
+ all: DEFAULT_LIST_ITEMS,
3267
+ },
3268
+ },
3269
+ {
3270
+ name: LIST_KIND_ATTRIBUTE,
3271
+ defaultValues: {
3272
+ all: DEFAULT_LIST_KIND,
3273
+ },
3274
+ },
3275
+ // Marker, start and reversed default to unset on purpose: the browser
3276
+ // already renders disc for unordered and 1, 2, 3 for ordered, and an
3277
+ // attribute that is not set is one the render path does not emit.
3278
+ {
3279
+ name: LIST_MARKER_ATTRIBUTE,
3280
+ defaultValues: {},
3281
+ },
3282
+ {
3283
+ name: LIST_START_ATTRIBUTE,
3284
+ defaultValues: {},
3285
+ },
3286
+ {
3287
+ name: LIST_REVERSED_ATTRIBUTE,
3288
+ defaultValues: {},
3289
+ },
3290
+ {
3291
+ name: LIST_LINKS_ATTRIBUTE,
3292
+ defaultValues: {},
3293
+ },
3294
+ ];
3295
+
3296
+ /**
3297
+ * Every setting the list offers, in the order the Modify drawer shows them.
3298
+ *
3299
+ * Order is not cosmetic. The drawer renders these top to bottom and the
3300
+ * documentation registry asserts its pages match that sequence, so moving an
3301
+ * entry here moves it in both places at once.
3302
+ */
3303
+ const attributeEditModeConfigs$1 = [
3304
+ {
3305
+ name: LIST_ITEMS_ATTRIBUTE,
3306
+ heading: 'List Items',
3307
+ subHeading: 'The items, one per line — each line becomes its own entry in the list. Blank lines are skipped, for 3 different device resolutions',
3308
+ defaultValues: {},
3309
+ },
3310
+ {
3311
+ name: LIST_KIND_ATTRIBUTE,
3312
+ heading: 'List Kind',
3313
+ subHeading: 'Bulleted (unordered) or numbered (ordered). Numbered lists can also set where counting starts and whether it counts down, for 3 different device resolutions',
3314
+ defaultValues: { all: DEFAULT_LIST_KIND },
3315
+ },
3316
+ {
3317
+ name: LIST_MARKER_ATTRIBUTE,
3318
+ heading: 'Marker Style',
3319
+ subHeading: 'What sits in front of each item: a disc, circle or square for bullets; numbers, letters or Roman numerals for numbering; or none to hide the markers. Unset uses the browser default for the list kind, for 3 different device resolutions',
3320
+ defaultValues: {},
3321
+ },
3322
+ {
3323
+ name: LIST_START_ATTRIBUTE,
3324
+ heading: 'Numbering Start',
3325
+ subHeading: 'The number the first item counts from. Only a numbered list reads this; leave unset to count from 1, for 3 different device resolutions',
3326
+ defaultValues: {},
3327
+ },
3328
+ {
3329
+ name: LIST_REVERSED_ATTRIBUTE,
3330
+ heading: 'Reversed Numbering',
3331
+ subHeading: 'Whether a numbered list counts down instead of up, e.g. a top-10 countdown. Only a numbered list reads this, for 3 different device resolutions',
3332
+ defaultValues: {},
3333
+ },
3334
+ {
3335
+ name: LIST_LINKS_ATTRIBUTE,
3336
+ heading: 'Links',
3337
+ subHeading: 'Links the items can point at. To link part of an item, wrap those words in {1|linked text}, where the number is the position of the entry in this list counting from 1; clearing an entry leaves its position in place so the other numbers keep pointing where they did, for 3 different device resolutions',
3338
+ defaultValues: {},
3339
+ },
3340
+ ];
3341
+
3342
+ const displayCardEntry$1 = {
3343
+ id: ComponentIdentifiers.rList,
3344
+ resolveNoContainer: resolveNoContainer$1,
3345
+ resolveRendersOwnElement: resolveRendersOwnElement$1,
3346
+ card: RListComponent,
3347
+ defaultAttributeValues: defaultAttributeValues$1,
3348
+ attributeEditModeConfigs: attributeEditModeConfigs$1,
3349
+ };
3350
+
3351
+ /**
3352
+ * The attribute names `r-accordion` reads.
3353
+ *
3354
+ * The component is a disclosure and nothing more specific than that: a summary
3355
+ * a reader clicks and the detail it expands to. An FAQ is one thing to build
3356
+ * out of it, a shipping policy or a "show more" under a truncated bio are
3357
+ * others, and the settings are named after the two halves of the element —
3358
+ * `<summary>` and the detail inside `<details>` — rather than after any one of
3359
+ * those uses.
3360
+ *
3361
+ * One accordion is one disclosure, rather than a component holding N
3362
+ * collapsible sections. A repeating-group attribute would need an editor this
3363
+ * library does not have, and several sections in one component leave the
3364
+ * nested-content slot with no single detail to belong to (see
3365
+ * `rulecms/__docs__/__ARCHITECTURE__/ACCORDION_COMPONENT_ARCHITECTURE.md`).
3366
+ * Several of these in a column is the multi-section case, which is also how an
3367
+ * author reorders one.
3368
+ *
3369
+ * Every name is accordion-specific, including the links, which are the same
3370
+ * numbered list `r-text` already offers. Sharing that attribute name would
3371
+ * have saved a line of editor wiring and cost the documentation contract:
3372
+ * RuleCMS publishes one page per component-scope attribute and requires
3373
+ * exactly one owning component. Reuse happens a level down, where the composer
3374
+ * resolves an attribute to an *editor* by name — the same arrangement `r-list`
3375
+ * uses.
3376
+ *
3377
+ * Nothing here duplicates a column style attribute. Spacing, colour and
3378
+ * typography come from Column Styles on the `<details>` element and inherit
3379
+ * into the summary and the detail; the attributes below are only what is
3380
+ * specific to a disclosure — what it says closed, what it says open, whether
3381
+ * it starts open, and whether the summary also belongs in the page outline.
3382
+ */
3383
+ const ACCORDION_SUMMARY_ATTRIBUTE = 'accordion-summary-3-resolutions';
3384
+ const ACCORDION_DETAIL_ATTRIBUTE = 'accordion-detail-3-resolutions';
3385
+ const ACCORDION_LINKS_ATTRIBUTE = 'accordion-links-3-resolutions';
3386
+ const ACCORDION_OPEN_ATTRIBUTE = 'accordion-open-3-resolutions';
3387
+ const ACCORDION_HEADING_LEVEL_ATTRIBUTE = 'accordion-heading-level-3-resolutions';
3388
+ /** What a freshly dropped accordion shows before the author types anything. */
3389
+ const DEFAULT_ACCORDION_SUMMARY = 'Summary';
3390
+ const DEFAULT_ACCORDION_DETAIL = 'The detail this summary expands to. Each line becomes its own paragraph.';
3391
+ /**
3392
+ * Heading levels the summary can be given, plus `none` for the common case.
3393
+ *
3394
+ * `h1` is deliberately absent: the page already has one, and ten disclosures
3395
+ * down the page would otherwise offer ten ways to compete with it. Levels
3396
+ * start at `h2` because a summary sits under whatever heading introduces the
3397
+ * section it is in.
3398
+ *
3399
+ * `none` is a real option rather than an unset value so the picker always has
3400
+ * a definite selection — there is no way to clear a select back to nothing —
3401
+ * and it is the default because a disclosure is not automatically part of the
3402
+ * document outline.
3403
+ */
3404
+ const ACCORDION_HEADING_LEVELS = ['none', 'h2', 'h3', 'h4', 'h5', 'h6'];
3405
+ const DEFAULT_ACCORDION_HEADING_LEVEL = 'none';
3406
+ /**
3407
+ * The summary, on one line. An unset attribute falls back to the placeholder,
3408
+ * but text the author deliberately cleared stays cleared: an empty `<summary>`
3409
+ * still draws its marker and still toggles, and the composer needs the element
3410
+ * to stay selectable on the canvas. Same distinction `r-list` draws for its
3411
+ * items and `r-button` for its label.
3412
+ *
3413
+ * Authored newlines collapse to single spaces. The editor behind this
3414
+ * attribute is the shared multi-line text field, and `<summary>` is a single
3415
+ * line of prose — HTML would collapse the whitespace anyway, so the resolved
3416
+ * value says so rather than leaving it to the browser.
3417
+ *
3418
+ * `{n|…}` markers contribute their inner text and nothing else. The summary
3419
+ * takes no links — inside a `<summary>` a click both follows the anchor and
3420
+ * toggles the disclosure — but the editor behind it is the same field that
3421
+ * offers the marker grammar for the detail, so an author will type one here.
3422
+ * Degrading it to prose is what the rest of the library does with a marker it
3423
+ * cannot resolve; leaving the braces on screen is the one outcome nobody wants.
3424
+ */
3425
+ const resolveAccordionSummary = (componentAttributes) => {
3426
+ const value = componentAttributes?.[ACCORDION_SUMMARY_ATTRIBUTE];
3427
+ const text = value === undefined || value === null
3428
+ ? DEFAULT_ACCORDION_SUMMARY
3429
+ : typeof value === 'string'
3430
+ ? value
3431
+ : String(value);
3432
+ return parseInlineLinkMarkers(text)
3433
+ .map((run) => run.text)
3434
+ .join('')
3435
+ .replace(/\s+/g, ' ')
3436
+ .trim();
3437
+ };
3438
+ /**
3439
+ * The detail, as one paragraph per authored line with blank lines dropped —
3440
+ * the same grammar `r-list` uses for its items, so an author who has met one
3441
+ * has met both. An unset attribute falls back to the placeholder; text the
3442
+ * author deliberately cleared stays cleared and the disclosure opens onto
3443
+ * whatever else is in it, or onto nothing.
3444
+ */
3445
+ const resolveAccordionDetail = (componentAttributes) => {
3446
+ const value = componentAttributes?.[ACCORDION_DETAIL_ATTRIBUTE];
3447
+ const text = value === undefined || value === null
3448
+ ? DEFAULT_ACCORDION_DETAIL
3449
+ : typeof value === 'string'
3450
+ ? value
3451
+ : String(value);
3452
+ return text
3453
+ .split(/\r?\n/)
3454
+ .map((line) => line.trim())
3455
+ .filter((line) => line !== '');
3456
+ };
3457
+ /**
3458
+ * Whether the disclosure renders already open. Select editors in the composer
3459
+ * store `'true'` / `'false'` as strings, while a hand-written or migrated
3460
+ * config may carry a real boolean; both mean the same thing here.
3461
+ *
3462
+ * This is an attribute rather than runtime state on purpose: `open` has to be
3463
+ * right in the server's HTML, because a disclosure a search engine or a
3464
+ * no-JavaScript reader should see expanded cannot wait for hydration.
3465
+ */
3466
+ const resolveAccordionOpen = (componentAttributes) => {
3467
+ const value = componentAttributes?.[ACCORDION_OPEN_ATTRIBUTE];
3468
+ if (typeof value === 'boolean') {
3469
+ return value;
3470
+ }
3471
+ return typeof value === 'string' && value.trim().toLowerCase() === 'true';
3472
+ };
3473
+ /**
3474
+ * The heading tag to wrap the summary in, or `undefined` for no heading.
3475
+ *
3476
+ * A stored value reaches an element name here, so the allowlist is the
3477
+ * boundary that keeps it from being whatever a request happened to write.
3478
+ * `none` and anything unrecognised both mean "no heading", which is also the
3479
+ * safe answer: a plain `<summary>` is a valid disclosure, while an invented
3480
+ * tag inside one is not.
3481
+ */
3482
+ const resolveAccordionHeadingLevel = (componentAttributes) => {
3483
+ const value = componentAttributes?.[ACCORDION_HEADING_LEVEL_ATTRIBUTE];
3484
+ if (value === 'none' || !ACCORDION_HEADING_LEVELS.includes(value)) {
3485
+ return undefined;
3486
+ }
3487
+ return value;
3488
+ };
3489
+
3490
+ const SUMMARY_LAYOUT_STYLE = {
3491
+ display: 'list-item',
3492
+ width: '100%',
3493
+ boxSizing: 'border-box',
3494
+ };
3495
+ const SUMMARY_HEADING_STYLE = {
3496
+ display: 'inline',
3497
+ margin: 0,
3498
+ };
3499
+ class RAccordionComponent {
3500
+ constructor() {
3501
+ this.summaryLayoutStyle = SUMMARY_LAYOUT_STYLE;
3502
+ this.summaryHeadingStyle = SUMMARY_HEADING_STYLE;
3503
+ }
3504
+ get summary() {
3505
+ return resolveAccordionSummary(this.componentAttributes);
3506
+ }
3507
+ get headingLevel() {
3508
+ return resolveAccordionHeadingLevel(this.componentAttributes);
3509
+ }
3510
+ get links() {
3511
+ return this.componentAttributes?.[ACCORDION_LINKS_ATTRIBUTE];
3512
+ }
3513
+ get open() {
3514
+ return resolveAccordionOpen(this.componentAttributes);
3515
+ }
3516
+ get detail() {
3517
+ return resolveAccordionDetail(this.componentAttributes);
3518
+ }
3519
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RAccordionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3520
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RAccordionComponent, isStandalone: true, selector: "rulecms-r-accordion", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
3521
+ <details [ngStyle]="styleProps" [class]="className || undefined" [attr.open]="open ? true : null">
3522
+ <summary [ngStyle]="summaryLayoutStyle">
3523
+ @if (headingLevel) {
3524
+ <rulecms-html-tag [tag]="headingLevel" [styleProps]="summaryHeadingStyle">
3525
+ {{ summary }}
3526
+ </rulecms-html-tag>
3527
+ } @else {
3528
+ {{ summary }}
3529
+ }
3530
+ </summary>
3531
+ @for (paragraph of detail; track $index) {
3532
+ <p>
3533
+ <rulecms-inline-markup [text]="paragraph" [links]="links" />
3534
+ </p>
3535
+ }
3536
+ <ng-content />
3537
+ </details>
3538
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: InlineMarkupComponent, selector: "rulecms-inline-markup", inputs: ["text", "links"] }, { kind: "component", type: HtmlTagComponent, selector: "rulecms-html-tag", inputs: ["tag", "styleProps", "className"] }] }); }
3539
+ }
3540
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RAccordionComponent, decorators: [{
3541
+ type: Component,
3542
+ args: [{
3543
+ selector: 'rulecms-r-accordion',
3544
+ standalone: true,
3545
+ imports: [NgStyle, InlineMarkupComponent, HtmlTagComponent],
3546
+ template: `
3547
+ <details [ngStyle]="styleProps" [class]="className || undefined" [attr.open]="open ? true : null">
3548
+ <summary [ngStyle]="summaryLayoutStyle">
3549
+ @if (headingLevel) {
3550
+ <rulecms-html-tag [tag]="headingLevel" [styleProps]="summaryHeadingStyle">
3551
+ {{ summary }}
3552
+ </rulecms-html-tag>
3553
+ } @else {
3554
+ {{ summary }}
3555
+ }
3556
+ </summary>
3557
+ @for (paragraph of detail; track $index) {
3558
+ <p>
3559
+ <rulecms-inline-markup [text]="paragraph" [links]="links" />
3560
+ </p>
3561
+ }
3562
+ <ng-content />
3563
+ </details>
3564
+ `,
3565
+ host: { style: 'display: contents' },
3566
+ }]
3567
+ }], propDecorators: { componentAttributes: [{
3568
+ type: Input
3569
+ }], styleProps: [{
3570
+ type: Input
3571
+ }], className: [{
3572
+ type: Input
3573
+ }] } });
3574
+
3575
+ /**
3576
+ * RAccordion always renders a `<details>` — a block-level element that holds
3577
+ * margin, padding, width and border as authored — and puts styleProps on that
3578
+ * element itself. No configuration changes the answer, so no wrapper div,
3579
+ * ever.
3580
+ */
3581
+ const resolveNoContainer = () => false;
3582
+
3583
+ /**
3584
+ * RAccordion's root is always a real element — `<details>` — so it can always
3585
+ * carry the column's className, and the component applies the one it receives.
3586
+ */
3587
+ const resolveRendersOwnElement = () => true;
3588
+
3589
+ const defaultAttributeValues = [
3590
+ {
3591
+ name: ACCORDION_SUMMARY_ATTRIBUTE,
3592
+ defaultValues: {
3593
+ all: DEFAULT_ACCORDION_SUMMARY,
3594
+ },
3595
+ },
3596
+ {
3597
+ name: ACCORDION_DETAIL_ATTRIBUTE,
3598
+ defaultValues: {
3599
+ all: DEFAULT_ACCORDION_DETAIL,
3600
+ },
3601
+ },
3602
+ // Closed by default, and unset rather than 'false': a disclosure the author
3603
+ // has not thought about should start collapsed, which is what an absent
3604
+ // `open` attribute already means in HTML.
3605
+ {
3606
+ name: ACCORDION_OPEN_ATTRIBUTE,
3607
+ defaultValues: {},
3608
+ },
3609
+ {
3610
+ name: ACCORDION_HEADING_LEVEL_ATTRIBUTE,
3611
+ defaultValues: {
3612
+ all: DEFAULT_ACCORDION_HEADING_LEVEL,
3613
+ },
3614
+ },
3615
+ {
3616
+ name: ACCORDION_LINKS_ATTRIBUTE,
3617
+ defaultValues: {},
3618
+ },
3619
+ ];
3620
+
3621
+ /**
3622
+ * Every setting the accordion offers, in the order the Modify drawer shows
3623
+ * them.
3624
+ *
3625
+ * Headings name the two halves of the element — Summary and Detail — not one
3626
+ * use of it. An author building an FAQ reads "Summary" as the question without
3627
+ * being told to; an author building a "show more" would have had to translate
3628
+ * "Question" into something it is not.
3629
+ *
3630
+ * Order is not cosmetic. The drawer renders these top to bottom and the
3631
+ * documentation registry asserts its pages match that sequence, so moving an
3632
+ * entry here moves it in both places at once.
3633
+ */
3634
+ const attributeEditModeConfigs = [
3635
+ {
3636
+ name: ACCORDION_SUMMARY_ATTRIBUTE,
3637
+ heading: 'Summary',
3638
+ subHeading: 'The line a reader sees closed and clicks to expand. One line — extra lines collapse into it, for 3 different device resolutions',
3639
+ defaultValues: {},
3640
+ },
3641
+ {
3642
+ name: ACCORDION_DETAIL_ATTRIBUTE,
3643
+ heading: 'Detail',
3644
+ subHeading: 'What the summary expands to, one paragraph per line. Blank lines are skipped, for 3 different device resolutions',
3645
+ defaultValues: {},
3646
+ },
3647
+ {
3648
+ name: ACCORDION_OPEN_ATTRIBUTE,
3649
+ heading: 'Open by Default',
3650
+ subHeading: 'Whether the detail is already expanded when the page loads. A reader can still collapse it, for 3 different device resolutions',
3651
+ defaultValues: {},
3652
+ },
3653
+ {
3654
+ name: ACCORDION_HEADING_LEVEL_ATTRIBUTE,
3655
+ heading: 'Summary Heading Level',
3656
+ subHeading: 'Whether the summary also counts as a heading in the page outline, and at which level. Leave it at none unless these summaries are the structure of the page, for 3 different device resolutions',
3657
+ defaultValues: { all: DEFAULT_ACCORDION_HEADING_LEVEL },
3658
+ },
3659
+ {
3660
+ name: ACCORDION_LINKS_ATTRIBUTE,
3661
+ heading: 'Links',
3662
+ subHeading: 'Links the detail can point at. To link part of the detail, wrap those words in {1|linked text}, where the number is the position of the entry in this list counting from 1; clearing an entry leaves its position in place so the other numbers keep pointing where they did, for 3 different device resolutions',
3663
+ defaultValues: {},
3664
+ },
3665
+ ];
3666
+
3667
+ const displayCardEntry = {
3668
+ id: ComponentIdentifiers.rAccordion,
3669
+ resolveNoContainer,
3670
+ resolveRendersOwnElement,
3671
+ card: RAccordionComponent,
3672
+ defaultAttributeValues,
3673
+ attributeEditModeConfigs,
3674
+ };
3675
+
3676
+ const cardEntries$1 = [
3677
+ displayCardEntry$8,
3678
+ displayCardEntry$7,
3679
+ displayCardEntry$6,
3680
+ displayCardEntry$5,
3681
+ displayCardEntry$4,
3682
+ displayCardEntry$3,
3683
+ displayCardEntry$2,
3684
+ displayCardEntry$1,
3685
+ displayCardEntry,
3686
+ ];
3687
+
3688
+ const cardGroups$1 = [
3689
+ { title: 'Cards', cards: [{ title: 'Cards', entries: cardEntries$1 }] },
3690
+ ];
3691
+
3692
+ let cachedSourceComponentsPlugin;
3693
+ const getComponentsFromRenderCardGroups = (group) => {
3694
+ const entries = group.reduce((all, current) => all.concat(current.cards.reduce((cards, card) => cards.concat(card.entries), [])), []);
3695
+ return entries.reduce((acc, entry) => {
3696
+ acc[entry.id] = {
3697
+ id: entry.id,
3698
+ card: entry.card,
3699
+ defaultAttributeValues: entry.defaultAttributeValues,
3700
+ ...(entry.resolveNoContainer !== undefined ? { resolveNoContainer: entry.resolveNoContainer } : {}),
3701
+ ...(entry.noContainer !== undefined ? { noContainer: entry.noContainer } : {}),
3702
+ ...(entry.resolveRendersOwnElement !== undefined
3703
+ ? { resolveRendersOwnElement: entry.resolveRendersOwnElement }
3704
+ : {}),
3705
+ };
3706
+ return acc;
3707
+ }, {});
3708
+ };
3709
+ /**
3710
+ * Returns the source components plugin with a lookup map of render components.
3711
+ * Results are cached after the first call.
3712
+ */
3713
+ const getSourceComponentsPlugin = () => {
3714
+ if (cachedSourceComponentsPlugin) {
3715
+ return cachedSourceComponentsPlugin;
3716
+ }
3717
+ const components = getComponentsFromRenderCardGroups(cardGroups$1);
3718
+ cachedSourceComponentsPlugin = {
3719
+ components,
3720
+ };
3721
+ return cachedSourceComponentsPlugin;
3722
+ };
3723
+
3724
+ /**
3725
+ * Mapping of attribute names to their corresponding editor components.
3726
+ *
3727
+ * The RuleCMS editor dynamically loads the appropriate editor component for each attribute
3728
+ * that users can edit. This mapping ensures that the correct component is loaded when
3729
+ * editing a given attribute.
3730
+ *
3731
+ * The attributes defined here are shared across all source components available for user modification.
3732
+ */
3733
+ const componentNameToModifyAttribute = {
3734
+ 'icon-name-3-resolutions': 'icon-name-3-resolutions',
3735
+ 'content-text-3-resolutions': 'content-text-3-resolutions',
3736
+ // r-button owns its own label and icon attributes — one component per
3737
+ // documented setting — but reuses the editors those names already resolve
3738
+ // to, so no new editor component is needed for either.
3739
+ 'button-label-3-resolutions': 'content-text-3-resolutions',
3740
+ 'button-icon-3-resolutions': 'icon-name-3-resolutions',
3741
+ // The preset is an object of three axes rather than one scalar, and the
3742
+ // button takes a single link rather than the numbered list rich text uses,
3743
+ // so neither fits an existing editor. Position and disabled are plain
3744
+ // two-value choices and go through the shared select.
3745
+ 'button-preset-3-resolutions': 'button-preset-3-resolutions',
3746
+ 'button-link-3-resolutions': 'button-link-3-resolutions',
3747
+ 'button-icon-position-3-resolutions': 'button-icon-position-3-resolutions',
3748
+ 'button-disabled-3-resolutions': 'button-disabled-3-resolutions',
3749
+ // Both of the divider's settings are closed sets of a handful of values, so
3750
+ // both go through the shared select and neither needs an editor built for it.
3751
+ 'divider-style-3-resolutions': 'divider-style-3-resolutions',
3752
+ 'divider-thickness-3-resolutions': 'divider-thickness-3-resolutions',
3753
+ // The embed URL is the one field in the library whose value is accepted or
3754
+ // refused rather than merely stored, so it gets an editor that can say which
3755
+ // it was while the author is still looking at the field. Its title is plain
3756
+ // text, and the other two are closed sets.
3757
+ 'embed-url-3-resolutions': 'embed-url-3-resolutions',
3758
+ 'embed-title-3-resolutions': 'image-text-3-resolutions',
3759
+ 'embed-aspect-ratio-3-resolutions': 'embed-aspect-ratio-3-resolutions',
3760
+ 'embed-loading-3-resolutions': 'embed-loading-3-resolutions',
3761
+ 'enclosing-tag-3-resolutions': 'enclosing-tag-3-resolutions',
3762
+ 'links-3-resolutions': 'links-3-resolutions',
3763
+ // r-list owns its own attribute names — one component per documented
3764
+ // setting — but reuses existing editors wherever one fits: the multi-line
3765
+ // text field for its items, the numbered link list for its links, and the
3766
+ // plain text field for where numbering starts (the number editor is
3767
+ // pixel-specific, and a numbering start is not a length). Kind, marker and
3768
+ // reversed are closed sets and go through the shared select.
3769
+ 'list-items-3-resolutions': 'content-text-3-resolutions',
3770
+ 'list-kind-3-resolutions': 'list-kind-3-resolutions',
3771
+ 'list-marker-3-resolutions': 'list-marker-3-resolutions',
3772
+ 'list-start-3-resolutions': 'image-text-3-resolutions',
3773
+ 'list-reversed-3-resolutions': 'list-reversed-3-resolutions',
3774
+ 'list-links-3-resolutions': 'links-3-resolutions',
3775
+ // r-accordion owns its own attribute names — one component per documented
3776
+ // setting — and reuses existing editors for everything that fits: the
3777
+ // multi-line text field for the summary and the detail, and the numbered
3778
+ // link list for the detail's links. Whether it starts open and whether the
3779
+ // summary is a heading are closed sets and go through the shared select.
3780
+ 'accordion-summary-3-resolutions': 'content-text-3-resolutions',
3781
+ 'accordion-detail-3-resolutions': 'content-text-3-resolutions',
3782
+ 'accordion-open-3-resolutions': 'accordion-open-3-resolutions',
3783
+ 'accordion-heading-level-3-resolutions': 'accordion-heading-level-3-resolutions',
3784
+ 'accordion-links-3-resolutions': 'links-3-resolutions',
3785
+ 'content-title-3-resolutions': 'content-title-3-resolutions',
3786
+ 'media-selector-3-resolutions': 'media-selector-3-resolutions',
3787
+ 'pass-through-image-attributes-alt-3-resolutions': 'image-alt-text-3-resolutions',
3788
+ 'pass-through-image-attributes-width-3-resolutions': 'image-intrinsic-width-3-resolutions',
3789
+ 'pass-through-image-attributes-height-3-resolutions': 'image-intrinsic-height-3-resolutions',
3790
+ 'pass-through-image-attributes-sizes-3-resolutions': 'image-sizes-3-resolutions',
3791
+ 'pass-through-image-attributes-loading-3-resolutions': 'image-loading-3-resolutions',
3792
+ 'pass-through-image-attributes-fetchpriority-3-resolutions': 'image-fetchpriority-3-resolutions',
3793
+ 'cloudinary-transformations-3-resolutions': 'cloudinary-transformations-3-resolutions',
3794
+ 'video-selector-3-resolutions': 'video-selector-3-resolutions',
3795
+ 'video-poster-selector-3-resolutions': 'video-poster-selector-3-resolutions',
3796
+ 'pass-through-video-attributes-controls-3-resolutions': 'video-controls-3-resolutions',
3797
+ 'pass-through-video-attributes-autoplay-3-resolutions': 'video-autoplay-3-resolutions',
3798
+ 'pass-through-video-attributes-loop-3-resolutions': 'video-loop-3-resolutions',
3799
+ 'pass-through-video-attributes-muted-3-resolutions': 'video-muted-3-resolutions',
3800
+ 'pass-through-video-attributes-playsinline-3-resolutions': 'video-playsinline-3-resolutions',
3801
+ 'pass-through-video-attributes-preload-3-resolutions': 'video-preload-3-resolutions',
3802
+ 'pass-through-video-attributes-width-3-resolutions': 'video-intrinsic-width-3-resolutions',
3803
+ 'pass-through-video-attributes-height-3-resolutions': 'video-intrinsic-height-3-resolutions',
3804
+ 'video-transformations-3-resolutions': 'video-transformations-3-resolutions',
3805
+ };
3806
+
3807
+ /**
3808
+ * Mapping of component IDs to their supported modifiable attributes.
3809
+ *
3810
+ * Each component type can have a specific list of attributes that users are allowed to modify
3811
+ * in the editor. This mapping defines which attributes are available for each component type.
3812
+ *
3813
+ * Note: This defines which attributes each component supports, while component-name-to-modify-attribute.ts
3814
+ * defines which editor component should be used to modify each attribute type.
3815
+ * todo: this is a little confusing and might require a deeper look into how RuleCMS uses this.
3816
+ */
3817
+ /**
3818
+ * Empty on purpose. Every component this library ships declares its editable
3819
+ * attributes through its own `attribute-edit-mode-configs.ts`, which is the
3820
+ * path the composer's Modify drawer reads. Listing an attribute here as well
3821
+ * shows it twice — once through the legacy shared-attribute editor, which has
3822
+ * no entry for modern `-3-resolutions` names and renders an empty section.
3823
+ */
3824
+ const attributeListForComponentTypes = {};
3825
+
3826
+ let returnValue;
3827
+ // Flatten nested card groups and cards into a flat array of entries, then converts to a lookup map by entry ID
3828
+ const getComponentMetadataForEditing = () => {
3829
+ if (returnValue) {
3830
+ return returnValue;
3831
+ }
3832
+ const entries = cardGroups$1.reduce((all, current) => all.concat(current.cards.reduce((cards, card) => cards.concat(card.entries), [])), []);
3833
+ const componentMetadata = entries.reduce((acc, entry) => {
3834
+ acc[entry.id] = {
3835
+ defaultAttributeValues: entry.defaultAttributeValues,
3836
+ attributeEditModeConfigs: entry.attributeEditModeConfigs,
3837
+ };
3838
+ return acc;
3839
+ }, {});
3840
+ returnValue = {
3841
+ attributeListForComponentTypes,
3842
+ componentNameToModifyAttribute,
3843
+ componentMetadata,
3844
+ };
3845
+ return returnValue;
3846
+ };
3847
+
3848
+ const cardEntries = [
3849
+ { id: ComponentIdentifiers.rText, icon: 'T', label: 'Text' },
3850
+ { id: ComponentIdentifiers.cloudinaryAdvancedImage, icon: '🖼️', label: 'Image' },
3851
+ { id: ComponentIdentifiers.rVideo, icon: '🎬', label: 'Video' },
3852
+ { id: ComponentIdentifiers.rIcon, icon: '◆', label: 'Icon' },
3853
+ { id: ComponentIdentifiers.rButton, icon: '⬭', label: 'Button' },
3854
+ { id: ComponentIdentifiers.rDivider, icon: '━', label: 'Divider' },
3855
+ { id: ComponentIdentifiers.rEmbed, icon: '⧉', label: 'Embed' },
3856
+ { id: ComponentIdentifiers.rList, icon: '☰', label: 'List' },
3857
+ { id: ComponentIdentifiers.rAccordion, icon: '▸', label: 'Accordion' },
3858
+ ];
3859
+
3860
+ const cardGroups = [
3861
+ {
3862
+ title: 'Elements',
3863
+ showInitially: true,
3864
+ cards: [{ title: 'Base Elements', entries: cardEntries }],
3865
+ },
3866
+ ];
3867
+
3868
+ let cachedPreviewComponents;
3869
+ /**
3870
+ * Returns the preview card groups for drag and drop functionality.
3871
+ * This replaces the dragDropPreviewCardGroups export.
3872
+ */
3873
+ const getPreviewCardGroups = () => {
3874
+ return cardGroups;
3875
+ };
3876
+ /**
3877
+ * Returns a lookup map of preview components by entry ID.
3878
+ * Results are cached after the first call.
3879
+ */
3880
+ const getPreviewComponents = () => {
3881
+ if (cachedPreviewComponents) {
3882
+ return cachedPreviewComponents;
3883
+ }
3884
+ // Flatten nested card groups and cards into a flat array of entries
3885
+ const entries = [];
3886
+ for (const group of cardGroups) {
3887
+ for (const card of group.cards) {
3888
+ entries.push(...card.entries);
3889
+ }
3890
+ }
3891
+ // Convert to a lookup map by entry ID
3892
+ cachedPreviewComponents = entries.reduce((acc, entry) => {
3893
+ acc[entry.id] = entry;
3894
+ return acc;
3895
+ }, {});
3896
+ return cachedPreviewComponents;
3897
+ };
3898
+
3899
+ /**
3900
+ * ComponentLibraryModule conformance (the @rulecms/widget-angular contract).
3901
+ *
3902
+ * This package is the DEFAULT component library. It conforms to the contract
3903
+ * STRUCTURALLY — it has no runtime dependency on @rulecms/widget-angular.
3904
+ * Consumers register it, e.g.:
3905
+ *
3906
+ * libraries: { default: () => import('@rulecms/source-components-angular') }
3907
+ *
3908
+ * or statically import the module for SSR pages.
3909
+ */
3910
+ const manifest = {
3911
+ id: 'default',
3912
+ version: '0.1.0',
3913
+ angularPeerRange: '>=17.0.0',
3914
+ };
3915
+ /** The render surface: component type → renderable entry. */
3916
+ const components = getSourceComponentsPlugin().components;
3917
+ /** The editor surface: palette + attribute metadata for the RuleCMS composer. */
3918
+ const editor = {
3919
+ previewComponents: getPreviewComponents(),
3920
+ previewCardGroups: getPreviewCardGroups(),
3921
+ componentMetadata: getComponentMetadataForEditing(),
3922
+ };
3923
+
3924
+ /**
3925
+ * Canonical letter order, so the same span always serializes the same way and
3926
+ * a source-mode author sees `{bi1|…}` rather than whichever order an editor
3927
+ * happened to apply the marks in.
3928
+ */
3929
+ const MARK_LETTERS = [
3930
+ ['bold', 'b'],
3931
+ ['italic', 'i'],
3932
+ ['strike', 's'],
3933
+ ];
3934
+ const markLetters = (marks) => MARK_LETTERS.filter(([mark]) => marks[mark])
3935
+ .map(([, letter]) => letter)
3936
+ .join('');
3937
+ /**
3938
+ * A span back to its marker. Text that carries neither marks nor an index is
3939
+ * emitted bare: a marker with nothing to say would only be noise an author has
3940
+ * to read past, and it round-trips to the same runs either way.
3941
+ */
3942
+ const formatSpan = (run) => {
3943
+ const letters = markLetters(run.marks);
3944
+ const index = run.index === undefined ? '' : `${run.index}`;
3945
+ if (letters === '' && index === '') {
3946
+ return run.text;
3947
+ }
3948
+ return `{${letters}${index}|${run.text}}`;
3949
+ };
3950
+ /** One run as authored text. */
3951
+ function formatInlineMarker(run) {
3952
+ switch (run.kind) {
3953
+ case 'text':
3954
+ return run.text;
3955
+ case 'break':
3956
+ return '{br|}';
3957
+ default:
3958
+ return formatSpan(run);
3959
+ }
3960
+ }
3961
+ /**
3962
+ * Runs back to the string they were parsed from.
3963
+ *
3964
+ * This is the writer half of the grammar, and it lives beside the parser so
3965
+ * that an editor never hand-assembles marker syntax. `parseInlineMarkers` and
3966
+ * this function are inverses for anything the parser produces, which is what
3967
+ * lets a rich editor round-trip through source mode without rewriting content
3968
+ * the author did not touch.
3969
+ */
3970
+ function serializeInlineMarkers(runs) {
3971
+ return runs.map(formatInlineMarker).join('');
3972
+ }
3973
+
3974
+ /**
3975
+ * Generated bundle index. Do not edit.
3976
+ */
3977
+
3978
+ export { ACCORDION_HEADING_LEVELS, BLOCK_ENCLOSING_TAGS, BUTTON_CLICK_HANDLER_PROP, BUTTON_CSS_VARIABLE_PREFIX, BUTTON_SIZES, BUTTON_TONES, BUTTON_VARIANTS, ComponentIdentifiers, DEFAULT_ACCORDION_HEADING_LEVEL, DEFAULT_BUTTON_LABEL, DEFAULT_BUTTON_PRESET, DEFAULT_EMBED_LOADING, DEFAULT_ICON_POSITION, DEFAULT_LIST_KIND, EMBED_ASPECT_RATIOS, EMBED_LOADING_VALUES, EMBED_PROVIDERS, EMBED_UNSHAPED_HEIGHT, ENCLOSING_TAGS, ICON_POSITIONS, INLINE_ENCLOSING_TAGS, InlineMarkupComponent, LIST_KINDS, LIST_MARKERS, REJECTED_HREF_SCHEMES, SIZE_SCALES, TONE_PALETTES, buildButtonPresetStyles, disabledStyles as buttonDisabledStyles, cardGroups$1 as cardGroups, components, editor, formatInlineMarker, getComponentMetadataForEditing, getPreviewCardGroups, getPreviewComponents, getSourceComponentsPlugin, isAllowedEmbedUrl, isBlockEnclosingTag, isButtonSize, isButtonTone, isButtonVariant, isEnclosingTag, isSafeHref, manifest, normaliseHost, parseInlineLinkMarkers, parseInlineMarkers, resolveButtonPreset, resolveEmbedUrl, resolveLinkAttributes, serializeInlineMarkers, toneCssVariable };
3979
+ //# sourceMappingURL=rulecms-source-components-angular.mjs.map