@supersoniks/concorde 5.0.4 → 5.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 (123) hide show
  1. package/.agents/skills/concorde-component-docs/SKILL.md +194 -0
  2. package/.agents/skills/concorde-component-docs/template.md +45 -0
  3. package/.tmp-cem/custom-elements.json +13705 -0
  4. package/ai/cursor/rules/concorde-theme.mdc +1 -1
  5. package/ai/cursor/rules/concorde.mdc +1 -1
  6. package/ai/skills/concorde/SKILL.md +1 -1
  7. package/ai/skills/concorde-theme/SKILL.md +21 -10
  8. package/build-infos.json +1 -1
  9. package/concorde-3d.bundle.js +3960 -0
  10. package/concorde-3d.es.js +33433 -0
  11. package/concorde-core.bundle.js +507 -513
  12. package/concorde-core.es.js +4027 -3702
  13. package/concorde-hugging-face-infer.bundle.js +32 -0
  14. package/concorde-hugging-face-infer.es.js +2952 -0
  15. package/concorde-interactive.bundle.js +1157 -0
  16. package/concorde-interactive.es.js +10734 -0
  17. package/concorde-shader.bundle.js +116 -0
  18. package/concorde-shader.es.js +8353 -0
  19. package/concorde-webgpu.bundle.js +307 -0
  20. package/concorde-webgpu.es.js +8627 -0
  21. package/custom-elements-manifest.config.mjs +14 -0
  22. package/dist/concorde-core.bundle.js +507 -513
  23. package/dist/concorde-core.es.js +4027 -3702
  24. package/dist/docs-mock-api-sw.js +2 -0
  25. package/dist/docs-mock-api-sw.js.map +2 -2
  26. package/gitlab/job_tests.sh +0 -0
  27. package/index.html +4 -2
  28. package/package.json +42 -2
  29. package/public/docs-mock-api-sw.js +2 -0
  30. package/public/docs-mock-api-sw.js.map +2 -2
  31. package/scripts/generate-sdui-catalog.mjs +250 -0
  32. package/scripts/generate-sdui-catalog.spec.ts +91 -0
  33. package/scripts/pre-build.mjs +15 -0
  34. package/src/core/components/functional/if/if.ts +0 -0
  35. package/src/core/components/functional/sdui/SDUIDescriptorTransformer.ts +5 -5
  36. package/src/core/components/functional/sdui/__fixtures__/descriptors.ts +251 -0
  37. package/src/core/components/functional/sdui/__snapshots__/sdui-action.spec.ts.snap +73 -0
  38. package/src/core/components/functional/sdui/__snapshots__/sdui-incremental.spec.ts.snap +50 -0
  39. package/src/core/components/functional/sdui/__snapshots__/sdui-partial.spec.ts.snap +50 -0
  40. package/src/core/components/functional/sdui/__snapshots__/sdui.regression.spec.ts.snap +461 -0
  41. package/src/core/components/functional/sdui/component-catalog.gaps.json +511 -0
  42. package/src/core/components/functional/sdui/component-catalog.json +3103 -0
  43. package/src/core/components/functional/sdui/component-catalog.spec.ts +23 -0
  44. package/src/core/components/functional/sdui/sdui-action.spec.ts +99 -0
  45. package/src/core/components/functional/sdui/sdui-dom-snapshot.ts +61 -0
  46. package/src/core/components/functional/sdui/sdui-extension-points.md +94 -0
  47. package/src/core/components/functional/sdui/sdui-incremental.spec.ts +137 -0
  48. package/src/core/components/functional/sdui/sdui-partial.spec.ts +83 -0
  49. package/src/core/components/functional/sdui/sdui-partial.ts +19 -0
  50. package/src/core/components/functional/sdui/sdui-safe.spec.ts +188 -0
  51. package/src/core/components/functional/sdui/sdui-safe.ts +100 -0
  52. package/src/core/components/functional/sdui/sdui.md +122 -0
  53. package/src/core/components/functional/sdui/sdui.regression.spec.ts +64 -0
  54. package/src/core/components/functional/sdui/sdui.ts +442 -34
  55. package/src/core/components/functional/sdui/types.ts +49 -0
  56. package/src/core/components/functional/value/value.ts +0 -0
  57. package/src/core/components/ui/_css/align.ts +1 -0
  58. package/src/core/components/ui/_css/type.ts +86 -8
  59. package/src/core/components/ui/_i18n/ui-aria-labels.ts +8 -0
  60. package/src/core/components/ui/alert/alert.md +21 -2
  61. package/src/core/components/ui/alert-messages/alert-messages.md +85 -0
  62. package/src/core/components/ui/badge/badge.md +19 -6
  63. package/src/core/components/ui/badge/badge.ts +8 -9
  64. package/src/core/components/ui/button/button.ts +5 -15
  65. package/src/core/components/ui/captcha/captcha.md +65 -6
  66. package/src/core/components/ui/card/card.md +73 -10
  67. package/src/core/components/ui/card/card.ts +24 -22
  68. package/src/core/components/ui/divider/divider.md +107 -13
  69. package/src/core/components/ui/divider/divider.ts +40 -33
  70. package/src/core/components/ui/form/fieldset/fieldset.md +175 -99
  71. package/src/core/components/ui/form/fieldset/fieldset.ts +15 -28
  72. package/src/core/components/ui/form/fieldset/legend.ts +13 -13
  73. package/src/core/components/ui/form/form-actions/form-actions.md +75 -52
  74. package/src/core/components/ui/form/form-actions/form-actions.ts +7 -5
  75. package/src/core/components/ui/form/form-layout/form-layout.md +109 -34
  76. package/src/core/components/ui/form/form-layout/form-layout.ts +27 -24
  77. package/src/core/components/ui/form/textarea/textarea.ts +4 -3
  78. package/src/core/components/ui/group/group.md +36 -36
  79. package/src/core/components/ui/group/group.ts +5 -10
  80. package/src/core/components/ui/icon/icon.md +132 -43
  81. package/src/core/components/ui/image/image.md +35 -3
  82. package/src/core/components/ui/link/link.md +43 -5
  83. package/src/core/components/ui/loader/loader.md +79 -22
  84. package/src/core/components/ui/loader/loader.ts +67 -27
  85. package/src/core/components/ui/loader/styles/fixed.ts +6 -11
  86. package/src/core/components/ui/menu/menu.md +133 -92
  87. package/src/core/components/ui/menu/menu.ts +53 -58
  88. package/src/core/components/ui/modal/modal.md +163 -47
  89. package/src/core/components/ui/modal/modal.ts +2 -4
  90. package/src/core/components/ui/pop/pop.md +217 -123
  91. package/src/core/components/ui/pop/pop.ts +0 -1
  92. package/src/core/components/ui/progress/progress.md +76 -30
  93. package/src/core/components/ui/progress/progress.ts +25 -56
  94. package/src/core/components/ui/table/table-caption.ts +2 -2
  95. package/src/core/components/ui/table/table-tbody.ts +3 -3
  96. package/src/core/components/ui/table/table-td.ts +23 -18
  97. package/src/core/components/ui/table/table-th.ts +8 -8
  98. package/src/core/components/ui/table/table-tr.ts +4 -4
  99. package/src/core/components/ui/table/table.md +147 -171
  100. package/src/core/components/ui/table/table.ts +20 -19
  101. package/src/core/components/ui/theme/theme-collection/auto.ts +15 -0
  102. package/src/core/components/ui/theme/theme-collection/core-variables.ts +8 -31
  103. package/src/core/components/ui/theme/theme-collection/dark.ts +1 -7
  104. package/src/core/components/ui/theme/theme-collection/light.ts +30 -26
  105. package/src/core/components/ui/theme/theme.md +122 -0
  106. package/src/core/components/ui/theme/theme.ts +30 -7
  107. package/src/core/components/ui/toast/toast-item.ts +6 -9
  108. package/src/core/components/ui/toast/toast.md +108 -58
  109. package/src/core/components/ui/toast/types.ts +1 -1
  110. package/src/core/components/ui/tooltip/tooltip.md +68 -26
  111. package/src/docs/_getting-started/start.md +1 -1
  112. package/src/docs/_getting-started/theming.md +223 -61
  113. package/src/docs/code.ts +125 -58
  114. package/src/docs/header/header.ts +62 -54
  115. package/src/docs/mock-api/router.ts +2 -0
  116. package/src/docs/navigation/navigation.ts +6 -3
  117. package/src/docs/search/docs-search.json +2504 -399
  118. package/src/docs/search/markdown-renderer.ts +6 -6
  119. package/src/docs/search/search.ts +4 -4
  120. package/src/docs/tailwind/css/tailwind.css +6 -5
  121. package/src/tsconfig.json +108 -0
  122. package/src/tsconfig.tsbuildinfo +1 -1
  123. package/tailwind.config.js +19 -0
@@ -1,8 +1,20 @@
1
1
  import default_library from "@supersoniks/concorde/core/components/functional/sdui/default-library.json";
2
2
  import SDUIDescriptorTransformer from "@supersoniks/concorde/core/components/functional/sdui/SDUIDescriptorTransformer";
3
+ import { getPublisherFromPath } from "@supersoniks/concorde/core/decorators/subscriber/publisherPath";
3
4
  import {
5
+ createSduiPlaceholder,
6
+ SDUI_NODE_ID_ATTR,
7
+ } from "@supersoniks/concorde/core/components/functional/sdui/sdui-partial";
8
+ import {
9
+ formatSafeJsWarning,
10
+ formatSafeNodeWarning,
11
+ isUnsafeSduiNode,
12
+ } from "@supersoniks/concorde/core/components/functional/sdui/sdui-safe";
13
+ import {
14
+ SDUIActionDetail,
4
15
  SDUIDescriptor,
5
16
  SDUINode,
17
+ SDUIOp,
6
18
  SDUITransformDescription,
7
19
  } from "@supersoniks/concorde/core/components/functional/sdui/types";
8
20
  import { Fetcher, Subscriber } from "@supersoniks/concorde/mixins";
@@ -21,6 +33,10 @@ const tagName = "sonic-sdui"; // For Astro.build
21
33
  * * Le format du JSON est décrit par le type SDUIDescriptor
22
34
  * * Un attribut supplémentaire `transformation` permet de transformer le json reçu avant la génération de l'interface utilisateur<br>
23
35
  * Son format est décrit par le type SDUITransformDescription
36
+ * * Attribut opt-in `profile="safe"` : refuse js, markup, innerHTML, prefix et suffix (nœud ignoré + warning).
37
+ * * Champ de nœud opt-in `action: { name, context }` : au clic, émet `sdui-action` (bubbles + composed).
38
+ * * Attribut opt-in `partial` : enfants `childNodeIds` absents → placeholder léger (sinon warning + skip).
39
+ * * Ops incrémentales opt-in : `upsertNode` / `removeNode` / `setData` (chemin à côté du descripteur entier).
24
40
  *
25
41
  * * Si le résultat de la requête est un objet, il est imbriqué dans un tableau pour garantir le fonctionnement.<br>
26
42
  * *
@@ -32,17 +48,57 @@ export class SonicSDUI extends Fetcher(Subscriber(LitElement)) {
32
48
  this.displayContents = true;
33
49
  this.isFetchEnabled = this.hasAttribute("fetch");
34
50
  super.connectedCallback();
51
+ if (this.opsDataProvider) this.bindOpsDataProvider();
35
52
  }
36
53
 
37
54
  @property() sduiKey?: string;
38
55
  @property() messagesKey?: string;
56
+ /**
57
+ * Profil de rendu opt-in. `safe` refuse js / markup / innerHTML / prefix / suffix.
58
+ * Sans attribut, comportement historique inchangé.
59
+ */
60
+ @property({ type: String, reflect: true }) profile?: string;
61
+ /**
62
+ * Rendu partiel opt-in : les `childNodeIds` introuvables deviennent des placeholders.
63
+ * Activé aussi en mode incrémental (ops étape 6).
64
+ */
65
+ @property({ type: Boolean, reflect: true }) partial = false;
66
+ /**
67
+ * DataProvider optionnel alimenté par un flux d'ops (`SDUIOp` ou `SDUIOp[]`).
68
+ * Chaque assignation est consommée via `applyOp`.
69
+ */
70
+ @property({ type: String }) opsDataProvider?: string;
39
71
  private sduiDescriptor: SDUIDescriptor = {};
72
+ private nodeIndex = new Map<string, SDUINode>();
73
+ /** Garde-fou cycles childNodeIds. */
74
+ private renderingStack = new Set<string>();
75
+ /** Forcé à true par les ops incrémentales. */
76
+ private incrementalPartial = false;
77
+ /** Mode ops : descripteur local cloné ; ignore les props publisher. */
78
+ private incrementalActive = false;
79
+ private opsUnsub?: () => void;
80
+
81
+ private get isSafeProfile(): boolean {
82
+ return this.profile === "safe";
83
+ }
84
+
85
+ private get isPartialMode(): boolean {
86
+ return this.partial || this.incrementalPartial;
87
+ }
40
88
 
41
89
  /**
42
90
  * On peut passer la description sous form de props, sinon il faut utiliser l'attribut fetch
43
91
  */
44
92
 
45
93
  willUpdate(changedProperties: PropertyValues): void {
94
+ if (changedProperties.has("opsDataProvider")) {
95
+ this.bindOpsDataProvider();
96
+ }
97
+ // Chemin incrémental : ne pas reconstruire depuis props (descripteur local).
98
+ if (this.incrementalActive) {
99
+ super.willUpdate(changedProperties);
100
+ return;
101
+ }
46
102
  if (this.props == null) {
47
103
  this.sduiDescriptor = {};
48
104
  }
@@ -50,13 +106,26 @@ export class SonicSDUI extends Fetcher(Subscriber(LitElement)) {
50
106
  const newSduiDescriptor = this.sduiKey
51
107
  ? (this.props as Record<string, SDUIDescriptor>)[this.sduiKey]
52
108
  : (this.props as SDUIDescriptor);
53
- if (this.sduiDescriptor == newSduiDescriptor) return;
109
+ const profileChanged = changedProperties.has("profile");
110
+ const partialChanged = changedProperties.has("partial");
111
+ if (
112
+ this.sduiDescriptor == newSduiDescriptor &&
113
+ !profileChanged &&
114
+ !partialChanged
115
+ )
116
+ return;
54
117
  this.sduiDescriptor = newSduiDescriptor;
55
118
  this.updateContents();
56
119
  }
57
120
  super.willUpdate(changedProperties);
58
121
  }
59
122
 
123
+ disconnectedCallback(): void {
124
+ this.opsUnsub?.();
125
+ this.opsUnsub = undefined;
126
+ super.disconnectedCallback();
127
+ }
128
+
60
129
  /**
61
130
  * updateContents est déclenché quand les sduiDescriptor sont renseignées
62
131
  * Le contenu du composant est regénéré en fonction du descripteur fourni
@@ -69,8 +138,32 @@ export class SonicSDUI extends Fetcher(Subscriber(LitElement)) {
69
138
  this.loadAssets();
70
139
  await this.loadLibrary();
71
140
  await this.transformSDUIDescriptor();
141
+ this.rebuildNodeIndex();
72
142
  this.parseRootNodes();
73
143
  }
144
+
145
+ /** Index `nodeId` → nœud (nodeById + parcours du tree / library). */
146
+ private rebuildNodeIndex() {
147
+ this.nodeIndex.clear();
148
+ const visit = (node: SDUINode) => {
149
+ if (node.nodeId) this.nodeIndex.set(node.nodeId, node);
150
+ node.nodes?.forEach(visit);
151
+ };
152
+ if (this.sduiDescriptor.nodeById) {
153
+ for (const id of Object.keys(this.sduiDescriptor.nodeById)) {
154
+ const n = this.sduiDescriptor.nodeById[id];
155
+ if (!n.nodeId) n.nodeId = id;
156
+ this.nodeIndex.set(id, n);
157
+ visit(n);
158
+ }
159
+ }
160
+ this.sduiDescriptor.nodes?.forEach(visit);
161
+ if (this.sduiDescriptor.library) {
162
+ for (const key of Object.keys(this.sduiDescriptor.library)) {
163
+ visit(this.sduiDescriptor.library[key]);
164
+ }
165
+ }
166
+ }
74
167
  /**
75
168
  * Suppressiond du contenu du composant avant le génération de la nouvelle ui
76
169
  */
@@ -86,8 +179,12 @@ export class SonicSDUI extends Fetcher(Subscriber(LitElement)) {
86
179
  **/
87
180
  private loadAssets() {
88
181
  if (!this.sduiDescriptor) return;
89
- if (this.sduiDescriptor.js) {
90
- for (const src of this.sduiDescriptor.js) HTML.loadJS(src);
182
+ if (this.sduiDescriptor.js?.length) {
183
+ if (this.isSafeProfile) {
184
+ console.warn(formatSafeJsWarning(this.sduiDescriptor.js));
185
+ } else {
186
+ for (const src of this.sduiDescriptor.js) HTML.loadJS(src);
187
+ }
91
188
  }
92
189
  if (this.sduiDescriptor.css) {
93
190
  for (const src of this.sduiDescriptor.css) HTML.loadCSS(src);
@@ -128,24 +225,48 @@ export class SonicSDUI extends Fetcher(Subscriber(LitElement)) {
128
225
  messageProvider.attributes = { subDataProvider: this.messagesKey };
129
226
  }
130
227
  nodes.push(messageProvider);
131
- nodes.forEach((node) => this.appendChild(this.parseChild(node)));
228
+ nodes.forEach((node, i) => {
229
+ const el = this.parseChild(node, `nodes[${i}]`);
230
+ if (el) this.appendChild(el);
231
+ });
132
232
  }
133
233
  /**
134
234
  * On parse un noeud ce qui crée un éléments graphique et ses enfants par recursivité via `handleChildNodes`
235
+ * En profil safe, un nœud avec markup/innerHTML/prefix/suffix est ignoré (null).
135
236
  */
136
- private parseChild(node: SDUINode) {
137
- const tagName = node.tagName || "div";
138
- let { element, contentElement } = this.handleLibrary(node, tagName);
139
- this.handleAttributes(node, element);
140
- element = this.handleMarkup(node, element);
141
- if (!contentElement) contentElement = element;
142
- this.handleChildNodes(node, contentElement, element);
143
- this.handleInnerHTML(node, contentElement);
144
- if (node.prefix || node.suffix) {
145
- const container = this.handlePrefixSuffix(node, element);
146
- return container;
237
+ private parseChild(node: SDUINode, path = "node"): HTMLElement | null {
238
+ if (this.isSafeProfile && isUnsafeSduiNode(node)) {
239
+ console.warn(formatSafeNodeWarning(path, node));
240
+ return null;
241
+ }
242
+ if (node.nodeId && this.renderingStack.has(node.nodeId)) {
243
+ console.warn(
244
+ `sonic-sdui : cycle détecté sur nodeId="${node.nodeId}" (${path})`
245
+ );
246
+ return this.isPartialMode ? createSduiPlaceholder(node.nodeId) : null;
247
+ }
248
+ if (node.nodeId) this.renderingStack.add(node.nodeId);
249
+ try {
250
+ const tagName = node.tagName || "div";
251
+ let { element, contentElement } = this.handleLibrary(node, tagName, path);
252
+ if (!element) return null;
253
+ this.handleAttributes(node, element);
254
+ element = this.handleMarkup(node, element);
255
+ if (!contentElement) contentElement = element;
256
+ this.handleChildNodes(node, contentElement, element, path);
257
+ this.handleTextContent(node, contentElement);
258
+ this.handleInnerHTML(node, contentElement);
259
+ let host = element;
260
+ if (node.prefix || node.suffix) {
261
+ host = this.handlePrefixSuffix(node, element);
262
+ }
263
+ // Après prefix/suffix : innerHTML recrée le DOM, donc binder l'action sur l'hôte final.
264
+ this.bindAction(node, host);
265
+ if (node.nodeId) host.setAttribute(SDUI_NODE_ID_ATTR, node.nodeId);
266
+ return host;
267
+ } finally {
268
+ if (node.nodeId) this.renderingStack.delete(node.nodeId);
147
269
  }
148
- return element;
149
270
  }
150
271
  /**
151
272
  * Si le noeud courant a des propriétés prefix et ou suffix il est entouré des markups fournis dans ces sduiDescriptor.
@@ -158,35 +279,100 @@ export class SonicSDUI extends Fetcher(Subscriber(LitElement)) {
158
279
  container.style.display = "contents";
159
280
  return container;
160
281
  }
282
+ /**
283
+ * Branche un clic → CustomEvent `sdui-action` si le nœud déclare `action.name`.
284
+ * bubbles + composed : un ancêtre (shell, page) peut écouter sans se coller au nœud.
285
+ */
286
+ private bindAction(node: SDUINode, element: HTMLElement) {
287
+ const action = node.action;
288
+ if (!action?.name) return;
289
+ element.addEventListener("click", () => {
290
+ const detail: SDUIActionDetail = {
291
+ name: action.name,
292
+ context: action.context ?? {},
293
+ sourceNodeId: node.nodeId,
294
+ };
295
+ this.dispatchEvent(
296
+ new CustomEvent<SDUIActionDetail>("sdui-action", {
297
+ detail,
298
+ bubbles: true,
299
+ composed: true,
300
+ })
301
+ );
302
+ });
303
+ }
161
304
  /**
162
305
  * Création des enfants du noeud courant
163
306
  * Si l'enfant à un attribut parentElementSelector, il est ajouté dans le noeud correspondant au sélecteur css associé et non pas dans l'élément directement.
307
+ * `childNodeIds` résout des nœuds du registre ; absents → placeholder (partial) ou skip.
164
308
  */
165
309
  private handleChildNodes(
166
310
  node: SDUINode,
167
311
  contentElement: HTMLElement,
168
- element: HTMLElement
312
+ element: HTMLElement,
313
+ path: string
169
314
  ) {
170
315
  if (node.nodes) {
171
316
  const children: Array<SDUINode> = node.nodes;
172
- for (const child of children) {
173
- const childElement = this.parseChild(child);
174
- let nodeToAppendOn = contentElement;
175
- if (child.parentElementSelector) {
176
- nodeToAppendOn =
177
- element.querySelector(child.parentElementSelector) ||
178
- contentElement;
317
+ for (let i = 0; i < children.length; i++) {
318
+ const child = children[i];
319
+ const childElement = this.parseChild(child, `${path}.nodes[${i}]`);
320
+ if (!childElement) continue;
321
+ this.appendChildElement(child, childElement, contentElement, element);
322
+ }
323
+ }
324
+ if (node.childNodeIds?.length) {
325
+ for (const childId of node.childNodeIds) {
326
+ const child = this.nodeIndex.get(childId);
327
+ if (child) {
328
+ const childElement = this.parseChild(
329
+ child,
330
+ `${path}.childNodeIds[${childId}]`
331
+ );
332
+ if (childElement) {
333
+ this.appendChildElement(
334
+ child,
335
+ childElement,
336
+ contentElement,
337
+ element
338
+ );
339
+ }
340
+ continue;
341
+ }
342
+ if (this.isPartialMode) {
343
+ this.appendToTarget(
344
+ contentElement,
345
+ createSduiPlaceholder(childId)
346
+ );
347
+ } else {
348
+ console.warn(
349
+ `sonic-sdui : childNodeId "${childId}" introuvable (${path}) — ignoré (activez partial pour un placeholder)`
350
+ );
179
351
  }
180
- if (nodeToAppendOn.shadowRoot)
181
- nodeToAppendOn.shadowRoot.appendChild(childElement);
182
- else if (nodeToAppendOn.tagName.toLocaleLowerCase() == "template") {
183
- const template: HTMLTemplateElement =
184
- nodeToAppendOn as HTMLTemplateElement;
185
- template.content.appendChild(childElement);
186
- } else nodeToAppendOn.appendChild(childElement);
187
352
  }
188
353
  }
189
354
  }
355
+
356
+ private appendChildElement(
357
+ child: SDUINode,
358
+ childElement: HTMLElement,
359
+ contentElement: HTMLElement,
360
+ element: HTMLElement
361
+ ) {
362
+ let nodeToAppendOn = contentElement;
363
+ if (child.parentElementSelector) {
364
+ nodeToAppendOn =
365
+ element.querySelector(child.parentElementSelector) || contentElement;
366
+ }
367
+ this.appendToTarget(nodeToAppendOn, childElement);
368
+ }
369
+
370
+ private appendToTarget(target: HTMLElement, childElement: HTMLElement) {
371
+ if (target.shadowRoot) target.shadowRoot.appendChild(childElement);
372
+ else if (target.tagName.toLocaleLowerCase() == "template") {
373
+ (target as HTMLTemplateElement).content.appendChild(childElement);
374
+ } else target.appendChild(childElement);
375
+ }
190
376
  /**
191
377
  * Gestion de librarie :
192
378
  * * Si l'élément référence un élément de la librairie, on se sert de cet élément comme model pour créer le composant graphique.
@@ -194,13 +380,25 @@ export class SonicSDUI extends Fetcher(Subscriber(LitElement)) {
194
380
  * * Si contentElementSelector est definit, alors contentElement correspond à l'élément obtenu par selection css d'après la valeurs de contentElementSelector
195
381
  * * Les éléments enfants seront ensuite ajoutés dans contentElement
196
382
  */
197
- private handleLibrary(node: SDUINode, tagName: string) {
198
- let element: HTMLElement;
383
+ private handleLibrary(
384
+ node: SDUINode,
385
+ tagName: string,
386
+ path: string
387
+ ): { element: HTMLElement | null; contentElement: HTMLElement | undefined } {
388
+ let element: HTMLElement | null;
199
389
  let contentElement: HTMLElement | undefined;
200
390
  if (node.libraryKey && this.sduiDescriptor.library) {
391
+ const libPath = `library.${node.libraryKey}`;
201
392
  element = this.parseChild(
202
- this.sduiDescriptor.library[node.libraryKey] || { tagName: "div" }
393
+ this.sduiDescriptor.library[node.libraryKey] || { tagName: "div" },
394
+ libPath
203
395
  );
396
+ if (!element) {
397
+ console.warn(
398
+ `sonic-sdui (profile="safe") : nœud ignoré (${path}) — modèle libraryKey="${node.libraryKey}" refusé`
399
+ );
400
+ return { element: null, contentElement: undefined };
401
+ }
204
402
  const selector = (this.sduiDescriptor.library[node.libraryKey] || {})
205
403
  .contentElementSelector;
206
404
  if (selector)
@@ -233,6 +431,19 @@ export class SonicSDUI extends Fetcher(Subscriber(LitElement)) {
233
431
  return element;
234
432
  }
235
433
 
434
+ /**
435
+ * Texte plain via textContent (pas de parse HTML). Autorisé en profile="safe".
436
+ * Ignoré si le nœud a déjà des enfants déclarés dans `nodes` (évite d'effacer le sous-arbre).
437
+ */
438
+ private handleTextContent(
439
+ node: SDUINode,
440
+ contentElement: HTMLElement | undefined
441
+ ) {
442
+ if (node.textContent == null || !contentElement) return;
443
+ if (node.nodes?.length || node.childNodeIds?.length) return;
444
+ contentElement.textContent = node.textContent;
445
+ }
446
+
236
447
  /**
237
448
  * si le noeud à une propriété innerHTML, on l'ajout ay innerHTML de l'élément html en cours de création
238
449
  */
@@ -252,4 +463,201 @@ export class SonicSDUI extends Fetcher(Subscriber(LitElement)) {
252
463
  contentElement.innerHTML += node.innerHTML;
253
464
  }
254
465
  }
466
+
467
+ // ——— Ops incrémentales (chemin à côté du descripteur entier) ———
468
+
469
+ /**
470
+ * Ajoute ou remplace un nœud par `nodeId` (requis).
471
+ * Rendu chirurgical : les frères DOM sont préservés.
472
+ */
473
+ upsertNode(node: SDUINode, parentId?: string, index?: number): void {
474
+ if (!node.nodeId) {
475
+ console.warn('sonic-sdui.upsertNode : "nodeId" est requis');
476
+ return;
477
+ }
478
+ this.beginIncremental();
479
+ const id = node.nodeId;
480
+ if (!this.sduiDescriptor.nodeById) this.sduiDescriptor.nodeById = {};
481
+ this.sduiDescriptor.nodeById[id] = node;
482
+ this.nodeIndex.set(id, node);
483
+
484
+ if (parentId) {
485
+ let parent =
486
+ this.nodeIndex.get(parentId) || this.sduiDescriptor.nodeById[parentId];
487
+ if (!parent) {
488
+ parent = {
489
+ nodeId: parentId,
490
+ tagName: "div",
491
+ childNodeIds: [],
492
+ };
493
+ this.sduiDescriptor.nodeById[parentId] = parent;
494
+ this.nodeIndex.set(parentId, parent);
495
+ if (!this.sduiDescriptor.nodes) this.sduiDescriptor.nodes = [];
496
+ if (!this.sduiDescriptor.nodes.some((n) => n.nodeId === parentId)) {
497
+ this.sduiDescriptor.nodes.push(parent);
498
+ }
499
+ }
500
+ if (!parent.childNodeIds) parent.childNodeIds = [];
501
+ const prev = parent.childNodeIds.indexOf(id);
502
+ if (prev >= 0) parent.childNodeIds.splice(prev, 1);
503
+ const at =
504
+ index != null
505
+ ? Math.max(0, Math.min(index, parent.childNodeIds.length))
506
+ : parent.childNodeIds.length;
507
+ parent.childNodeIds.splice(at, 0, id);
508
+ } else {
509
+ if (!this.sduiDescriptor.nodes) this.sduiDescriptor.nodes = [];
510
+ const roots = this.sduiDescriptor.nodes;
511
+ const ri = roots.findIndex((n) => n.nodeId === id);
512
+ if (ri >= 0) roots[ri] = node;
513
+ else if (index != null) {
514
+ roots.splice(Math.max(0, Math.min(index, roots.length)), 0, node);
515
+ } else roots.push(node);
516
+ }
517
+
518
+ this.rebuildNodeIndex();
519
+ this.domUpsertNode(node, parentId, index);
520
+ }
521
+
522
+ /** Supprime un nœud du registre et du DOM (frères préservés). */
523
+ removeNode(nodeId: string): void {
524
+ this.beginIncremental();
525
+ delete this.sduiDescriptor.nodeById?.[nodeId];
526
+ this.nodeIndex.delete(nodeId);
527
+ if (this.sduiDescriptor.nodes) {
528
+ this.sduiDescriptor.nodes = this.sduiDescriptor.nodes.filter(
529
+ (n) => n.nodeId !== nodeId
530
+ );
531
+ }
532
+ if (this.sduiDescriptor.nodeById) {
533
+ for (const n of Object.values(this.sduiDescriptor.nodeById)) {
534
+ if (n.childNodeIds) {
535
+ n.childNodeIds = n.childNodeIds.filter((id) => id !== nodeId);
536
+ }
537
+ }
538
+ }
539
+ this.findDomNode(nodeId)?.remove();
540
+ }
541
+
542
+ /**
543
+ * Met à jour une donnée publisher par chemin (`provider.cle.imbrique` ou relatif
544
+ * au `dataProvider` du composant si le premier segment ne matche pas).
545
+ */
546
+ setData(path: string, value: unknown): void {
547
+ const direct = getPublisherFromPath(path);
548
+ if (direct) {
549
+ direct.set(value);
550
+ return;
551
+ }
552
+ if (this.dataProvider) {
553
+ const nested = getPublisherFromPath(`${this.dataProvider}.${path}`);
554
+ if (nested) {
555
+ nested.set(value);
556
+ return;
557
+ }
558
+ }
559
+ console.warn(`sonic-sdui.setData : chemin introuvable "${path}"`);
560
+ }
561
+
562
+ /** Applique une op (méthode ou entrée de flux `opsDataProvider`). */
563
+ applyOp(op: SDUIOp): void {
564
+ switch (op.op) {
565
+ case "upsertNode":
566
+ this.upsertNode(op.node, op.parentId, op.index);
567
+ break;
568
+ case "removeNode":
569
+ this.removeNode(op.nodeId);
570
+ break;
571
+ case "setData":
572
+ this.setData(op.path, op.value);
573
+ break;
574
+ default:
575
+ console.warn("sonic-sdui.applyOp : op inconnue", op);
576
+ }
577
+ }
578
+
579
+ /** Quitte le mode incrémental ; le prochain `props` refait un rendu complet. */
580
+ endIncremental(): void {
581
+ this.incrementalActive = false;
582
+ this.incrementalPartial = false;
583
+ }
584
+
585
+ private beginIncremental() {
586
+ if (this.incrementalActive) return;
587
+ this.sduiDescriptor = structuredClone(this.sduiDescriptor || {});
588
+ if (!this.sduiDescriptor.nodeById) this.sduiDescriptor.nodeById = {};
589
+ if (!this.sduiDescriptor.nodes) this.sduiDescriptor.nodes = [];
590
+ // Fusion library défaut si absente (évite un rebuild complet)
591
+ if (!this.sduiDescriptor.library) {
592
+ const library: Record<string, SDUINode> = {};
593
+ Object.assign(library, default_library);
594
+ this.sduiDescriptor.library = library;
595
+ }
596
+ this.incrementalActive = true;
597
+ this.incrementalPartial = true;
598
+ this.rebuildNodeIndex();
599
+ }
600
+
601
+ private findDomNode(nodeId: string): HTMLElement | null {
602
+ const escaped =
603
+ typeof CSS !== "undefined" && CSS.escape
604
+ ? CSS.escape(nodeId)
605
+ : nodeId.replace(/"/g, '\\"');
606
+ // Placeholders portent aussi SDUI_NODE_ID_ATTR
607
+ return this.querySelector(
608
+ `[${SDUI_NODE_ID_ATTR}="${escaped}"]`
609
+ ) as HTMLElement | null;
610
+ }
611
+
612
+ private domUpsertNode(
613
+ node: SDUINode,
614
+ parentId?: string,
615
+ index?: number
616
+ ) {
617
+ const rendered = this.parseChild(node, `upsert:${node.nodeId}`);
618
+ if (!rendered) return;
619
+ const existing = this.findDomNode(node.nodeId!);
620
+ if (existing && existing !== rendered) {
621
+ existing.replaceWith(rendered);
622
+ return;
623
+ }
624
+ if (existing === rendered) return;
625
+
626
+ if (parentId) {
627
+ const parentEl = this.findDomNode(parentId);
628
+ if (parentEl) {
629
+ const kids = Array.from(parentEl.children);
630
+ if (index != null && index < kids.length) {
631
+ parentEl.insertBefore(rendered, kids[index]);
632
+ } else parentEl.appendChild(rendered);
633
+ return;
634
+ }
635
+ }
636
+
637
+ const toast = this.querySelector("sonic-toast-message-subscriber");
638
+ if (toast) this.insertBefore(rendered, toast);
639
+ else this.appendChild(rendered);
640
+ }
641
+
642
+ private bindOpsDataProvider() {
643
+ this.opsUnsub?.();
644
+ this.opsUnsub = undefined;
645
+ const id = this.opsDataProvider;
646
+ if (!id) return;
647
+ const pub = getPublisherFromPath(id);
648
+ if (!pub) return;
649
+ const handler = (payload: unknown) => {
650
+ if (payload == null) return;
651
+ const list = Array.isArray(payload) ? payload : [payload];
652
+ for (const item of list) {
653
+ if (item && typeof item === "object" && "op" in (item as object)) {
654
+ this.applyOp(item as SDUIOp);
655
+ }
656
+ }
657
+ };
658
+ pub.onAssign(handler);
659
+ this.opsUnsub = () => pub.offAssign(handler);
660
+ const current = pub.get();
661
+ if (current != null) handler(current);
662
+ }
255
663
  }
@@ -3,18 +3,56 @@ export type SDUIDescriptor = {
3
3
  css?: Array<string>;
4
4
  library?: Record<string, SDUINode>;
5
5
  nodes?: Array<SDUINode>;
6
+ /**
7
+ * Registre plat de nœuds indexés par `nodeId` (opt-in).
8
+ * Permet de référencer des enfants via `childNodeIds` avant qu'ils n'arrivent
9
+ * (rendu partiel / ops incrémentales).
10
+ */
11
+ nodeById?: Record<string, SDUINode>;
6
12
  };
13
+ /** Action déclarative sur un nœud — émet l'événement `sdui-action` au clic. */
14
+ export type SDUINodeAction = {
15
+ name: string;
16
+ context?: Record<string, unknown>;
17
+ };
18
+
19
+ /** Detail de l'événement CustomEvent `sdui-action`. */
20
+ export type SDUIActionDetail = {
21
+ name: string;
22
+ context: Record<string, unknown>;
23
+ /** Renseigné plus tard si le nœud porte un `nodeId` (ops incrémentales). */
24
+ sourceNodeId?: string;
25
+ };
26
+
7
27
  export type SDUINode = {
8
28
  libraryKey?: string;
9
29
  markup?: string;
10
30
  tagName?: string;
11
31
  attributes?: Record<string, string>;
12
32
  nodes?: Array<SDUINode>;
33
+ /** Texte plain (pas de parse HTML). Autorisé en profile="safe". Ignoré si `nodes` est non vide. */
34
+ textContent?: string;
13
35
  innerHTML?: string;
14
36
  prefix?: string;
15
37
  suffix?: string;
16
38
  contentElementSelector?: string;
17
39
  parentElementSelector?: string;
40
+ /**
41
+ * Action déclarative opt-in. Au clic, émet `sdui-action`
42
+ * (bubbles + composed) avec `{ name, context, sourceNodeId? }`.
43
+ * N'interfère pas avec sonic-submit / POST formulaire.
44
+ */
45
+ action?: SDUINodeAction;
46
+ /**
47
+ * Identifiant logique de nœud (pas l'attribut DOM `id`).
48
+ * Utilisé dans `sdui-action.detail.sourceNodeId` ; ops incrémentales plus tard.
49
+ */
50
+ nodeId?: string;
51
+ /**
52
+ * Enfants référencés par `nodeId` (registre `descriptor.nodeById` + index du tree).
53
+ * Si un id est absent : placeholder en mode partial, sinon warning + skip.
54
+ */
55
+ childNodeIds?: string[];
18
56
  };
19
57
 
20
58
  export type SDUITransformDescription = {
@@ -32,3 +70,14 @@ export type SDUITransformAction = {
32
70
  ui?: SDUINode;
33
71
  };
34
72
  export type SDUITransformList = Array<{parent: SDUINode; child: SDUINode; index: number}>;
73
+
74
+ /** Opération incrémentale (méthodes publiques ou flux opsDataProvider). */
75
+ export type SDUIOp =
76
+ | {
77
+ op: "upsertNode";
78
+ node: SDUINode;
79
+ parentId?: string;
80
+ index?: number;
81
+ }
82
+ | { op: "removeNode"; nodeId: string }
83
+ | { op: "setData"; path: string; value: unknown };
File without changes
@@ -0,0 +1 @@
1
+ export type Align = "left" | "center" | "right";