@aglyn/plugins-mui 1.0.0-beta.143

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 (177) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +22 -0
  3. package/package.json +47 -0
  4. package/src/index.d.ts +21 -0
  5. package/src/index.js +22 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/accordion.d.ts +96 -0
  8. package/src/lib/components/accordion.js +392 -0
  9. package/src/lib/components/accordion.js.map +1 -0
  10. package/src/lib/components/app-bar.d.ts +32 -0
  11. package/src/lib/components/app-bar.js +86 -0
  12. package/src/lib/components/app-bar.js.map +1 -0
  13. package/src/lib/components/blocks.d.ts +46 -0
  14. package/src/lib/components/blocks.js +1007 -0
  15. package/src/lib/components/blocks.js.map +1 -0
  16. package/src/lib/components/box.d.ts +47 -0
  17. package/src/lib/components/box.js +125 -0
  18. package/src/lib/components/box.js.map +1 -0
  19. package/src/lib/components/breadcrumbs.d.ts +44 -0
  20. package/src/lib/components/breadcrumbs.js +172 -0
  21. package/src/lib/components/breadcrumbs.js.map +1 -0
  22. package/src/lib/components/button.d.ts +89 -0
  23. package/src/lib/components/button.js +272 -0
  24. package/src/lib/components/button.js.map +1 -0
  25. package/src/lib/components/card.d.ts +61 -0
  26. package/src/lib/components/card.js +336 -0
  27. package/src/lib/components/card.js.map +1 -0
  28. package/src/lib/components/collection.d.ts +514 -0
  29. package/src/lib/components/collection.js +2999 -0
  30. package/src/lib/components/collection.js.map +1 -0
  31. package/src/lib/components/container.d.ts +42 -0
  32. package/src/lib/components/container.js +169 -0
  33. package/src/lib/components/container.js.map +1 -0
  34. package/src/lib/components/custom-html.d.ts +78 -0
  35. package/src/lib/components/custom-html.js +219 -0
  36. package/src/lib/components/custom-html.js.map +1 -0
  37. package/src/lib/components/data-table.d.ts +57 -0
  38. package/src/lib/components/data-table.js +216 -0
  39. package/src/lib/components/data-table.js.map +1 -0
  40. package/src/lib/components/document-root.d.ts +77 -0
  41. package/src/lib/components/document-root.js +118 -0
  42. package/src/lib/components/document-root.js.map +1 -0
  43. package/src/lib/components/drawer.d.ts +93 -0
  44. package/src/lib/components/drawer.js +566 -0
  45. package/src/lib/components/drawer.js.map +1 -0
  46. package/src/lib/components/function-controls.d.ts +66 -0
  47. package/src/lib/components/function-controls.js +207 -0
  48. package/src/lib/components/function-controls.js.map +1 -0
  49. package/src/lib/components/function-scope.d.ts +110 -0
  50. package/src/lib/components/function-scope.js +478 -0
  51. package/src/lib/components/function-scope.js.map +1 -0
  52. package/src/lib/components/function-widget.d.ts +75 -0
  53. package/src/lib/components/function-widget.js +307 -0
  54. package/src/lib/components/function-widget.js.map +1 -0
  55. package/src/lib/components/grid.d.ts +91 -0
  56. package/src/lib/components/grid.js +322 -0
  57. package/src/lib/components/grid.js.map +1 -0
  58. package/src/lib/components/icon.d.ts +54 -0
  59. package/src/lib/components/icon.js +163 -0
  60. package/src/lib/components/icon.js.map +1 -0
  61. package/src/lib/components/image-list.d.ts +58 -0
  62. package/src/lib/components/image-list.js +336 -0
  63. package/src/lib/components/image-list.js.map +1 -0
  64. package/src/lib/components/image.d.ts +154 -0
  65. package/src/lib/components/image.js +451 -0
  66. package/src/lib/components/image.js.map +1 -0
  67. package/src/lib/components/inline-text.d.ts +74 -0
  68. package/src/lib/components/inline-text.js +407 -0
  69. package/src/lib/components/inline-text.js.map +1 -0
  70. package/src/lib/components/language-switcher.d.ts +35 -0
  71. package/src/lib/components/language-switcher.js +179 -0
  72. package/src/lib/components/language-switcher.js.map +1 -0
  73. package/src/lib/components/layout-slot.d.ts +68 -0
  74. package/src/lib/components/layout-slot.js +192 -0
  75. package/src/lib/components/layout-slot.js.map +1 -0
  76. package/src/lib/components/link-box-accessible-name.d.ts +79 -0
  77. package/src/lib/components/link-box-accessible-name.js +158 -0
  78. package/src/lib/components/link-box-accessible-name.js.map +1 -0
  79. package/src/lib/components/link-box.d.ts +66 -0
  80. package/src/lib/components/link-box.js +212 -0
  81. package/src/lib/components/link-box.js.map +1 -0
  82. package/src/lib/components/list-item-text.d.ts +22 -0
  83. package/src/lib/components/list-item-text.js +86 -0
  84. package/src/lib/components/list-item-text.js.map +1 -0
  85. package/src/lib/components/list-item.d.ts +32 -0
  86. package/src/lib/components/list-item.js +127 -0
  87. package/src/lib/components/list-item.js.map +1 -0
  88. package/src/lib/components/list.d.ts +34 -0
  89. package/src/lib/components/list.js +108 -0
  90. package/src/lib/components/list.js.map +1 -0
  91. package/src/lib/components/markdown.d.ts +129 -0
  92. package/src/lib/components/markdown.js +853 -0
  93. package/src/lib/components/markdown.js.map +1 -0
  94. package/src/lib/components/nav-menu.d.ts +68 -0
  95. package/src/lib/components/nav-menu.js +683 -0
  96. package/src/lib/components/nav-menu.js.map +1 -0
  97. package/src/lib/components/pagination.d.ts +59 -0
  98. package/src/lib/components/pagination.js +243 -0
  99. package/src/lib/components/pagination.js.map +1 -0
  100. package/src/lib/components/paper.d.ts +47 -0
  101. package/src/lib/components/paper.js +202 -0
  102. package/src/lib/components/paper.js.map +1 -0
  103. package/src/lib/components/plugin-frame.d.ts +64 -0
  104. package/src/lib/components/plugin-frame.js +335 -0
  105. package/src/lib/components/plugin-frame.js.map +1 -0
  106. package/src/lib/components/plugin.d.ts +95 -0
  107. package/src/lib/components/plugin.js +304 -0
  108. package/src/lib/components/plugin.js.map +1 -0
  109. package/src/lib/components/product.d.ts +40 -0
  110. package/src/lib/components/product.js +287 -0
  111. package/src/lib/components/product.js.map +1 -0
  112. package/src/lib/components/reusable-instance.d.ts +57 -0
  113. package/src/lib/components/reusable-instance.js +126 -0
  114. package/src/lib/components/reusable-instance.js.map +1 -0
  115. package/src/lib/components/screen-link.d.ts +85 -0
  116. package/src/lib/components/screen-link.js +324 -0
  117. package/src/lib/components/screen-link.js.map +1 -0
  118. package/src/lib/components/search-box.d.ts +41 -0
  119. package/src/lib/components/search-box.js +125 -0
  120. package/src/lib/components/search-box.js.map +1 -0
  121. package/src/lib/components/section.d.ts +56 -0
  122. package/src/lib/components/section.js +132 -0
  123. package/src/lib/components/section.js.map +1 -0
  124. package/src/lib/components/stack.d.ts +55 -0
  125. package/src/lib/components/stack.js +309 -0
  126. package/src/lib/components/stack.js.map +1 -0
  127. package/src/lib/components/tabs.d.ts +288 -0
  128. package/src/lib/components/tabs.js +788 -0
  129. package/src/lib/components/tabs.js.map +1 -0
  130. package/src/lib/components/theme-mode-switcher.d.ts +38 -0
  131. package/src/lib/components/theme-mode-switcher.js +168 -0
  132. package/src/lib/components/theme-mode-switcher.js.map +1 -0
  133. package/src/lib/components/toolbar.d.ts +37 -0
  134. package/src/lib/components/toolbar.js +117 -0
  135. package/src/lib/components/toolbar.js.map +1 -0
  136. package/src/lib/components/typography.d.ts +33 -0
  137. package/src/lib/components/typography.js +433 -0
  138. package/src/lib/components/typography.js.map +1 -0
  139. package/src/lib/components/video-lightbox-controls.d.ts +131 -0
  140. package/src/lib/components/video-lightbox-controls.js +536 -0
  141. package/src/lib/components/video-lightbox-controls.js.map +1 -0
  142. package/src/lib/components/video-lightbox.d.ts +60 -0
  143. package/src/lib/components/video-lightbox.js +247 -0
  144. package/src/lib/components/video-lightbox.js.map +1 -0
  145. package/src/lib/components/video-playback-beacon.d.ts +80 -0
  146. package/src/lib/components/video-playback-beacon.js +128 -0
  147. package/src/lib/components/video-playback-beacon.js.map +1 -0
  148. package/src/lib/components/video-player-frame.d.ts +58 -0
  149. package/src/lib/components/video-player-frame.js +51 -0
  150. package/src/lib/components/video-player-frame.js.map +1 -0
  151. package/src/lib/components/video.d.ts +220 -0
  152. package/src/lib/components/video.js +874 -0
  153. package/src/lib/components/video.js.map +1 -0
  154. package/src/lib/constants/bundle-common.d.ts +17 -0
  155. package/src/lib/constants/bundle-common.js +18 -0
  156. package/src/lib/constants/bundle-common.js.map +1 -0
  157. package/src/lib/constants/field-presets.d.ts +93 -0
  158. package/src/lib/constants/field-presets.js +248 -0
  159. package/src/lib/constants/field-presets.js.map +1 -0
  160. package/src/lib/plugin.d.ts +80 -0
  161. package/src/lib/plugin.js +599 -0
  162. package/src/lib/plugin.js.map +1 -0
  163. package/src/lib/utils/author-html-label.d.ts +73 -0
  164. package/src/lib/utils/author-html-label.js +89 -0
  165. package/src/lib/utils/author-html-label.js.map +1 -0
  166. package/src/lib/utils/drop-cleared-props.d.ts +18 -0
  167. package/src/lib/utils/drop-cleared-props.js +74 -0
  168. package/src/lib/utils/drop-cleared-props.js.map +1 -0
  169. package/src/lib/utils/element-picker.d.ts +99 -0
  170. package/src/lib/utils/element-picker.js +140 -0
  171. package/src/lib/utils/element-picker.js.map +1 -0
  172. package/src/lib/utils/generate-preset-id.d.ts +19 -0
  173. package/src/lib/utils/generate-preset-id.js +25 -0
  174. package/src/lib/utils/generate-preset-id.js.map +1 -0
  175. package/src/lib/utils/link-target-props.d.ts +53 -0
  176. package/src/lib/utils/link-target-props.js +53 -0
  177. package/src/lib/utils/link-target-props.js.map +1 -0
@@ -0,0 +1,169 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import * as Aglyn from "@aglyn/aglyn";
18
+ import { mdiTextBoxOutline, mdiViewArrayOutline } from "@aglyn/shared-data-mdi";
19
+ import MuiContainer from "@mui/material/Container";
20
+ import { createElement, forwardRef } from "react";
21
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
22
+ import { applySemanticElement, semanticElementAttribute, semanticElementLabelAttribute } from "../utils/element-picker.js";
23
+ import { dropClearedProps } from "../utils/drop-cleared-props.js";
24
+ import { generatePresetId } from "../utils/generate-preset-id.js";
25
+ /**
26
+ * MUI's Container behind the cleared-prop guard (AGL-1435).
27
+ *
28
+ * `maxWidth` is the one authorable attribute in this plugin where a cleared
29
+ * value changes PAGE LAYOUT instead of showing an obviously wrong value, and
30
+ * it satisfied neither branch of MUI's own logic:
31
+ *
32
+ * - `''` / `null` is not `undefined`, so the `maxWidth = 'lg'` destructuring
33
+ * default in `@mui/system/Container/createContainer.js` never fires;
34
+ * - `''` / `null` is falsy, so the `ownerState.maxWidth &&` guard skips both
35
+ * the branch that emits the `max-width` rule and the one that emits the
36
+ * matching utility class.
37
+ *
38
+ * The result is a section with NO width constraint at all — full-bleed, with
39
+ * nothing thrown and nothing visibly wrong in the editor. Clearing the field
40
+ * persists `null` (the AGL-1226 shape; 106 cleared prop values live in the
41
+ * corpus today across other components), so this was reachable, not
42
+ * theoretical. Dropping the key instead lets MUI's own `lg` apply.
43
+ */ const Container = forwardRef((props, ref)=>createElement(MuiContainer, _extends({}, applySemanticElement(dropClearedProps(props)), {
44
+ ref
45
+ })));
46
+ Container.displayName = 'AglynContainer';
47
+ // Component ids are persisted in screen documents; keep the legacy ids.
48
+ export const ID = 'muiContainer';
49
+ export const schema = {
50
+ $id: ID,
51
+ pluginId: BUNDLE_ID,
52
+ displayName: 'Container',
53
+ description: 'Centers your page content and caps how wide it gets.',
54
+ category: Aglyn.ComponentCategory.LAYOUT,
55
+ icon: {
56
+ path: mdiViewArrayOutline.path,
57
+ sx: {
58
+ color: '#2196f3'
59
+ }
60
+ },
61
+ attributes: [
62
+ semanticElementAttribute('this container'),
63
+ semanticElementLabelAttribute(),
64
+ {
65
+ name: 'fixed',
66
+ description: "If true, set the max-width to match the min-width of the current breakpoint. This is useful if you'd prefer to design for a fixed set of sizes instead of trying to accommodate a fully fluid viewport. It's fluid by default.",
67
+ component: Aglyn.FieldComponentType.SWITCH,
68
+ label: 'Fixed Breakpoints'
69
+ },
70
+ {
71
+ name: 'disableGutters',
72
+ description: 'Removes the container’s side padding, so its content runs to the ' + 'full width it is centered in. Use it for a full-bleed band; leave ' + 'it off for text, which needs the margin on a phone.',
73
+ component: Aglyn.FieldComponentType.SWITCH,
74
+ label: 'Disable Gutters?'
75
+ },
76
+ {
77
+ name: 'maxWidth',
78
+ description: 'The width this section stops growing at. New containers start at ' + 'XL (1536px), the standard for full-width page sections. Long-form ' + 'prose — legal pages, docs, blog and changelog bodies — uses MD ' + '(900px), which lands near the 65–75 characters a line that reads ' + 'comfortably; the "Prose Container" preset starts there. LG (1200px) ' + 'is the deliberate middle case for a wide but text-led section, not ' + 'a default to spread. Clearing the field falls back to the MUI ' + 'default of LG. There is no "Default" option: "Fluid Responsive" is ' + 'the only way to render edge-to-edge, and it has to be chosen on ' + 'purpose.',
79
+ component: Aglyn.FieldComponentType.SELECT,
80
+ label: 'Max Width',
81
+ // Every value here is a real, persistable one. The list used to open
82
+ // with `{ value: '', label: 'Default' }`, which was unpersistable by
83
+ // construction — the attributes form strips `''` on change (AGL-1191)
84
+ // — and, on any path that DID land it, rendered full-bleed rather than
85
+ // any default at all (AGL-1435).
86
+ options: [
87
+ {
88
+ value: 'xs',
89
+ label: 'XS - Mobile (444px)'
90
+ },
91
+ {
92
+ value: 'sm',
93
+ label: 'SM - Tablet (600px)'
94
+ },
95
+ {
96
+ value: 'md',
97
+ label: 'MD - Laptop (900px)'
98
+ },
99
+ {
100
+ value: 'lg',
101
+ label: 'LG - Desktop (1200px)'
102
+ },
103
+ {
104
+ value: 'xl',
105
+ label: 'XL - Widescreen (1536px, section standard)'
106
+ },
107
+ {
108
+ value: false,
109
+ label: 'Fluid Responsive (no max width)'
110
+ }
111
+ ]
112
+ }
113
+ ]
114
+ };
115
+ export const presets = [
116
+ {
117
+ $id: generatePresetId(ID),
118
+ type: Aglyn.NodeType.PRESET,
119
+ displayName: 'Container',
120
+ icon: {
121
+ path: mdiViewArrayOutline.path,
122
+ sx: {
123
+ color: '#2196f3'
124
+ }
125
+ },
126
+ category: Aglyn.ComponentCategory.LAYOUT,
127
+ data: {
128
+ $id: null,
129
+ componentId: ID,
130
+ pluginId: BUNDLE_ID,
131
+ // Explicit, not implicit: a dropped Container is a page section, and
132
+ // sections are XL here (245 of the 248 authored on the marketing host).
133
+ // Relying on MUI's implicit `lg` is what made "what does no value mean"
134
+ // an open question in the first place (AGL-1435).
135
+ props: {
136
+ maxWidth: 'xl'
137
+ }
138
+ }
139
+ },
140
+ {
141
+ // Case is MD on READING grounds, not as "one step down" — at XL a
142
+ // paragraph runs 110–120 characters a line, and even LG (1200px) is far
143
+ // past the 65–75 the eye tracks without losing its place. MD is the
144
+ // stock breakpoint that lands in that band, so the standard grows a
145
+ // third case without growing a bespoke number: the 1328px ban holds
146
+ // because 1328 is a content width, not a breakpoint.
147
+ $id: generatePresetId(ID, 'prose'),
148
+ type: Aglyn.NodeType.PRESET,
149
+ displayName: 'Prose Container',
150
+ icon: {
151
+ path: mdiTextBoxOutline.path,
152
+ sx: {
153
+ color: '#2196f3'
154
+ }
155
+ },
156
+ category: Aglyn.ComponentCategory.LAYOUT,
157
+ data: {
158
+ $id: null,
159
+ componentId: ID,
160
+ pluginId: BUNDLE_ID,
161
+ props: {
162
+ maxWidth: 'md'
163
+ }
164
+ }
165
+ }
166
+ ];
167
+ export default Container;
168
+
169
+ //# sourceMappingURL=container.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/container.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n mdiTextBoxOutline,\n mdiViewArrayOutline,\n} from '@aglyn/shared-data-mdi'\nimport MuiContainer, { type ContainerProps } from '@mui/material/Container'\nimport { createElement, forwardRef } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n applySemanticElement,\n semanticElementAttribute,\n semanticElementLabelAttribute,\n} from '../utils/element-picker'\nimport { dropClearedProps } from '../utils/drop-cleared-props'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n/**\n * MUI's Container behind the cleared-prop guard (AGL-1435).\n *\n * `maxWidth` is the one authorable attribute in this plugin where a cleared\n * value changes PAGE LAYOUT instead of showing an obviously wrong value, and\n * it satisfied neither branch of MUI's own logic:\n *\n * - `''` / `null` is not `undefined`, so the `maxWidth = 'lg'` destructuring\n * default in `@mui/system/Container/createContainer.js` never fires;\n * - `''` / `null` is falsy, so the `ownerState.maxWidth &&` guard skips both\n * the branch that emits the `max-width` rule and the one that emits the\n * matching utility class.\n *\n * The result is a section with NO width constraint at all — full-bleed, with\n * nothing thrown and nothing visibly wrong in the editor. Clearing the field\n * persists `null` (the AGL-1226 shape; 106 cleared prop values live in the\n * corpus today across other components), so this was reachable, not\n * theoretical. Dropping the key instead lets MUI's own `lg` apply.\n */\nconst Container = forwardRef<HTMLDivElement, ContainerProps>((props, ref) =>\n createElement(MuiContainer, {\n ...applySemanticElement(dropClearedProps(props) as Record<string, unknown>),\n ref,\n }),\n)\nContainer.displayName = 'AglynContainer'\n\n// Component ids are persisted in screen documents; keep the legacy ids.\nexport const ID: Aglyn.ComponentId = 'muiContainer'\n\nexport const schema: Aglyn.ComponentSchema<ContainerProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Container',\n description: 'Centers your page content and caps how wide it gets.',\n category: Aglyn.ComponentCategory.LAYOUT,\n icon: {\n path: mdiViewArrayOutline.path,\n sx: { color: '#2196f3' },\n },\n attributes: [\n semanticElementAttribute('this container'),\n semanticElementLabelAttribute(),\n {\n name: 'fixed',\n description:\n \"If true, set the max-width to match the min-width of the current breakpoint. This is useful if you'd prefer to design for a fixed set of sizes instead of trying to accommodate a fully fluid viewport. It's fluid by default.\",\n component: Aglyn.FieldComponentType.SWITCH,\n label: 'Fixed Breakpoints',\n },\n {\n name: 'disableGutters',\n description:\n 'Removes the container’s side padding, so its content runs to the ' +\n 'full width it is centered in. Use it for a full-bleed band; leave ' +\n 'it off for text, which needs the margin on a phone.',\n component: Aglyn.FieldComponentType.SWITCH,\n label: 'Disable Gutters?',\n },\n {\n name: 'maxWidth',\n description:\n 'The width this section stops growing at. New containers start at ' +\n 'XL (1536px), the standard for full-width page sections. Long-form ' +\n 'prose — legal pages, docs, blog and changelog bodies — uses MD ' +\n '(900px), which lands near the 65–75 characters a line that reads ' +\n 'comfortably; the \"Prose Container\" preset starts there. LG (1200px) ' +\n 'is the deliberate middle case for a wide but text-led section, not ' +\n 'a default to spread. Clearing the field falls back to the MUI ' +\n 'default of LG. There is no \"Default\" option: \"Fluid Responsive\" is ' +\n 'the only way to render edge-to-edge, and it has to be chosen on ' +\n 'purpose.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'Max Width',\n // Every value here is a real, persistable one. The list used to open\n // with `{ value: '', label: 'Default' }`, which was unpersistable by\n // construction — the attributes form strips `''` on change (AGL-1191)\n // — and, on any path that DID land it, rendered full-bleed rather than\n // any default at all (AGL-1435).\n options: [\n { value: 'xs', label: 'XS - Mobile (444px)' },\n { value: 'sm', label: 'SM - Tablet (600px)' },\n { value: 'md', label: 'MD - Laptop (900px)' },\n { value: 'lg', label: 'LG - Desktop (1200px)' },\n { value: 'xl', label: 'XL - Widescreen (1536px, section standard)' },\n { value: false, label: 'Fluid Responsive (no max width)' },\n ],\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Container',\n icon: {\n path: mdiViewArrayOutline.path,\n sx: { color: '#2196f3' },\n },\n category: Aglyn.ComponentCategory.LAYOUT,\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n // Explicit, not implicit: a dropped Container is a page section, and\n // sections are XL here (245 of the 248 authored on the marketing host).\n // Relying on MUI's implicit `lg` is what made \"what does no value mean\"\n // an open question in the first place (AGL-1435).\n props: { maxWidth: 'xl' },\n },\n },\n {\n // Case is MD on READING grounds, not as \"one step down\" — at XL a\n // paragraph runs 110–120 characters a line, and even LG (1200px) is far\n // past the 65–75 the eye tracks without losing its place. MD is the\n // stock breakpoint that lands in that band, so the standard grows a\n // third case without growing a bespoke number: the 1328px ban holds\n // because 1328 is a content width, not a breakpoint.\n $id: generatePresetId(ID, 'prose'),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Prose Container',\n icon: {\n path: mdiTextBoxOutline.path,\n sx: { color: '#2196f3' },\n },\n category: Aglyn.ComponentCategory.LAYOUT,\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: { maxWidth: 'md' },\n },\n },\n]\n\nexport default Container\n"],"names":["Aglyn","mdiTextBoxOutline","mdiViewArrayOutline","MuiContainer","createElement","forwardRef","BUNDLE_ID","applySemanticElement","semanticElementAttribute","semanticElementLabelAttribute","dropClearedProps","generatePresetId","Container","props","ref","displayName","ID","schema","$id","pluginId","description","category","ComponentCategory","LAYOUT","icon","path","sx","color","attributes","name","component","FieldComponentType","SWITCH","label","SELECT","options","value","presets","type","NodeType","PRESET","data","componentId","maxWidth"],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import * as Aglyn from '@aglyn/aglyn';
18
+ import type { SxProps } from '@mui/material/styles';
19
+ export declare const ID: Aglyn.ComponentId;
20
+ export interface CustomHtmlProps {
21
+ /** Author-provided markup, sanitized before every render. */
22
+ html?: string;
23
+ /** Scoped styles injected as a <style> tag (selectors are the
24
+ * author's responsibility; keep them specific). */
25
+ css?: string;
26
+ /**
27
+ * Embed mode (AGL-320): renders the RAW snippet inside a sandboxed
28
+ * iframe (scripts allowed, same-origin denied) for third-party
29
+ * widgets. Business-plan gated by the contentGating entitlement's
30
+ * sibling `commerce` checks server-side; the besigner surfaces the
31
+ * gate in the attribute description.
32
+ */
33
+ embedMode?: boolean;
34
+ /** Iframe height (px) in embed mode. */
35
+ embedHeight?: number;
36
+ /**
37
+ * Authored node styles, handed over by the renderer rather than typed
38
+ * into an attribute — recomposed below so the Styles panel's values are
39
+ * merged rather than replaced (AGL-1284).
40
+ */
41
+ sx?: SxProps;
42
+ }
43
+ /**
44
+ * Sanitization policy (AGL-320): an allowlist — no scripts, iframes,
45
+ * objects, or event-handler attributes. Applied at EVERY render (server,
46
+ * client, and besigner canvas), so stored markup can never execute even if a
47
+ * write bypassed the console. Embed mode never touches the DOM — the raw
48
+ * snippet only exists inside `sandbox="allow-scripts"` (no
49
+ * allow-same-origin), so it cannot reach cookies, storage, or the parent
50
+ * page.
51
+ *
52
+ * The engine is `Aglyn.sanitizeAuthorHtml` (AGL-1901), which replaced a
53
+ * per-instance DOMPurify. DOMPurify needs a real DOM, so this component
54
+ * could only sanitize in an effect, and every Custom HTML block was
55
+ * therefore absent from the server response — the SSR/SEO half AGL-1268 left
56
+ * open. The new sanitizer is a pure function of the string, so the same call
57
+ * runs on the server and in the browser and both produce the same bytes.
58
+ *
59
+ * What carried over unchanged:
60
+ *
61
+ * - the forbidden set (`script`, `iframe`, `object`, `embed`, `form`,
62
+ * `base`), `srcdoc`/`formaction`, and `ALLOW_DATA_ATTR: false`;
63
+ * - AGL-1725's `style`-attribute rule. DOMPurify performs NO CSS filtering,
64
+ * which is why that hook existed at all; `sanitizeAuthorHtml` applies
65
+ * `sanitizeAuthorCss` to every `style` attribute itself, so the rule is
66
+ * part of the sanitizer rather than a hook a future caller could forget to
67
+ * register.
68
+ * - the `<style>` element being dropped from the `html` attribute, so the
69
+ * `css` attribute below stays the ONLY route to a style block. That was
70
+ * DOMPurify's behaviour and `custom-html-author-css.spec.tsx` pins it;
71
+ * a first pass at AGL-1901 mistook it for a gap and let the tag through,
72
+ * which that spec caught.
73
+ */
74
+ export declare function sanitizeCustomHtml(html: string): string;
75
+ declare const CustomHtml: import("react").ForwardRefExoticComponent<CustomHtmlProps & import("react").RefAttributes<HTMLDivElement>>;
76
+ export declare const schema: Aglyn.ComponentSchema<CustomHtmlProps>;
77
+ export declare const presets: Aglyn.PresetSchema[];
78
+ export default CustomHtml;
@@ -0,0 +1,219 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
+ /**
5
+ * @license
6
+ * Copyright 2026 Aglyn LLC
7
+ *
8
+ * Licensed under the Apache License, Version 2.0 (the "License");
9
+ * you may not use this file except in compliance with the License.
10
+ * You may obtain a copy of the License at
11
+ *
12
+ * http://www.apache.org/licenses/LICENSE-2.0
13
+ *
14
+ * Unless required by applicable law or agreed to in writing, software
15
+ * distributed under the License is distributed on an "AS IS" BASIS,
16
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ * See the License for the specific language governing permissions and
18
+ * limitations under the License.
19
+ */ import * as Aglyn from "@aglyn/aglyn";
20
+ import { mdiCodeTags } from "@aglyn/shared-data-mdi";
21
+ import Box from "@mui/material/Box";
22
+ import Typography from "@mui/material/Typography";
23
+ import { forwardRef, useContext } from "react";
24
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
25
+ import { generatePresetId } from "../utils/generate-preset-id.js";
26
+ // Component ids are persisted in screen documents; never rename.
27
+ export const ID = 'custom-html';
28
+ /**
29
+ * Sanitization policy (AGL-320): an allowlist — no scripts, iframes,
30
+ * objects, or event-handler attributes. Applied at EVERY render (server,
31
+ * client, and besigner canvas), so stored markup can never execute even if a
32
+ * write bypassed the console. Embed mode never touches the DOM — the raw
33
+ * snippet only exists inside `sandbox="allow-scripts"` (no
34
+ * allow-same-origin), so it cannot reach cookies, storage, or the parent
35
+ * page.
36
+ *
37
+ * The engine is `Aglyn.sanitizeAuthorHtml` (AGL-1901), which replaced a
38
+ * per-instance DOMPurify. DOMPurify needs a real DOM, so this component
39
+ * could only sanitize in an effect, and every Custom HTML block was
40
+ * therefore absent from the server response — the SSR/SEO half AGL-1268 left
41
+ * open. The new sanitizer is a pure function of the string, so the same call
42
+ * runs on the server and in the browser and both produce the same bytes.
43
+ *
44
+ * What carried over unchanged:
45
+ *
46
+ * - the forbidden set (`script`, `iframe`, `object`, `embed`, `form`,
47
+ * `base`), `srcdoc`/`formaction`, and `ALLOW_DATA_ATTR: false`;
48
+ * - AGL-1725's `style`-attribute rule. DOMPurify performs NO CSS filtering,
49
+ * which is why that hook existed at all; `sanitizeAuthorHtml` applies
50
+ * `sanitizeAuthorCss` to every `style` attribute itself, so the rule is
51
+ * part of the sanitizer rather than a hook a future caller could forget to
52
+ * register.
53
+ * - the `<style>` element being dropped from the `html` attribute, so the
54
+ * `css` attribute below stays the ONLY route to a style block. That was
55
+ * DOMPurify's behaviour and `custom-html-author-css.spec.tsx` pins it;
56
+ * a first pass at AGL-1901 mistook it for a gap and let the tag through,
57
+ * which that spec caught.
58
+ */ export function sanitizeCustomHtml(html) {
59
+ return Aglyn.sanitizeAuthorHtml(html);
60
+ }
61
+ const CustomHtml = /*#__PURE__*/ forwardRef((props, ref)=>{
62
+ const { html, css, embedMode, embedHeight } = props, rest = _object_without_properties_loose(props, [
63
+ "html",
64
+ "css",
65
+ "embedMode",
66
+ "embedHeight"
67
+ ]);
68
+ // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).
69
+ const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [
70
+ props['sx']
71
+ ];
72
+ const { hostId } = Aglyn.useSite();
73
+ const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext);
74
+ // Same shape as typography's rich text, and fixed the same way
75
+ // (AGL-1901): sanitized DURING render rather than in an effect. The
76
+ // effect existed because DOMPurify needs a DOM — the server rendered
77
+ // nothing, so the first client render had to render nothing too or
78
+ // hydration would mismatch (AGL-1268). `sanitizeCustomHtml` is now a
79
+ // pure function of the string, so both sides compute the same markup and
80
+ // the block is in the server response where a crawler can read it.
81
+ const sanitized = !html || embedMode ? '' : sanitizeCustomHtml(html);
82
+ if (!(html == null ? void 0 : html.trim())) {
83
+ // The hint is for the author, so only editing surfaces draw it; a
84
+ // published page renders the bare element.
85
+ if (!suppressNavigation) return /*#__PURE__*/ _jsx(Box, _extends({
86
+ ref: ref
87
+ }, rest));
88
+ return /*#__PURE__*/ _jsx(Box, _extends({
89
+ ref: ref
90
+ }, rest, {
91
+ // MERGE, never replace (AGL-1284): the placeholder's own look goes
92
+ // first so an author who styles the empty state can override it.
93
+ sx: [
94
+ {
95
+ p: 3,
96
+ border: '1px dashed',
97
+ borderColor: 'divider',
98
+ borderRadius: 1,
99
+ color: 'text.secondary',
100
+ fontSize: 13,
101
+ fontFamily: 'system-ui, sans-serif'
102
+ },
103
+ ...nodeSx
104
+ ],
105
+ children: '</> Custom HTML — add markup in the attributes panel'
106
+ }));
107
+ }
108
+ if (embedMode) {
109
+ return /*#__PURE__*/ _jsxs(Box, _extends({
110
+ ref: ref
111
+ }, rest, {
112
+ children: [
113
+ !hostId ? /*#__PURE__*/ _jsx(Typography, {
114
+ variant: "caption",
115
+ color: "text.secondary",
116
+ children: 'Embed (sandboxed iframe) — runs on the live site'
117
+ }) : null,
118
+ /*#__PURE__*/ _jsx("iframe", {
119
+ title: "Embedded content",
120
+ sandbox: "allow-scripts allow-popups",
121
+ srcDoc: html,
122
+ style: {
123
+ width: '100%',
124
+ height: `${Math.max(40, embedHeight != null ? embedHeight : 320)}px`,
125
+ border: 0
126
+ }
127
+ })
128
+ ]
129
+ }));
130
+ }
131
+ return /*#__PURE__*/ _jsxs(Box, _extends({
132
+ ref: ref
133
+ }, rest, {
134
+ children: [
135
+ css ? /*#__PURE__*/ _jsx("style", {
136
+ children: Aglyn.sanitizeAuthorCss(css)
137
+ }) : null,
138
+ /*#__PURE__*/ _jsx("div", {
139
+ dangerouslySetInnerHTML: {
140
+ __html: sanitized
141
+ }
142
+ })
143
+ ]
144
+ }));
145
+ });
146
+ CustomHtml.displayName = 'AglynCustomHtml';
147
+ export const schema = {
148
+ $id: ID,
149
+ pluginId: BUNDLE_ID,
150
+ displayName: 'Custom HTML',
151
+ description: 'Your own markup, sanitised on render, or isolated in a sandboxed iframe.',
152
+ category: Aglyn.ComponentCategory.DATA_DISPLAY,
153
+ icon: {
154
+ path: mdiCodeTags.path,
155
+ sx: {
156
+ color: '#7b1fa2'
157
+ }
158
+ },
159
+ flags: {
160
+ selfClosing: Aglyn.FEATURE_FLAG.ENABLED
161
+ },
162
+ attributes: [
163
+ {
164
+ name: 'html',
165
+ label: 'HTML',
166
+ description: 'Sanitized on every render: scripts, iframes, and on* handlers ' + 'are stripped. Use Embed mode for third-party widgets.',
167
+ component: Aglyn.FieldComponentType.TEXT_FIELD
168
+ },
169
+ {
170
+ name: 'css',
171
+ label: 'CSS',
172
+ // AGL-1725 — the "warn" half of warn-and-disclose. An author can only
173
+ // weigh this egress if they are told it exists, and the attributes
174
+ // panel is where they are typing the url().
175
+ description: 'Injected as a style tag — keep selectors specific. A url() ' + 'pointing off your site makes every visitor’s browser contact that ' + 'host, which sees their IP address and which page they are on. ' + 'Insecure http:// URLs are not loaded.',
176
+ component: Aglyn.FieldComponentType.TEXT_FIELD
177
+ },
178
+ {
179
+ name: 'embedMode',
180
+ label: 'Embed mode (sandboxed)',
181
+ description: 'Runs the raw snippet in a sandboxed iframe (scripts allowed, no ' + 'access to your site or visitor data). For third-party widgets.',
182
+ component: Aglyn.FieldComponentType.CHECKBOX
183
+ },
184
+ {
185
+ name: 'embedHeight',
186
+ label: 'Embed height (px)',
187
+ description: 'Iframe height in embed mode (default 320).',
188
+ component: Aglyn.FieldComponentType.TEXT_FIELD,
189
+ type: 'number'
190
+ }
191
+ ]
192
+ };
193
+ export const presets = [
194
+ {
195
+ $id: generatePresetId(ID),
196
+ type: Aglyn.NodeType.PRESET,
197
+ displayName: 'Custom HTML',
198
+ pluginId: BUNDLE_ID,
199
+ description: 'Sanitized markup block, with a sandboxed embed mode',
200
+ category: Aglyn.ComponentCategory.DATA_DISPLAY,
201
+ icon: {
202
+ path: mdiCodeTags.path,
203
+ sx: {
204
+ color: '#7b1fa2'
205
+ }
206
+ },
207
+ data: {
208
+ $id: null,
209
+ componentId: ID,
210
+ pluginId: BUNDLE_ID,
211
+ props: {
212
+ html: '<p>Hello from custom HTML</p>'
213
+ }
214
+ }
215
+ }
216
+ ];
217
+ export default CustomHtml;
218
+
219
+ //# sourceMappingURL=custom-html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/custom-html.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiCodeTags } from '@aglyn/shared-data-mdi'\nimport Box from '@mui/material/Box'\nimport type { SxProps } from '@mui/material/styles'\nimport Typography from '@mui/material/Typography'\nimport { forwardRef, useContext } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n// Component ids are persisted in screen documents; never rename.\nexport const ID: Aglyn.ComponentId = 'custom-html'\n\nexport interface CustomHtmlProps {\n /** Author-provided markup, sanitized before every render. */\n html?: string\n /** Scoped styles injected as a <style> tag (selectors are the\n * author's responsibility; keep them specific). */\n css?: string\n /**\n * Embed mode (AGL-320): renders the RAW snippet inside a sandboxed\n * iframe (scripts allowed, same-origin denied) for third-party\n * widgets. Business-plan gated by the contentGating entitlement's\n * sibling `commerce` checks server-side; the besigner surfaces the\n * gate in the attribute description.\n */\n embedMode?: boolean\n /** Iframe height (px) in embed mode. */\n embedHeight?: number\n /**\n * Authored node styles, handed over by the renderer rather than typed\n * into an attribute — recomposed below so the Styles panel's values are\n * merged rather than replaced (AGL-1284).\n */\n sx?: SxProps\n}\n\n/**\n * Sanitization policy (AGL-320): an allowlist — no scripts, iframes,\n * objects, or event-handler attributes. Applied at EVERY render (server,\n * client, and besigner canvas), so stored markup can never execute even if a\n * write bypassed the console. Embed mode never touches the DOM — the raw\n * snippet only exists inside `sandbox=\"allow-scripts\"` (no\n * allow-same-origin), so it cannot reach cookies, storage, or the parent\n * page.\n *\n * The engine is `Aglyn.sanitizeAuthorHtml` (AGL-1901), which replaced a\n * per-instance DOMPurify. DOMPurify needs a real DOM, so this component\n * could only sanitize in an effect, and every Custom HTML block was\n * therefore absent from the server response — the SSR/SEO half AGL-1268 left\n * open. The new sanitizer is a pure function of the string, so the same call\n * runs on the server and in the browser and both produce the same bytes.\n *\n * What carried over unchanged:\n *\n * - the forbidden set (`script`, `iframe`, `object`, `embed`, `form`,\n * `base`), `srcdoc`/`formaction`, and `ALLOW_DATA_ATTR: false`;\n * - AGL-1725's `style`-attribute rule. DOMPurify performs NO CSS filtering,\n * which is why that hook existed at all; `sanitizeAuthorHtml` applies\n * `sanitizeAuthorCss` to every `style` attribute itself, so the rule is\n * part of the sanitizer rather than a hook a future caller could forget to\n * register.\n * - the `<style>` element being dropped from the `html` attribute, so the\n * `css` attribute below stays the ONLY route to a style block. That was\n * DOMPurify's behaviour and `custom-html-author-css.spec.tsx` pins it;\n * a first pass at AGL-1901 mistook it for a gap and let the tag through,\n * which that spec caught.\n */\nexport function sanitizeCustomHtml(html: string): string {\n return Aglyn.sanitizeAuthorHtml(html)\n}\n\nconst CustomHtml = forwardRef<HTMLDivElement, CustomHtmlProps>(\n (props, ref) => {\n const { html, css, embedMode, embedHeight, ...rest } = props\n // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).\n const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [props['sx']]\n const { hostId } = Aglyn.useSite()\n const { suppressNavigation } = useContext(Aglyn.ScreenLinkContext)\n\n // Same shape as typography's rich text, and fixed the same way\n // (AGL-1901): sanitized DURING render rather than in an effect. The\n // effect existed because DOMPurify needs a DOM — the server rendered\n // nothing, so the first client render had to render nothing too or\n // hydration would mismatch (AGL-1268). `sanitizeCustomHtml` is now a\n // pure function of the string, so both sides compute the same markup and\n // the block is in the server response where a crawler can read it.\n const sanitized = !html || embedMode ? '' : sanitizeCustomHtml(html)\n\n if (!html?.trim()) {\n // The hint is for the author, so only editing surfaces draw it; a\n // published page renders the bare element.\n if (!suppressNavigation) return <Box ref={ref} {...rest} />\n return (\n <Box\n ref={ref}\n {...rest}\n // MERGE, never replace (AGL-1284): the placeholder's own look goes\n // first so an author who styles the empty state can override it.\n sx={[\n {\n p: 3,\n border: '1px dashed',\n borderColor: 'divider',\n borderRadius: 1,\n color: 'text.secondary',\n fontSize: 13,\n fontFamily: 'system-ui, sans-serif',\n },\n ...nodeSx,\n ]}\n >\n {'</> Custom HTML — add markup in the attributes panel'}\n </Box>\n )\n }\n\n if (embedMode) {\n return (\n <Box ref={ref} {...rest}>\n {!hostId ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Embed (sandboxed iframe) — runs on the live site'}\n </Typography>\n ) : null}\n <iframe\n title=\"Embedded content\"\n sandbox=\"allow-scripts allow-popups\"\n srcDoc={html}\n style={{\n width: '100%',\n height: `${Math.max(40, embedHeight ?? 320)}px`,\n border: 0,\n }}\n />\n </Box>\n )\n }\n\n return (\n <Box ref={ref} {...rest}>\n {/* AGL-1725: the `css` attribute never reached DOMPurify — that\n policy covers `html` only — so this <style> block was the one\n surface on a published site where an author's raw CSS shipped to\n every visitor unexamined. `sanitizeAuthorCss` refuses schemes\n that cannot be defended (http:, and anything not https/data/blob)\n and deliberately leaves the HOST alone; see the module header on\n why AGL-1701's input restriction does not transfer to a site\n owner styling their own site. */}\n {css ? <style>{Aglyn.sanitizeAuthorCss(css)}</style> : null}\n {/* Sanitized above on every render — see sanitizeCustomHtml. */}\n <div dangerouslySetInnerHTML={{ __html: sanitized }} />\n </Box>\n )\n },\n)\nCustomHtml.displayName = 'AglynCustomHtml'\n\nexport const schema: Aglyn.ComponentSchema<CustomHtmlProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Custom HTML',\n description:\n 'Your own markup, sanitised on render, or isolated in a sandboxed iframe.',\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: { path: mdiCodeTags.path, sx: { color: '#7b1fa2' } },\n flags: { selfClosing: Aglyn.FEATURE_FLAG.ENABLED },\n attributes: [\n {\n name: 'html',\n label: 'HTML',\n description:\n 'Sanitized on every render: scripts, iframes, and on* handlers ' +\n 'are stripped. Use Embed mode for third-party widgets.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'css',\n label: 'CSS',\n // AGL-1725 — the \"warn\" half of warn-and-disclose. An author can only\n // weigh this egress if they are told it exists, and the attributes\n // panel is where they are typing the url().\n description:\n 'Injected as a style tag — keep selectors specific. A url() ' +\n 'pointing off your site makes every visitor’s browser contact that ' +\n 'host, which sees their IP address and which page they are on. ' +\n 'Insecure http:// URLs are not loaded.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n },\n {\n name: 'embedMode',\n label: 'Embed mode (sandboxed)',\n description:\n 'Runs the raw snippet in a sandboxed iframe (scripts allowed, no ' +\n 'access to your site or visitor data). For third-party widgets.',\n component: Aglyn.FieldComponentType.CHECKBOX,\n },\n {\n name: 'embedHeight',\n label: 'Embed height (px)',\n description: 'Iframe height in embed mode (default 320).',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n type: 'number',\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Custom HTML',\n pluginId: BUNDLE_ID,\n description: 'Sanitized markup block, with a sandboxed embed mode',\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: { path: mdiCodeTags.path, sx: { color: '#7b1fa2' } },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: { html: '<p>Hello from custom HTML</p>' },\n },\n },\n]\n\nexport default CustomHtml\n"],"names":["Aglyn","mdiCodeTags","Box","Typography","forwardRef","useContext","BUNDLE_ID","generatePresetId","ID","sanitizeCustomHtml","html","sanitizeAuthorHtml","CustomHtml","props","ref","css","embedMode","embedHeight","rest","nodeSx","Array","isArray","hostId","useSite","suppressNavigation","ScreenLinkContext","sanitized","trim","sx","p","border","borderColor","borderRadius","color","fontSize","fontFamily","variant","iframe","title","sandbox","srcDoc","style","width","height","Math","max","sanitizeAuthorCss","div","dangerouslySetInnerHTML","__html","displayName","schema","$id","pluginId","description","category","ComponentCategory","DATA_DISPLAY","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","name","label","component","FieldComponentType","TEXT_FIELD","CHECKBOX","type","presets","NodeType","PRESET","data","componentId"],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import * as Aglyn from '@aglyn/aglyn';
18
+ export declare const DATA_TABLE_ID: Aglyn.ComponentId;
19
+ export interface DataTableProps {
20
+ /**
21
+ * The grid, pipe-delimited, one row per line — and its column alignment,
22
+ * as a markdown divider row. See `@aglyn/shared-data-enums`'s `data-table` utilities.
23
+ */
24
+ rows?: string;
25
+ /** Render the first row as a header. */
26
+ headerRow?: boolean;
27
+ /** 1-based column to emphasise, or 0/absent for none. */
28
+ emphasizeColumn?: number | string;
29
+ }
30
+ /**
31
+ * A comparison table (AGL-2543).
32
+ *
33
+ * The feature matrix is the load-bearing block on a competitor comparison
34
+ * page, and the palette had no way to build one: searching `table` returned
35
+ * **Table of Contents** and **Pricing Table**, neither of which is a general
36
+ * grid. The only route was to author markdown pipe syntax inside a Markdown
37
+ * element — which renders correctly and is exactly the audience the besigner
38
+ * exists to spare.
39
+ *
40
+ * Three things the Markdown route could not give, and the reason this is an
41
+ * element rather than a docs note:
42
+ *
43
+ * - **Header emphasis**, without asking an author to remember that the first
44
+ * row is special.
45
+ * - **A highlighted column** — the "ours" column on a comparison page is the
46
+ * entire point of the block, and markdown has no way to say it.
47
+ * - **A grid to edit**, rather than pipe syntax in a text box — see the
48
+ * `DATA_TABLE` field this element's `rows` attribute asks for.
49
+ *
50
+ * Narrow-screen behaviour is NOT one of them (AGL-2568). Both routes draw the
51
+ * same block, so both take the same treatment from `scroll-overflow`: the
52
+ * grid scrolls inside its own box once it is too wide to read, and says so.
53
+ */
54
+ declare const DataTable: import("react").ForwardRefExoticComponent<DataTableProps & import("react").RefAttributes<HTMLDivElement>>;
55
+ export default DataTable;
56
+ export declare const dataTableSchema: Aglyn.ComponentSchema<DataTableProps>;
57
+ export declare const dataTablePresets: Aglyn.PresetSchema[];