@supersoniks/concorde 5.0.5 → 5.1.1

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 (282) hide show
  1. package/.claude/launch.json +15 -0
  2. package/.claude/vite.http.config.mjs +7 -0
  3. package/.taste-skill/after/desktop-fold.jpg +0 -0
  4. package/.taste-skill/after/desktop-page-01.jpg +0 -0
  5. package/.taste-skill/after/desktop-page-02.jpg +0 -0
  6. package/.taste-skill/after/desktop-page-03.jpg +0 -0
  7. package/.taste-skill/after/mobile-fold.jpg +0 -0
  8. package/.taste-skill/after/mobile-page-01.jpg +0 -0
  9. package/.taste-skill/after/report.json +4103 -0
  10. package/.taste-skill/after/tablet-fold.jpg +0 -0
  11. package/.taste-skill/after/tablet-page-01.jpg +0 -0
  12. package/.taste-skill/after/tablet-page-02.jpg +0 -0
  13. package/.taste-skill/before/desktop-fold.jpg +0 -0
  14. package/.taste-skill/before/desktop-page-01.jpg +0 -0
  15. package/.taste-skill/before/desktop-page-02.jpg +0 -0
  16. package/.taste-skill/before/mobile-fold.jpg +0 -0
  17. package/.taste-skill/before/mobile-page-01.jpg +0 -0
  18. package/.taste-skill/before/report.json +2556 -0
  19. package/.taste-skill/before/tablet-fold.jpg +0 -0
  20. package/.taste-skill/before/tablet-page-01.jpg +0 -0
  21. package/.taste-skill/direction.json +510 -0
  22. package/.taste-skill/notes.md +84 -0
  23. package/.taste-skill/prod/desktop-fold.jpg +0 -0
  24. package/.taste-skill/prod/desktop-page-01.jpg +0 -0
  25. package/.taste-skill/prod/desktop-page-02.jpg +0 -0
  26. package/.taste-skill/prod/desktop-page-03.jpg +0 -0
  27. package/.taste-skill/prod/mobile-fold.jpg +0 -0
  28. package/.taste-skill/prod/mobile-page-01.jpg +0 -0
  29. package/.taste-skill/prod/mobile-page-02.jpg +0 -0
  30. package/.taste-skill/prod/report.json +1973 -0
  31. package/.taste-skill/prod/tablet-fold.jpg +0 -0
  32. package/.taste-skill/prod/tablet-page-01.jpg +0 -0
  33. package/.taste-skill/prod/tablet-page-02.jpg +0 -0
  34. package/.tmp-cem/custom-elements.json +13705 -0
  35. package/AUDIT.md +228 -0
  36. package/PLAN-AMELIORATION.md +220 -0
  37. package/build-infos.json +1 -1
  38. package/concorde-core.bundle.js +196 -196
  39. package/concorde-core.es.js +3854 -3572
  40. package/custom-elements-manifest.config.mjs +14 -0
  41. package/dist/concorde-core.bundle.js +196 -196
  42. package/dist/concorde-core.es.js +3854 -3572
  43. package/dist/img/archive/concorde-001-2cv.webp +0 -0
  44. package/dist/img/archive/concorde-red-arrows-2002-poster.jpg +0 -0
  45. package/dist/img/archive/concorde-red-arrows-2002.webm +0 -0
  46. package/dist/img/archive/credits.json +28 -0
  47. package/docs/assets/index-Bb-_LRyY.js +12280 -0
  48. package/docs/index.html +1 -1
  49. package/index.html +27 -63
  50. package/package.json +44 -3
  51. package/public/img/archive/concorde-001-2cv.webp +0 -0
  52. package/public/img/archive/concorde-red-arrows-2002-poster.jpg +0 -0
  53. package/public/img/archive/concorde-red-arrows-2002.webm +0 -0
  54. package/public/img/archive/credits.json +28 -0
  55. package/scripts/generate-sdui-catalog.mjs +250 -0
  56. package/scripts/generate-sdui-catalog.spec.ts +91 -0
  57. package/scripts/pre-build.mjs +15 -0
  58. package/src/core/components/functional/fetch/fetch.md +2 -2
  59. package/src/core/components/functional/sdui/SDUIDescriptorTransformer.ts +5 -5
  60. package/src/core/components/functional/sdui/__fixtures__/descriptors.ts +251 -0
  61. package/src/core/components/functional/sdui/__snapshots__/sdui-action.spec.ts.snap +73 -0
  62. package/src/core/components/functional/sdui/__snapshots__/sdui-incremental.spec.ts.snap +50 -0
  63. package/src/core/components/functional/sdui/__snapshots__/sdui-partial.spec.ts.snap +50 -0
  64. package/src/core/components/functional/sdui/__snapshots__/sdui.regression.spec.ts.snap +461 -0
  65. package/src/core/components/functional/sdui/component-catalog.gaps.json +511 -0
  66. package/src/core/components/functional/sdui/component-catalog.json +3103 -0
  67. package/src/core/components/functional/sdui/component-catalog.spec.ts +23 -0
  68. package/src/core/components/functional/sdui/sdui-action.spec.ts +99 -0
  69. package/src/core/components/functional/sdui/sdui-dom-snapshot.ts +61 -0
  70. package/src/core/components/functional/sdui/sdui-extension-points.md +94 -0
  71. package/src/core/components/functional/sdui/sdui-incremental.spec.ts +137 -0
  72. package/src/core/components/functional/sdui/sdui-partial.spec.ts +83 -0
  73. package/src/core/components/functional/sdui/sdui-partial.ts +19 -0
  74. package/src/core/components/functional/sdui/sdui-safe.spec.ts +188 -0
  75. package/src/core/components/functional/sdui/sdui-safe.ts +100 -0
  76. package/src/core/components/functional/sdui/sdui.md +122 -0
  77. package/src/core/components/functional/sdui/sdui.regression.spec.ts +64 -0
  78. package/src/core/components/functional/sdui/sdui.ts +442 -34
  79. package/src/core/components/functional/sdui/types.ts +49 -0
  80. package/src/core/components/ui/modal/modal.ts +14 -11
  81. package/src/docs/_getting-started/components.md +5 -0
  82. package/src/docs/_getting-started/theming.md +2 -2
  83. package/src/docs/code.ts +88 -75
  84. package/src/docs/components/docs-demo-sources.ts +16 -8
  85. package/src/docs/components/docs-lit-demo.ts +19 -37
  86. package/src/docs/docs.ts +5 -0
  87. package/src/docs/example/docs-boarding-pass-demo.ts +134 -0
  88. package/src/docs/example/docs-provider-keys.ts +12 -0
  89. package/src/docs/header/header.ts +233 -69
  90. package/src/docs/home/catalog.ts +44 -0
  91. package/src/docs/home/home.ts +513 -0
  92. package/src/docs/home/mach-stage.ts +389 -0
  93. package/src/docs/layout.ts +3 -1
  94. package/src/docs/navigation/components-index.ts +56 -0
  95. package/src/docs/navigation/navigation.ts +240 -232
  96. package/src/docs/navigation/pager.ts +67 -0
  97. package/src/docs/navigation/toc.ts +98 -0
  98. package/src/docs/search/docs-search.json +330 -20
  99. package/src/docs/search/markdown-renderer.ts +11 -3
  100. package/src/docs/shiki/index.ts +109 -0
  101. package/src/docs/tailwind/css/base/scrollbar.css +22 -0
  102. package/src/docs/tailwind/css/base/theme.css +96 -0
  103. package/src/docs/tailwind/css/base/typography.css +4 -0
  104. package/src/docs/tailwind/css/docs/boarding-pass.css +65 -0
  105. package/src/docs/tailwind/css/docs/content.css +32 -0
  106. package/src/docs/tailwind/css/docs/page.css +10 -0
  107. package/src/docs/tailwind/css/docs.css +9 -0
  108. package/src/docs/tailwind/css/home/hero.css +210 -0
  109. package/src/docs/tailwind/css/home/motion.css +84 -0
  110. package/src/docs/tailwind/css/home/sections.css +416 -0
  111. package/src/docs/tailwind/css/tailwind.css +7 -185
  112. package/src/tsconfig.json +113 -2
  113. package/src/tsconfig.tsbuildinfo +1 -1
  114. package/tailwind.config.js +8 -4
  115. package/docs/assets/index-wVZpLVYW.js +0 -12122
  116. package/docs/crawl/core/components/functional/date/date.html +0 -190
  117. package/docs/crawl/core/components/functional/fetch/fetch.html +0 -109
  118. package/docs/crawl/core/components/functional/if/if.html +0 -27
  119. package/docs/crawl/core/components/functional/jsonata/jsonata.html +0 -79
  120. package/docs/crawl/core/components/functional/list/list.html +0 -70
  121. package/docs/crawl/core/components/functional/mix/mix.html +0 -45
  122. package/docs/crawl/core/components/functional/queue/queue.html +0 -96
  123. package/docs/crawl/core/components/functional/router/router.html +0 -106
  124. package/docs/crawl/core/components/functional/sdui/sdui.html +0 -269
  125. package/docs/crawl/core/components/functional/states/states.html +0 -66
  126. package/docs/crawl/core/components/functional/submit/submit.html +0 -160
  127. package/docs/crawl/core/components/functional/subscriber/subscriber.html +0 -67
  128. package/docs/crawl/core/components/functional/value/value.html +0 -51
  129. package/docs/crawl/core/components/ui/alert/alert.html +0 -211
  130. package/docs/crawl/core/components/ui/alert-messages/alert-messages.html +0 -131
  131. package/docs/crawl/core/components/ui/badge/badge.html +0 -197
  132. package/docs/crawl/core/components/ui/button/button.html +0 -150
  133. package/docs/crawl/core/components/ui/captcha/captcha.html +0 -112
  134. package/docs/crawl/core/components/ui/card/card.html +0 -253
  135. package/docs/crawl/core/components/ui/divider/divider.html +0 -144
  136. package/docs/crawl/core/components/ui/form/checkbox/checkbox.html +0 -73
  137. package/docs/crawl/core/components/ui/form/fieldset/fieldset.html +0 -266
  138. package/docs/crawl/core/components/ui/form/form-actions/form-actions.html +0 -122
  139. package/docs/crawl/core/components/ui/form/form-layout/form-layout.html +0 -121
  140. package/docs/crawl/core/components/ui/form/input/input.html +0 -109
  141. package/docs/crawl/core/components/ui/form/input-autocomplete/input-autocomplete.html +0 -38
  142. package/docs/crawl/core/components/ui/form/radio/radio.html +0 -56
  143. package/docs/crawl/core/components/ui/form/select/select.html +0 -73
  144. package/docs/crawl/core/components/ui/form/switch/switch.html +0 -56
  145. package/docs/crawl/core/components/ui/form/textarea/textarea.html +0 -59
  146. package/docs/crawl/core/components/ui/group/group.html +0 -104
  147. package/docs/crawl/core/components/ui/icon/icon.html +0 -222
  148. package/docs/crawl/core/components/ui/image/image.html +0 -298
  149. package/docs/crawl/core/components/ui/link/link.html +0 -100
  150. package/docs/crawl/core/components/ui/loader/loader.html +0 -124
  151. package/docs/crawl/core/components/ui/menu/menu.html +0 -394
  152. package/docs/crawl/core/components/ui/modal/modal.html +0 -342
  153. package/docs/crawl/core/components/ui/pop/pop.html +0 -296
  154. package/docs/crawl/core/components/ui/progress/progress.html +0 -131
  155. package/docs/crawl/core/components/ui/table/table.html +0 -545
  156. package/docs/crawl/core/components/ui/theme/theme.html +0 -131
  157. package/docs/crawl/core/components/ui/toast/toast.html +0 -233
  158. package/docs/crawl/core/components/ui/tooltip/tooltip.html +0 -140
  159. package/docs/crawl/docs/_core-concept/dataFlow.html +0 -177
  160. package/docs/crawl/docs/_core-concept/overview.html +0 -66
  161. package/docs/crawl/docs/_core-concept/subscriber.html +0 -75
  162. package/docs/crawl/docs/_decorators/ancestor-attribute.html +0 -117
  163. package/docs/crawl/docs/_decorators/auto-subscribe.html +0 -202
  164. package/docs/crawl/docs/_decorators/bind.html +0 -141
  165. package/docs/crawl/docs/_decorators/get.html +0 -149
  166. package/docs/crawl/docs/_decorators/handle.html +0 -165
  167. package/docs/crawl/docs/_decorators/on-assign.html +0 -355
  168. package/docs/crawl/docs/_decorators/patch.html +0 -78
  169. package/docs/crawl/docs/_decorators/post.html +0 -139
  170. package/docs/crawl/docs/_decorators/publish.html +0 -61
  171. package/docs/crawl/docs/_decorators/put.html +0 -78
  172. package/docs/crawl/docs/_decorators/subscribe.html +0 -109
  173. package/docs/crawl/docs/_decorators/wait-for-ancestors.html +0 -137
  174. package/docs/crawl/docs/_directives/sub.html +0 -93
  175. package/docs/crawl/docs/_getting-started/ai-agents.html +0 -173
  176. package/docs/crawl/docs/_getting-started/concorde-manual-install.html +0 -100
  177. package/docs/crawl/docs/_getting-started/concorde-outside.html +0 -58
  178. package/docs/crawl/docs/_getting-started/create-a-component.html +0 -111
  179. package/docs/crawl/docs/_getting-started/my-first-component.html +0 -189
  180. package/docs/crawl/docs/_getting-started/my-first-subscriber.html +0 -110
  181. package/docs/crawl/docs/_getting-started/pubsub.html +0 -62
  182. package/docs/crawl/docs/_getting-started/start.html +0 -76
  183. package/docs/crawl/docs/_getting-started/theming.html +0 -333
  184. package/docs/crawl/docs/_misc/api-configuration.html +0 -149
  185. package/docs/crawl/docs/_misc/dataProviderKey.html +0 -135
  186. package/docs/crawl/docs/_misc/docs-mock-api.html +0 -158
  187. package/docs/crawl/docs/_misc/dynamic-path.html +0 -180
  188. package/docs/crawl/docs/_misc/endpoint.html +0 -50
  189. package/docs/crawl/docs/_misc/formelement-subscriber-removal.html +0 -138
  190. package/docs/crawl/docs/_misc/html-integration.html +0 -48
  191. package/docs/crawl/hubs/decorators.html +0 -547
  192. package/docs/crawl/hubs/form-components.html +0 -381
  193. package/docs/crawl/hubs/remote-data.html +0 -452
  194. package/docs/crawl/index.html +0 -102
  195. package/docs/llms-full.txt +0 -7008
  196. package/docs/llms.txt +0 -97
  197. package/docs/sitemap.xml +0 -240
  198. package/docs/src/core/components/functional/date/date.md +0 -290
  199. package/docs/src/core/components/functional/fetch/fetch.md +0 -125
  200. package/docs/src/core/components/functional/if/if.md +0 -9
  201. package/docs/src/core/components/functional/jsonata/jsonata.md +0 -56
  202. package/docs/src/core/components/functional/list/list.md +0 -65
  203. package/docs/src/core/components/functional/mix/mix.md +0 -41
  204. package/docs/src/core/components/functional/queue/queue.md +0 -72
  205. package/docs/src/core/components/functional/router/router.md +0 -94
  206. package/docs/src/core/components/functional/sdui/default-library.json +0 -108
  207. package/docs/src/core/components/functional/sdui/example.json +0 -99
  208. package/docs/src/core/components/functional/sdui/sdui.md +0 -356
  209. package/docs/src/core/components/functional/states/states.md +0 -87
  210. package/docs/src/core/components/functional/submit/submit.md +0 -114
  211. package/docs/src/core/components/functional/subscriber/subscriber.md +0 -91
  212. package/docs/src/core/components/functional/value/value.md +0 -35
  213. package/docs/src/core/components/ui/alert/alert.md +0 -196
  214. package/docs/src/core/components/ui/alert-messages/alert-messages.md +0 -85
  215. package/docs/src/core/components/ui/badge/badge.md +0 -188
  216. package/docs/src/core/components/ui/button/button.md +0 -183
  217. package/docs/src/core/components/ui/captcha/captcha.md +0 -71
  218. package/docs/src/core/components/ui/card/card.md +0 -215
  219. package/docs/src/core/components/ui/divider/divider.md +0 -129
  220. package/docs/src/core/components/ui/form/checkbox/checkbox.md +0 -77
  221. package/docs/src/core/components/ui/form/fieldset/fieldset.md +0 -205
  222. package/docs/src/core/components/ui/form/form-actions/form-actions.md +0 -100
  223. package/docs/src/core/components/ui/form/form-layout/form-layout.md +0 -119
  224. package/docs/src/core/components/ui/form/input/input.md +0 -142
  225. package/docs/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +0 -29
  226. package/docs/src/core/components/ui/form/radio/radio.md +0 -57
  227. package/docs/src/core/components/ui/form/select/select.md +0 -71
  228. package/docs/src/core/components/ui/form/switch/switch.md +0 -57
  229. package/docs/src/core/components/ui/form/textarea/textarea.md +0 -65
  230. package/docs/src/core/components/ui/group/group.md +0 -75
  231. package/docs/src/core/components/ui/icon/icon.md +0 -214
  232. package/docs/src/core/components/ui/icon/icons.json +0 -1
  233. package/docs/src/core/components/ui/image/image.md +0 -233
  234. package/docs/src/core/components/ui/link/link.md +0 -81
  235. package/docs/src/core/components/ui/loader/loader.md +0 -112
  236. package/docs/src/core/components/ui/menu/menu.md +0 -370
  237. package/docs/src/core/components/ui/modal/modal.md +0 -235
  238. package/docs/src/core/components/ui/pop/pop.md +0 -294
  239. package/docs/src/core/components/ui/progress/progress.md +0 -109
  240. package/docs/src/core/components/ui/table/table.md +0 -431
  241. package/docs/src/core/components/ui/theme/theme.md +0 -122
  242. package/docs/src/core/components/ui/toast/toast.md +0 -216
  243. package/docs/src/core/components/ui/tooltip/tooltip.md +0 -124
  244. package/docs/src/docs/_core-concept/dataFlow.md +0 -88
  245. package/docs/src/docs/_core-concept/overview.md +0 -57
  246. package/docs/src/docs/_core-concept/subscriber.md +0 -75
  247. package/docs/src/docs/_decorators/ancestor-attribute.md +0 -128
  248. package/docs/src/docs/_decorators/auto-subscribe.md +0 -202
  249. package/docs/src/docs/_decorators/bind.md +0 -167
  250. package/docs/src/docs/_decorators/get.md +0 -162
  251. package/docs/src/docs/_decorators/handle.md +0 -175
  252. package/docs/src/docs/_decorators/on-assign.md +0 -390
  253. package/docs/src/docs/_decorators/patch.md +0 -70
  254. package/docs/src/docs/_decorators/post.md +0 -146
  255. package/docs/src/docs/_decorators/publish.md +0 -55
  256. package/docs/src/docs/_decorators/put.md +0 -70
  257. package/docs/src/docs/_decorators/subscribe.md +0 -100
  258. package/docs/src/docs/_decorators/wait-for-ancestors.md +0 -163
  259. package/docs/src/docs/_directives/sub.md +0 -91
  260. package/docs/src/docs/_getting-started/ai-agents.md +0 -76
  261. package/docs/src/docs/_getting-started/concorde-manual-install.md +0 -133
  262. package/docs/src/docs/_getting-started/concorde-outside.md +0 -33
  263. package/docs/src/docs/_getting-started/create-a-component.md +0 -139
  264. package/docs/src/docs/_getting-started/my-first-component.md +0 -236
  265. package/docs/src/docs/_getting-started/my-first-subscriber.md +0 -120
  266. package/docs/src/docs/_getting-started/pubsub.md +0 -37
  267. package/docs/src/docs/_getting-started/start.md +0 -47
  268. package/docs/src/docs/_getting-started/theming.md +0 -253
  269. package/docs/src/docs/_misc/api-configuration.md +0 -81
  270. package/docs/src/docs/_misc/dataProviderKey.md +0 -168
  271. package/docs/src/docs/_misc/docs-mock-api.md +0 -63
  272. package/docs/src/docs/_misc/dynamic-path.md +0 -71
  273. package/docs/src/docs/_misc/endpoint.md +0 -45
  274. package/docs/src/docs/_misc/formelement-subscriber-removal.md +0 -115
  275. package/docs/src/docs/_misc/html-integration.md +0 -15
  276. package/docs/src/docs/search/docs-search.json +0 -12242
  277. package/docs/src/tag-list.json +0 -1
  278. package/docs/src/tsconfig-model.json +0 -23
  279. package/docs/src/tsconfig.json +0 -1242
  280. package/src/docs/prism/index.ts +0 -6
  281. package/src/docs/prism/prism.css +0 -158
  282. package/src/docs/prism/prism.js +0 -1022
@@ -0,0 +1,188 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import TestUtils from "@supersoniks/concorde/test-utils/TestUtils";
3
+ import {
4
+ markupAndInnerHTML,
5
+ prefixSuffix,
6
+ layoutSafe,
7
+ } from "./__fixtures__/descriptors";
8
+ import { snapshotLightDom } from "./sdui-dom-snapshot";
9
+ import {
10
+ validateSafeDescriptor,
11
+ isUnsafeSduiNode,
12
+ } from "./sdui-safe";
13
+ import type { SDUIDescriptor } from "./types";
14
+ import "./sdui";
15
+ import type { SonicSDUI } from "./sdui";
16
+
17
+ function clone<T>(value: T): T {
18
+ return structuredClone(value);
19
+ }
20
+
21
+ async function flushSdui(elt: SonicSDUI) {
22
+ await elt.updateComplete;
23
+ await Promise.resolve();
24
+ await Promise.resolve();
25
+ await elt.updateComplete;
26
+ }
27
+
28
+ async function render(
29
+ descriptor: SDUIDescriptor,
30
+ attrs: Record<string, string> = {}
31
+ ): Promise<SonicSDUI> {
32
+ const attrStr = Object.entries(attrs)
33
+ .map(([k, v]) => `${k}="${v}"`)
34
+ .join(" ");
35
+ const [elt] = TestUtils.bootstrap(
36
+ `<sonic-sdui ${attrStr}></sonic-sdui>`
37
+ ) as unknown as SonicSDUI[];
38
+ elt.props = clone(descriptor) as never;
39
+ await flushSdui(elt);
40
+ return elt;
41
+ }
42
+
43
+ afterEach(() => {
44
+ document.body.innerHTML = "";
45
+ vi.restoreAllMocks();
46
+ });
47
+
48
+ describe("validateSafeDescriptor", () => {
49
+ it("ne signale rien sur un descripteur sans HTML/JS libre", () => {
50
+ expect(validateSafeDescriptor(layoutSafe)).toEqual([]);
51
+ });
52
+
53
+ it("signale js, markup, innerHTML, prefix, suffix", () => {
54
+ const descriptor: SDUIDescriptor = {
55
+ js: ["https://example.test/x.js"],
56
+ library: {
57
+ bad: { markup: "<b>x</b>" },
58
+ },
59
+ nodes: [
60
+ { innerHTML: "a" },
61
+ { prefix: "p", suffix: "s", tagName: "div" },
62
+ {
63
+ tagName: "div",
64
+ nodes: [{ markup: "<i>nested</i>" }],
65
+ },
66
+ ],
67
+ };
68
+ const violations = validateSafeDescriptor(descriptor);
69
+ expect(violations.map((v) => v.path).sort()).toEqual(
70
+ [
71
+ "js",
72
+ "library.bad.markup",
73
+ "nodes[0].innerHTML",
74
+ "nodes[1].prefix",
75
+ "nodes[1].suffix",
76
+ "nodes[2].nodes[0].markup",
77
+ ].sort()
78
+ );
79
+ });
80
+
81
+ it("isUnsafeSduiNode ne regarde que le nœud courant", () => {
82
+ expect(isUnsafeSduiNode({ tagName: "div", nodes: [{ innerHTML: "x" }] })).toBe(
83
+ false
84
+ );
85
+ expect(isUnsafeSduiNode({ innerHTML: "x" })).toBe(true);
86
+ });
87
+ });
88
+
89
+ describe('sonic-sdui profile="safe"', () => {
90
+ it("sans profile, markup/innerHTML restent rendus (comportement historique)", async () => {
91
+ const elt = await render(markupAndInnerHTML);
92
+ const snap = snapshotLightDom(elt);
93
+ expect(JSON.stringify(snap)).toContain("badge");
94
+ expect(JSON.stringify(snap)).toContain("gras");
95
+ });
96
+
97
+ it("ignore les nœuds markup/innerHTML avec un warning", async () => {
98
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
99
+ const elt = await render(markupAndInnerHTML, { profile: "safe" });
100
+ const snap = snapshotLightDom(elt);
101
+ // Seul le toast subscriber reste
102
+ expect(snap).toEqual([{ tag: "sonic-toast-message-subscriber" }]);
103
+ expect(warn).toHaveBeenCalled();
104
+ expect(
105
+ warn.mock.calls.some((c) => String(c[0]).includes('profile="safe"'))
106
+ ).toBe(true);
107
+ });
108
+
109
+ it("ignore prefix/suffix", async () => {
110
+ vi.spyOn(console, "warn").mockImplementation(() => {});
111
+ const elt = await render(prefixSuffix, { profile: "safe" });
112
+ expect(snapshotLightDom(elt)).toEqual([
113
+ { tag: "sonic-toast-message-subscriber" },
114
+ ]);
115
+ });
116
+
117
+ it("laisse passer un layout sans HTML libre", async () => {
118
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
119
+ const elt = await render(layoutSafe, { profile: "safe" });
120
+ const snap = snapshotLightDom(elt);
121
+ expect(JSON.stringify(snap)).toContain("sdui-layout");
122
+ expect(JSON.stringify(snap)).toContain("sonic-input");
123
+ expect(JSON.stringify(snap)).toContain("Email");
124
+ expect(JSON.stringify(snap)).toContain("Titre safe");
125
+ expect(JSON.stringify(snap)).toContain("Go");
126
+ expect(warn).not.toHaveBeenCalled();
127
+ });
128
+
129
+ it("rend textContent en plain text (pas de HTML)", async () => {
130
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
131
+ const elt = await render(
132
+ {
133
+ nodes: [
134
+ {
135
+ tagName: "p",
136
+ textContent: "<b>not bold</b> & still text",
137
+ },
138
+ ],
139
+ },
140
+ { profile: "safe" }
141
+ );
142
+ const p = elt.querySelector("p");
143
+ expect(p?.textContent).toBe("<b>not bold</b> & still text");
144
+ expect(p?.querySelector("b")).toBeNull();
145
+ expect(warn).not.toHaveBeenCalled();
146
+ });
147
+
148
+ it("n'appelle pas loadJS pour descriptor.js", async () => {
149
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
150
+ const append = vi.spyOn(document.head, "appendChild");
151
+ await render(
152
+ { js: ["https://example.test/unsafe.js"], nodes: [{ tagName: "div" }] },
153
+ { profile: "safe" }
154
+ );
155
+ const scriptAppends = append.mock.calls.filter((c) => {
156
+ const n = c[0] as Node;
157
+ return n && (n as HTMLElement).tagName === "SCRIPT";
158
+ });
159
+ expect(scriptAppends).toHaveLength(0);
160
+ expect(
161
+ warn.mock.calls.some((c) => String(c[0]).includes("chargement js ignoré"))
162
+ ).toBe(true);
163
+ });
164
+
165
+ it("ignore un enfant dangereux sans casser le parent", async () => {
166
+ vi.spyOn(console, "warn").mockImplementation(() => {});
167
+ const elt = await render(
168
+ {
169
+ nodes: [
170
+ {
171
+ tagName: "div",
172
+ attributes: { class: "parent" },
173
+ nodes: [
174
+ { tagName: "span", attributes: { class: "ok" }, nodes: [] },
175
+ { innerHTML: "<img src=x onerror=alert(1)>" },
176
+ ],
177
+ },
178
+ ],
179
+ },
180
+ { profile: "safe" }
181
+ );
182
+ const html = JSON.stringify(snapshotLightDom(elt));
183
+ expect(html).toContain('"class":"parent"');
184
+ expect(html).toContain('"class":"ok"');
185
+ expect(html).not.toContain("onerror");
186
+ expect(html).not.toContain("img");
187
+ });
188
+ });
@@ -0,0 +1,100 @@
1
+ import type { SDUIDescriptor, SDUINode } from "./types";
2
+
3
+ /** Champs de nœud refusés en profil safe (injection HTML / scripts). */
4
+ export const SAFE_FORBIDDEN_NODE_FIELDS = [
5
+ "markup",
6
+ "innerHTML",
7
+ "prefix",
8
+ "suffix",
9
+ ] as const;
10
+
11
+ export type SafeForbiddenNodeField = (typeof SAFE_FORBIDDEN_NODE_FIELDS)[number];
12
+
13
+ export type SafeDescriptorViolation = {
14
+ /** Chemin JSON-like, ex. `js`, `nodes[0].markup`, `library.card.innerHTML` */
15
+ path: string;
16
+ /** Champ en cause */
17
+ field: "js" | SafeForbiddenNodeField;
18
+ /** Message lisible */
19
+ message: string;
20
+ };
21
+
22
+ function nodeForbiddenFields(node: SDUINode): SafeForbiddenNodeField[] {
23
+ const found: SafeForbiddenNodeField[] = [];
24
+ for (const field of SAFE_FORBIDDEN_NODE_FIELDS) {
25
+ const value = node[field];
26
+ if (value != null && value !== "") found.push(field);
27
+ }
28
+ return found;
29
+ }
30
+
31
+ function collectNodeViolations(
32
+ node: SDUINode,
33
+ path: string,
34
+ out: SafeDescriptorViolation[]
35
+ ) {
36
+ const fields = nodeForbiddenFields(node);
37
+ for (const field of fields) {
38
+ out.push({
39
+ path: `${path}.${field}`,
40
+ field,
41
+ message: `Champ "${field}" interdit en profil safe (${path}).`,
42
+ });
43
+ }
44
+ if (node.nodes) {
45
+ node.nodes.forEach((child, i) => {
46
+ collectNodeViolations(child, `${path}.nodes[${i}]`, out);
47
+ });
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Valide un descripteur SDUI pour le profil `safe`.
53
+ * Fonction pure : aucun effet de bord, réutilisable côté serveur.
54
+ */
55
+ export function validateSafeDescriptor(
56
+ descriptor: SDUIDescriptor | null | undefined
57
+ ): SafeDescriptorViolation[] {
58
+ const violations: SafeDescriptorViolation[] = [];
59
+ if (!descriptor) return violations;
60
+
61
+ if (descriptor.js && descriptor.js.length > 0) {
62
+ violations.push({
63
+ path: "js",
64
+ field: "js",
65
+ message: `Chargement de scripts (js) interdit en profil safe (${descriptor.js.length} URL).`,
66
+ });
67
+ }
68
+
69
+ if (descriptor.library) {
70
+ for (const key of Object.keys(descriptor.library)) {
71
+ collectNodeViolations(
72
+ descriptor.library[key],
73
+ `library.${key}`,
74
+ violations
75
+ );
76
+ }
77
+ }
78
+
79
+ if (descriptor.nodes) {
80
+ descriptor.nodes.forEach((node, i) => {
81
+ collectNodeViolations(node, `nodes[${i}]`, violations);
82
+ });
83
+ }
84
+
85
+ return violations;
86
+ }
87
+
88
+ /** True si le nœud lui-même (pas ses enfants) porte un champ interdit. */
89
+ export function isUnsafeSduiNode(node: SDUINode): boolean {
90
+ return nodeForbiddenFields(node).length > 0;
91
+ }
92
+
93
+ export function formatSafeNodeWarning(path: string, node: SDUINode): string {
94
+ const fields = nodeForbiddenFields(node);
95
+ return `sonic-sdui (profile="safe") : nœud ignoré (${path}) — champs refusés : ${fields.join(", ")}`;
96
+ }
97
+
98
+ export function formatSafeJsWarning(urls: string[]): string {
99
+ return `sonic-sdui (profile="safe") : chargement js ignoré (${urls.length} URL) — ${urls.join(", ")}`;
100
+ }
@@ -24,6 +24,119 @@ As a fetcher, invalidating the publisher or updating the endpoint will trigger a
24
24
 
25
25
  It has no shadowdom, and its display style is set to `contents`, so an empty SDUI has the fewest impact on the layout.
26
26
 
27
+ ### Safe profile (opt-in)
28
+
29
+ Set `profile="safe"` on `sonic-sdui` to refuse untrusted HTML/JS from the descriptor:
30
+
31
+ * `js` URLs are **not** loaded
32
+ * nodes that use `markup`, `innerHTML`, `prefix`, or `suffix` are **skipped** (not rendered)
33
+ * `textContent` is **allowed** (plain text, no HTML parsing)
34
+ * a clear `console.warn` is emitted for each refusal
35
+ * without the attribute, behaviour is unchanged
36
+
37
+ Use the pure helper `validateSafeDescriptor(descriptor)` (from `sdui-safe`) to list violations server-side or in a custom validator — it returns `{ path, field, message }[]` and has no side effects.
38
+
39
+ <sonic-code>
40
+ <template>
41
+ <sonic-sdui
42
+ profile="safe"
43
+ dataProvider="sdui-safe-example"
44
+ props='{
45
+ "nodes":[
46
+ { "tagName":"p", "textContent":"Plain text is fine" },
47
+ { "innerHTML":"&lt;script&gt;/* refused */&lt;/script&gt;" }
48
+ ]
49
+ }'
50
+ ></sonic-sdui>
51
+ </template>
52
+ </sonic-code>
53
+
54
+ ### Component catalog (build-only)
55
+
56
+ At build time, Concorde generates a **neutral JSON catalog** of custom elements for SDUI / external agents:
57
+
58
+ * file: `src/core/components/functional/sdui/component-catalog.json`
59
+ * import: `@supersoniks/concorde/sdui/component-catalog.json` (or the long `core/components/functional/sdui/…` path)
60
+ * regenerate: `yarn generate:sdui-catalog` (also runs in `prebuild`)
61
+ * source: Custom Elements Manifest via `@custom-elements-manifest/analyzer` (Lit)
62
+ * no runtime impact
63
+
64
+ Each entry includes `tag`, `props` (name, attribute, type, allowed `values` when detectable, `default`, description), plus slots/events when present.
65
+
66
+ Components with thin JSDoc are listed in `component-catalog.gaps.json` (same folder) — annotations are **not** mass-rewritten in this step.
67
+
68
+ ### Incremental operations (opt-in)
69
+
70
+ Beside the full-descriptor rebuild path, `sonic-sdui` exposes surgical ops (siblings keep DOM state):
71
+
72
+ * `upsertNode(node, parentId?, index?)` — `node.nodeId` required
73
+ * `removeNode(nodeId)`
74
+ * `setData(path, value)` — updates a publisher path (`form.email` or `provider.nested`)
75
+ * `applyOp(op)` / optional `opsDataProvider` for a stream of `SDUIOp`
76
+ * `endIncremental()` — return to props → full rebuild
77
+
78
+ Public extension surface (also covered above): `profile="safe"`, `textContent`, `sdui-action` / `action`, component catalog JSON, `partial` + `childNodeIds` / `nodeById`, and these incremental ops.
79
+
80
+ ### Partial rendering (opt-in)
81
+
82
+ Set the boolean attribute `partial` on `sonic-sdui` to tolerate missing child references:
83
+
84
+ * Declare a flat registry with `nodeById` and point to children with `childNodeIds: ["a", "b"]`
85
+ * If a `childNodeId` is **missing** and `partial` is set → a light placeholder is inserted
86
+ (`data-sdui-placeholder`, `data-node-id`, `display: contents`, `aria-hidden`)
87
+ * If `partial` is **not** set → `console.warn` and the missing child is skipped (no crash)
88
+ * Nested `nodes` arrays keep working as before; without `childNodeIds` / `partial`, behaviour is unchanged
89
+ * Incremental ops (later) reuse the same placeholder path
90
+
91
+ <sonic-code>
92
+ <template>
93
+ <sonic-sdui
94
+ partial
95
+ dataProvider="sdui-partial-example"
96
+ props='{
97
+ "nodes":[{
98
+ "tagName":"div",
99
+ "childNodeIds":["title", "body"]
100
+ }],
101
+ "nodeById":{
102
+ "title":{ "nodeId":"title", "tagName":"h2", "textContent":"Hello" }
103
+ }
104
+ }'
105
+ ></sonic-sdui>
106
+ </template>
107
+ </sonic-code>
108
+
109
+ ### Actions (`sdui-action`)
110
+
111
+ Declarative opt-in on a node: set `action: { name, context? }` (and optionally `nodeId`).
112
+ A click on the rendered element dispatches a `CustomEvent` named **`sdui-action`** on `sonic-sdui` with:
113
+
114
+ * `detail: { name, context, sourceNodeId? }`
115
+ * `bubbles: true`, `composed: true` — so a host (page shell, parent) can listen without binding each node
116
+
117
+ This does **not** replace form submit (`sonic-submit` / POST / `messagesKey`). Those keep working as before; only nodes that declare `action` emit `sdui-action`.
118
+
119
+ <sonic-code>
120
+ <template>
121
+ <sonic-sdui
122
+ dataProvider="sdui-action-example"
123
+ props='{
124
+ "nodes":[{
125
+ "tagName":"sonic-button",
126
+ "nodeId":"confirm-btn",
127
+ "textContent":"Confirm",
128
+ "action":{ "name":"confirm", "context":{ "step":1 } }
129
+ }]
130
+ }'
131
+ ></sonic-sdui>
132
+ <script type="module">
133
+ document.querySelector("sonic-sdui")?.addEventListener("sdui-action", (e) => {
134
+ console.log(e.detail); // { name, context, sourceNodeId }
135
+ });
136
+ </script>
137
+ </template>
138
+ </sonic-code>
139
+
27
140
  ## SDUI descriptor
28
141
 
29
142
  ### TypeScript definition:
@@ -34,6 +147,7 @@ It has no shadowdom, and its display style is set to `contents`, so an empty SDU
34
147
  css?: Array&lt;string&gt;;
35
148
  library?: Record&lt;string, SDUINode&gt;;
36
149
  nodes?: Array&lt;SDUINode&gt;;
150
+ nodeById?: Record&lt;string, SDUINode&gt;;
37
151
  };
38
152
  </template>
39
153
  </sonic-code>
@@ -44,6 +158,7 @@ It has no shadowdom, and its display style is set to `contents`, so an empty SDU
44
158
  * **css**: An array of URLs pointing to CSS files to load.
45
159
  * **library**: A record of SDUINode definitions intended to be used as a model for other nodes. The keys of the record can be used as the libraryKey of the node. This node will then specialize the model with its own descriptor.
46
160
  * **nodes**: An array of SDUINode descriptors. Each SDUINode will result in an HTMLElement added to the root of the component.
161
+ * **nodeById**: Optional flat map of nodes by `nodeId`, used with `childNodeIds` for partial / incremental graphs.
47
162
 
48
163
  ## SDUINode
49
164
 
@@ -60,12 +175,15 @@ TypeScript definition:
60
175
  tagName?: string;
61
176
  attributes?: Record&lt;string, string&gt;;
62
177
  nodes?: Array&lt;SDUINode&gt;;
178
+ textContent?: string;
63
179
  innerHTML?: string;
64
180
  prefix?: string;
65
181
  suffix?: string;
66
182
  libraryKey?: string;
67
183
  contentElementSelector?: string;
68
184
  parentElementSelector?: string;
185
+ action?: { name: string; context?: Record&lt;string, unknown&gt; };
186
+ nodeId?: string;
69
187
  };
70
188
  </template>
71
189
  </sonic-code>
@@ -75,12 +193,16 @@ Summary of properties:
75
193
  * **tagName**: The tag name of the HTML element.
76
194
  * **attributes**: A string key/value pair storing each attribute name/value of the HTML element created.
77
195
  * **nodes**: An array of SDUINode descriptors. The children of the HTMLElement are created this way.
196
+ * **textContent**: Plain text for the node (no HTML parsing). Preferred for safe UIs; ignored when `nodes` is non-empty. Allowed with `profile="safe"`.
78
197
  * **innerHTML**: The inner HTML of the current node is defined here if needed.
79
198
  * **prefix**: Use it if you want to wrap the component with some HTML string in conjunction with the suffix.
80
199
  * **suffix**: Use it if you want to wrap the component with some HTML string in conjunction with the prefix.
81
200
  * **libraryKey**: An identifier that points to an SDUINode to be used as a model for this node.
82
201
  * **contentElementSelector**: For SDUINodes defined in the library only. It is a CSS selector that defines where child nodes are added.
83
202
  * **parentElementSelector**: If the current node, as a future child, has a `parentElementSelector` attribute, it is added to the node corresponding to the associated CSS selector rather than directly to the element.
203
+ * **action**: Optional `{ name, context? }`. Click emits `sdui-action` (see above).
204
+ * **nodeId**: Optional logical id (not the DOM `id` attribute). Copied into `sdui-action.detail.sourceNodeId` when set.
205
+ * **childNodeIds**: Optional list of `nodeId`s to append as children (from `nodeById` / tree index). See Partial rendering.
84
206
 
85
207
  ### Markup
86
208
  Use it if you prefer to define your node entirely using an HTML string.
@@ -0,0 +1,64 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import TestUtils from "@supersoniks/concorde/test-utils/TestUtils";
3
+ import SDUIDescriptorTransformer from "./SDUIDescriptorTransformer";
4
+ import {
5
+ renderFixtures,
6
+ transformRemapDescription,
7
+ transformRemapInput,
8
+ } from "./__fixtures__/descriptors";
9
+ import { snapshotLightDom } from "./sdui-dom-snapshot";
10
+ import type { SDUIDescriptor } from "./types";
11
+ import "./sdui";
12
+ import type { SonicSDUI } from "./sdui";
13
+
14
+ function clone<T>(value: T): T {
15
+ return structuredClone(value);
16
+ }
17
+
18
+ /** Attendre la fin de updateContents (await loadLibrary / transform). */
19
+ async function flushSdui(elt: SonicSDUI) {
20
+ await elt.updateComplete;
21
+ await Promise.resolve();
22
+ await Promise.resolve();
23
+ await elt.updateComplete;
24
+ }
25
+
26
+ async function renderDescriptor(descriptor: SDUIDescriptor): Promise<SonicSDUI> {
27
+ const [elt] = TestUtils.bootstrap(
28
+ `<sonic-sdui></sonic-sdui>`
29
+ ) as unknown as SonicSDUI[];
30
+ // Clone : parseRootNodes mute nodes (toast) et updateContents mute library
31
+ elt.props = clone(descriptor) as never;
32
+ await flushSdui(elt);
33
+ return elt;
34
+ }
35
+
36
+ afterEach(() => {
37
+ document.body.innerHTML = "";
38
+ });
39
+
40
+ describe("sonic-sdui non-régression (DOM snapshots)", () => {
41
+ for (const [name, descriptor] of Object.entries(renderFixtures)) {
42
+ it(`fixture "${name}"`, async () => {
43
+ const elt = await renderDescriptor(descriptor);
44
+ expect(snapshotLightDom(elt)).toMatchSnapshot();
45
+ });
46
+ }
47
+ });
48
+
49
+ describe("SDUIDescriptorTransformer non-régression", () => {
50
+ it("remap legacy-button → sonic-button (descripteur)", async () => {
51
+ const descriptor = clone(transformRemapInput);
52
+ const transformer = new SDUIDescriptorTransformer();
53
+ await transformer.transform(descriptor, clone(transformRemapDescription));
54
+ expect(descriptor).toMatchSnapshot();
55
+ });
56
+
57
+ it("remap puis rendu DOM", async () => {
58
+ const descriptor = clone(transformRemapInput);
59
+ const transformer = new SDUIDescriptorTransformer();
60
+ await transformer.transform(descriptor, clone(transformRemapDescription));
61
+ const elt = await renderDescriptor(descriptor);
62
+ expect(snapshotLightDom(elt)).toMatchSnapshot();
63
+ });
64
+ });