@supersoniks/concorde 4.9.7 → 4.9.91

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 (72) hide show
  1. package/build-infos.json +1 -1
  2. package/concorde-core.bundle.js +265 -265
  3. package/concorde-core.es.js +4600 -4385
  4. package/dist/concorde-core.bundle.js +265 -265
  5. package/dist/concorde-core.es.js +4600 -4385
  6. package/dist/js/core/components/functional/queue/queue.js +25 -153
  7. package/dist/js/core/components/functional/queue/queue.js.map +1 -1
  8. package/dist/js/core/components/ui/_css/scroll.js +2 -0
  9. package/dist/js/core/components/ui/_css/scroll.js.map +1 -1
  10. package/dist/js/core/decorators/api.js +92 -2
  11. package/dist/js/core/decorators/api.js.map +1 -1
  12. package/dist/js/core/utils/SafariScrollGuard.js +384 -0
  13. package/dist/js/core/utils/SafariScrollGuard.js.map +1 -0
  14. package/dist/js/decorators.js +4 -0
  15. package/dist/js/decorators.js.map +1 -1
  16. package/dist/js/utils.js +3 -0
  17. package/dist/js/utils.js.map +1 -1
  18. package/dist/types/core/components/functional/queue/queue.d.ts +8 -24
  19. package/dist/types/core/components/functional/queue/queue.d.ts.map +1 -1
  20. package/dist/types/core/components/ui/_css/scroll.d.ts.map +1 -1
  21. package/dist/types/core/decorators/api.d.ts +82 -60
  22. package/dist/types/core/decorators/api.d.ts.map +1 -1
  23. package/dist/types/core/utils/SafariScrollGuard.d.ts +36 -0
  24. package/dist/types/core/utils/SafariScrollGuard.d.ts.map +1 -0
  25. package/dist/types/decorators.d.ts +3 -1
  26. package/dist/types/decorators.d.ts.map +1 -1
  27. package/dist/types/utils.d.ts +1 -0
  28. package/dist/types/utils.d.ts.map +1 -1
  29. package/docs/assets/index-BL6WVF7g.css +1 -0
  30. package/docs/assets/{index-hElUgV9_.js → index-DVn8AeyA.js} +572 -555
  31. package/docs/crawl/docs/_decorators/get.html +64 -81
  32. package/docs/crawl/docs/_decorators/patch.html +35 -23
  33. package/docs/crawl/docs/_decorators/post.html +72 -72
  34. package/docs/crawl/docs/_decorators/put.html +36 -21
  35. package/docs/crawl/docs/_getting-started/ai-agents.html +45 -6
  36. package/docs/crawl/hubs/decorators.html +141 -69
  37. package/docs/crawl/hubs/remote-data.html +141 -69
  38. package/docs/index.html +2 -2
  39. package/docs/llms-full.txt +203 -149
  40. package/docs/llms.txt +4 -4
  41. package/docs/src/core/components/functional/queue/queue.md +0 -0
  42. package/docs/src/core/components/ui/button/button.md +0 -0
  43. package/docs/src/docs/_decorators/get.md +55 -81
  44. package/docs/src/docs/_decorators/patch.md +36 -25
  45. package/docs/src/docs/_decorators/post.md +71 -62
  46. package/docs/src/docs/_decorators/put.md +37 -24
  47. package/docs/src/docs/_getting-started/ai-agents.md +24 -4
  48. package/docs/src/docs/search/docs-search.json +151 -1
  49. package/docs/src/tsconfig.json +3 -0
  50. package/package.json +6 -2
  51. package/src/core/components/functional/queue/queue.ts +33 -164
  52. package/src/core/components/ui/_css/scroll.ts +3 -0
  53. package/src/core/decorators/api.post.spec.ts +79 -1
  54. package/src/core/decorators/api.spec.ts +100 -1
  55. package/src/core/decorators/api.ts +252 -138
  56. package/src/core/utils/SafariScrollGuard.ts +429 -0
  57. package/src/decorators.ts +7 -0
  58. package/src/docs/_decorators/get.md +81 -80
  59. package/src/docs/_decorators/patch.md +36 -25
  60. package/src/docs/_decorators/post.md +99 -62
  61. package/src/docs/_decorators/put.md +37 -24
  62. package/src/docs/components/docs-demo-sources.ts +1 -6
  63. package/src/docs/example/decorators-demo-geo.ts +0 -21
  64. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +41 -132
  65. package/src/docs/search/docs-search.json +156 -241
  66. package/src/docs/tailwind/css/tailwind.css +0 -0
  67. package/src/tsconfig.json +3 -0
  68. package/src/tsconfig.tsbuildinfo +1 -1
  69. package/src/utils.ts +13 -0
  70. package/vite.config.mts +4 -0
  71. package/vitest.setup.ts +22 -0
  72. package/docs/assets/index-Bnp7gVaA.css +0 -1
@@ -4744,6 +4744,51 @@
4744
4744
  }
4745
4745
  }
4746
4746
  },
4747
+ {
4748
+ "search": "The decorator returned by get(...) can be kept as a typed handle. Its refetch(component) action targets only the connected component instance and works even before the first ApiResult exists:\n",
4749
+ "files": {
4750
+ "docs/_decorators/get.md": {
4751
+ "title": "@get",
4752
+ "hashes": {
4753
+ "local-refetch-handle": {
4754
+ "count": 1,
4755
+ "title": "Local refetch handle",
4756
+ "type": "paragraph"
4757
+ }
4758
+ }
4759
+ }
4760
+ }
4761
+ },
4762
+ {
4763
+ "search": "The inline form remains unchanged:\n",
4764
+ "files": {
4765
+ "docs/_decorators/get.md": {
4766
+ "title": "@get",
4767
+ "hashes": {
4768
+ "local-refetch-handle": {
4769
+ "count": 1,
4770
+ "title": "Local refetch handle",
4771
+ "type": "paragraph"
4772
+ }
4773
+ }
4774
+ }
4775
+ }
4776
+ },
4777
+ {
4778
+ "search": "For a refetch triggered from another component, keep using triggerKey and invalidate().\n",
4779
+ "files": {
4780
+ "docs/_decorators/get.md": {
4781
+ "title": "@get",
4782
+ "hashes": {
4783
+ "local-refetch-handle": {
4784
+ "count": 1,
4785
+ "title": "Local refetch handle",
4786
+ "type": "paragraph"
4787
+ }
4788
+ }
4789
+ }
4790
+ }
4791
+ },
4747
4792
  {
4748
4793
  "search": "Pass as the second argument (scoped config) or third (with a configuration key). Same shape as send decorators where applicable:\n",
4749
4794
  "files": {
@@ -4759,6 +4804,51 @@
4759
4804
  }
4760
4805
  }
4761
4806
  },
4807
+ {
4808
+ "search": "Same pattern as @post: a publisher acts as a signal, independent of the response payload.\n",
4809
+ "files": {
4810
+ "docs/_decorators/get.md": {
4811
+ "title": "@get",
4812
+ "hashes": {
4813
+ "manual-refetch-triggerkey": {
4814
+ "count": 1,
4815
+ "title": "Manual refetch (`triggerKey`)",
4816
+ "type": "paragraph"
4817
+ }
4818
+ }
4819
+ }
4820
+ }
4821
+ },
4822
+ {
4823
+ "search": "Prefer triggerKey over mutating the API configuration publisher when you only want to reload the same URL — no need to touch serviceURL, tokens, etc.\n",
4824
+ "files": {
4825
+ "docs/_decorators/get.md": {
4826
+ "title": "@get",
4827
+ "hashes": {
4828
+ "manual-refetch-triggerkey": {
4829
+ "count": 1,
4830
+ "title": "Manual refetch (`triggerKey`)",
4831
+ "type": "paragraph"
4832
+ }
4833
+ }
4834
+ }
4835
+ }
4836
+ },
4837
+ {
4838
+ "search": "The docs mock adds an X-Fetched-At response header on /docs-mock-api/geo/communes so live demos can show when the last fetch happened (response.headers.get("X-Fetched-At")).\n",
4839
+ "files": {
4840
+ "docs/_decorators/get.md": {
4841
+ "title": "@get",
4842
+ "hashes": {
4843
+ "manual-refetch-triggerkey": {
4844
+ "count": 1,
4845
+ "title": "Manual refetch (`triggerKey`)",
4846
+ "type": "paragraph"
4847
+ }
4848
+ }
4849
+ }
4850
+ }
4851
+ },
4762
4852
  {
4763
4853
  "search": "Same demo service as sonic-queue (/docs-mock-api/geo/). Publisher setup lives in decorators-demo-geo.ts and decorators-demo-subscribe-publish-get-demos.ts.\n",
4764
4854
  "files": {
@@ -4775,7 +4865,22 @@
4775
4865
  }
4776
4866
  },
4777
4867
  {
4778
- "search": "Dynamic config and endpoint path (demo-api-get-configuration-key in doc sources):\n",
4868
+ "search": "@get with triggerKey — refresh button + X-Fetched-At timestamp in the UI:\n",
4869
+ "files": {
4870
+ "docs/_decorators/get.md": {
4871
+ "title": "@get",
4872
+ "hashes": {
4873
+ "live-demos": {
4874
+ "count": 1,
4875
+ "title": "Live demos",
4876
+ "type": "paragraph"
4877
+ }
4878
+ }
4879
+ }
4880
+ }
4881
+ },
4882
+ {
4883
+ "search": "Dynamic config and endpoint path. Manual refetch is triggered from a separate component (same triggerKey publisher):\n",
4779
4884
  "files": {
4780
4885
  "docs/_decorators/get.md": {
4781
4886
  "title": "@get",
@@ -5884,6 +5989,36 @@
5884
5989
  }
5885
5990
  }
5886
5991
  },
5992
+ {
5993
+ "search": "The decorator returned by post(...) can be kept as a typed handle. Its send(component) action re-runs the POST with the current body publisher:\n",
5994
+ "files": {
5995
+ "docs/_decorators/post.md": {
5996
+ "title": "@post",
5997
+ "hashes": {
5998
+ "local-send-handle": {
5999
+ "count": 1,
6000
+ "title": "Local send handle",
6001
+ "type": "paragraph"
6002
+ }
6003
+ }
6004
+ }
6005
+ }
6006
+ },
6007
+ {
6008
+ "search": "send() is an explicit write operation. It is intentionally distinct from GET invalidation because replaying a POST, PUT, or PATCH can have side effects.\n",
6009
+ "files": {
6010
+ "docs/_decorators/post.md": {
6011
+ "title": "@post",
6012
+ "hashes": {
6013
+ "local-send-handle": {
6014
+ "count": 1,
6015
+ "title": "Local send handle",
6016
+ "type": "paragraph"
6017
+ }
6018
+ }
6019
+ }
6020
+ }
6021
+ },
5887
6022
  {
5888
6023
  "search": "Mock service: POST /docs-mock-api/api/register (same route as sonic-submit demos). Publisher setup lives in decorators-demo-post.ts.\n",
5889
6024
  "files": {
@@ -6874,6 +7009,21 @@
6874
7009
  }
6875
7010
  }
6876
7011
  },
7012
+ {
7013
+ "search": "Everything after <!-- concorde-ai --> is replaced on each ai-init run (idempotent). Do not paste a full copy of the Concorde guide above the marker — put project-specific notes in AGENTS.concorde.md (overlay). If the preamble duplicates package AGENTS.md, ai-init slims or drops it and prints a warning. Legacy filenames AGENTS.overlay.md / AGENTS.starter.md are still accepted with a deprecation warning.\n",
7014
+ "files": {
7015
+ "docs/_getting-started/ai-agents.md": {
7016
+ "title": "AI agents (skills & AGENTS.md)",
7017
+ "hashes": {
7018
+ "agentsmd-policy-non-invasive": {
7019
+ "count": 1,
7020
+ "title": "`AGENTS.md` policy (non-invasive)",
7021
+ "type": "paragraph"
7022
+ }
7023
+ }
7024
+ }
7025
+ }
7026
+ },
6877
7027
  {
6878
7028
  "search": "Source files in the npm package: node_modules/@supersoniks/concorde/ai/ (see also ai/README.md in the Concorde repository).\n",
6879
7029
  "files": {
@@ -789,6 +789,9 @@
789
789
  "./utils/PublisherProxy": [
790
790
  "/sites/concorde/src/core/utils/PublisherProxy.ts"
791
791
  ],
792
+ "./utils/SafariScrollGuard": [
793
+ "/sites/concorde/src/core/utils/SafariScrollGuard.ts"
794
+ ],
792
795
  "./utils/Utils": [
793
796
  "/sites/concorde/src/core/utils/Utils.ts"
794
797
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supersoniks/concorde",
3
- "version": "4.9.7",
3
+ "version": "4.9.91",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "",
@@ -13,7 +13,7 @@
13
13
  "postbuild-docs": "node scripts/post-build-docs.js",
14
14
  "prepublishOnly": "node scripts/pre-publish.mjs",
15
15
  "test": "vitest --reporter verbose --watch",
16
- "test:ci": "vitest run --reporter verbose",
16
+ "test:ci": "vitest run --reporter verbose && node --test scripts/ai-init.test.mjs",
17
17
  "ai-init": "node scripts/ai-init.mjs"
18
18
  },
19
19
  "exports": {
@@ -1062,6 +1062,10 @@
1062
1062
  "types": "./dist/types/core/utils/PublisherProxy.d.ts",
1063
1063
  "default": "./dist/js/core/utils/PublisherProxy.js"
1064
1064
  },
1065
+ "./utils/SafariScrollGuard": {
1066
+ "types": "./dist/types/core/utils/SafariScrollGuard.d.ts",
1067
+ "default": "./dist/js/core/utils/SafariScrollGuard.js"
1068
+ },
1065
1069
  "./utils/Utils": {
1066
1070
  "types": "./dist/types/core/utils/Utils.d.ts",
1067
1071
  "default": "./dist/js/core/utils/Utils.js"
@@ -5,6 +5,13 @@ import { map } from "lit/directives/map.js";
5
5
  import DataProvider, { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy";
6
6
  import "@supersoniks/concorde/core/components/functional/list/list";
7
7
  import { HTML } from "@supersoniks/concorde/utils";
8
+ import {
9
+ isSafariScrollEngine,
10
+ resetScrollRoot,
11
+ restoreScrollRoot,
12
+ watchScrollRoot,
13
+ type ScrollRoot,
14
+ } from "@supersoniks/concorde/core/utils/SafariScrollGuard";
8
15
  import { ListItems } from "@supersoniks/concorde/core/components/functional/list/list";
9
16
 
10
17
  type QueueItem = {
@@ -74,7 +81,7 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
74
81
  this.requestId = 0;
75
82
  this.isFirstRequest = true;
76
83
  this.nextHadEvent = false;
77
- this.teardownScrollGuard();
84
+ this.teardownScrollSync();
78
85
  this.publisher.set({});
79
86
  super.disconnectedCallback();
80
87
  return;
@@ -106,8 +113,7 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
106
113
  HTML.getAncestorAttributeValue(this.parentElement, "dataProvider") ||
107
114
  "";
108
115
  }
109
- this.captureScrollPosition();
110
- this.ensureScrollGuard();
116
+ this.ensureScrollSync();
111
117
  super.connectedCallback();
112
118
  this.publisher.set({});
113
119
  this.key = this.getAttribute("key");
@@ -164,8 +170,7 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
164
170
  if (searchHash == this.searchHash && !this.isFirstRequest) return;
165
171
  this.searchHash = searchHash;
166
172
  // Nouvelle recherche : ne pas restaurer l'ancienne position de scroll
167
- this.storedScrollX = 0;
168
- this.storedScrollY = 0;
173
+ resetScrollRoot(this.resolveScrollRoot(), { x: 0, y: 0 });
169
174
 
170
175
  /**
171
176
  * on reset les données avant de lancer la requète
@@ -193,23 +198,15 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
193
198
  };
194
199
 
195
200
  /**
196
- * Conteneur scrollé (élément ou window) — Safari / iOS only.
197
- * Stock = dernière position user (touch/wheel/pointer + settle inertie).
198
- * Restore = updated (enfants Lit) + MutationObserver (subscribers / modal).
201
+ * Conteneur scrollé — Safari : stock/restore via SafariScrollGuard
202
+ * (capture intent user globale). Ici : sync root + restore après Lit.
199
203
  */
200
- scrollRoot: HTMLElement | Window | null = null;
201
- storedScrollY = 0;
202
- storedScrollX = 0;
203
- private static readonly SCROLL_SETTLE_MS = 150;
204
- private userScrollEngaged = false;
205
- private restoringScroll = false;
206
- private scrollGuardActive = false;
207
- private scrollSettleTimer?: ReturnType<typeof setTimeout>;
204
+ scrollRoot: ScrollRoot | null = null;
205
+ private scrollSyncActive = false;
208
206
  private mutationObserver: MutationObserver | null = null;
209
- private boundScrollTarget: EventTarget | null = null;
210
207
  private restoreGeneration = 0;
211
208
 
212
- resolveScrollRoot(): HTMLElement | Window {
209
+ resolveScrollRoot(): ScrollRoot {
213
210
  const ancestor = HTML.getScrollingAncestor(this);
214
211
  if (
215
212
  !ancestor ||
@@ -221,136 +218,21 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
221
218
  return ancestor as HTMLElement;
222
219
  }
223
220
 
224
- /** Lit la position courante — ne pas appeler sur un scroll “fantôme” Safari. */
225
- captureScrollPosition() {
226
- if (!this.isSafari()) {
227
- return;
228
- }
229
- const root = this.resolveScrollRoot();
230
- this.scrollRoot = root;
231
- if (root === window) {
232
- this.storedScrollX = window.scrollX;
233
- this.storedScrollY = window.scrollY;
234
- } else {
235
- const el = root as HTMLElement;
236
- this.storedScrollX = el.scrollLeft;
237
- this.storedScrollY = el.scrollTop;
238
- }
239
- }
240
-
241
- restoreScrollPosition() {
242
- if (!this.isSafari()) {
243
- return;
244
- }
245
- const root = this.scrollRoot ?? this.resolveScrollRoot();
246
- const x = this.storedScrollX;
247
- const y = this.storedScrollY;
248
- this.restoringScroll = true;
249
- try {
250
- if (root === window) {
251
- if (
252
- Math.abs(x - window.scrollX) > 10 ||
253
- Math.abs(y - window.scrollY) > 10
254
- ) {
255
- window.scrollTo(x, y);
256
- }
257
- } else {
258
- const el = root as HTMLElement;
259
- if (
260
- Math.abs(x - el.scrollLeft) > 10 ||
261
- Math.abs(y - el.scrollTop) > 10
262
- ) {
263
- el.scrollLeft = x;
264
- el.scrollTop = y;
265
- }
266
- }
267
- } finally {
268
- requestAnimationFrame(() => {
269
- this.restoringScroll = false;
270
- });
271
- }
272
- }
273
-
274
221
  isSafari() {
275
- return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
222
+ return isSafariScrollEngine();
276
223
  }
277
224
 
278
- private markUserScrollEngaged = () => {
279
- this.userScrollEngaged = true;
280
- };
281
-
282
- private armScrollSettle = () => {
283
- clearTimeout(this.scrollSettleTimer);
284
- this.scrollSettleTimer = setTimeout(() => {
285
- this.userScrollEngaged = false;
286
- this.scrollSettleTimer = undefined;
287
- }, Queue.SCROLL_SETTLE_MS);
288
- };
289
-
290
- private onScrollRootScroll = () => {
291
- if (!this.isSafari() || this.restoringScroll || !this.userScrollEngaged) {
292
- return;
293
- }
294
- this.captureScrollPosition();
295
- // iOS : l’inertie continue après touchend → prolonger l’engagement
296
- this.armScrollSettle();
297
- };
298
-
299
- private onUserScrollIntent = () => {
300
- if (!this.isSafari()) return;
301
- this.markUserScrollEngaged();
302
- };
303
-
304
- private onUserScrollIntentEnd = () => {
305
- if (!this.isSafari()) return;
306
- this.armScrollSettle();
307
- };
308
-
309
- private onWheelIntent = () => {
310
- if (!this.isSafari()) return;
311
- this.markUserScrollEngaged();
312
- this.armScrollSettle();
313
- };
314
-
315
- ensureScrollGuard() {
316
- if (!this.isSafari() || !this.isConnected) {
225
+ ensureScrollSync() {
226
+ if (!isSafariScrollEngine() || !this.isConnected) {
317
227
  return;
318
228
  }
319
229
  const root = this.resolveScrollRoot();
320
- const target: EventTarget = root === window ? window : root;
321
- if (this.scrollGuardActive && this.boundScrollTarget === target) {
322
- this.scrollRoot = root;
230
+ this.scrollRoot = root;
231
+ watchScrollRoot(root);
232
+ if (this.scrollSyncActive) {
323
233
  return;
324
234
  }
325
- this.teardownScrollGuard();
326
- this.scrollRoot = root;
327
- this.boundScrollTarget = target;
328
- this.scrollGuardActive = true;
329
-
330
- target.addEventListener("scroll", this.onScrollRootScroll, {
331
- passive: true,
332
- });
333
- // Intent user : touch (iOS), wheel, pointer (scrollbar desktop Safari)
334
- target.addEventListener("touchstart", this.onUserScrollIntent, {
335
- passive: true,
336
- });
337
- target.addEventListener("touchend", this.onUserScrollIntentEnd, {
338
- passive: true,
339
- });
340
- target.addEventListener("touchcancel", this.onUserScrollIntentEnd, {
341
- passive: true,
342
- });
343
- target.addEventListener("wheel", this.onWheelIntent, { passive: true });
344
- target.addEventListener("pointerdown", this.onUserScrollIntent, {
345
- passive: true,
346
- });
347
- target.addEventListener("pointerup", this.onUserScrollIntentEnd, {
348
- passive: true,
349
- });
350
- target.addEventListener("pointercancel", this.onUserScrollIntentEnd, {
351
- passive: true,
352
- });
353
-
235
+ this.scrollSyncActive = true;
354
236
  this.mutationObserver = new MutationObserver(() => {
355
237
  this.scheduleRestoreAfterChildren();
356
238
  });
@@ -361,27 +243,11 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
361
243
  });
362
244
  }
363
245
 
364
- teardownScrollGuard() {
246
+ teardownScrollSync() {
365
247
  this.restoreGeneration++;
366
- clearTimeout(this.scrollSettleTimer);
367
- this.scrollSettleTimer = undefined;
368
- this.userScrollEngaged = false;
369
- this.restoringScroll = false;
370
- if (this.boundScrollTarget) {
371
- const target = this.boundScrollTarget;
372
- target.removeEventListener("scroll", this.onScrollRootScroll);
373
- target.removeEventListener("touchstart", this.onUserScrollIntent);
374
- target.removeEventListener("touchend", this.onUserScrollIntentEnd);
375
- target.removeEventListener("touchcancel", this.onUserScrollIntentEnd);
376
- target.removeEventListener("wheel", this.onWheelIntent);
377
- target.removeEventListener("pointerdown", this.onUserScrollIntent);
378
- target.removeEventListener("pointerup", this.onUserScrollIntentEnd);
379
- target.removeEventListener("pointercancel", this.onUserScrollIntentEnd);
380
- this.boundScrollTarget = null;
381
- }
382
248
  this.mutationObserver?.disconnect();
383
249
  this.mutationObserver = null;
384
- this.scrollGuardActive = false;
250
+ this.scrollSyncActive = false;
385
251
  this.scrollRoot = null;
386
252
  }
387
253
 
@@ -390,7 +256,7 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
390
256
  * Ne pas await updateComplete *dans* updated (deadlock) — on sort du cycle via void async.
391
257
  */
392
258
  scheduleRestoreAfterChildren() {
393
- if (!this.isSafari() || !this.isConnected) {
259
+ if (!isSafariScrollEngine() || !this.isConnected) {
394
260
  return;
395
261
  }
396
262
  const generation = ++this.restoreGeneration;
@@ -410,14 +276,17 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
410
276
  await new Promise<void>((r) => requestAnimationFrame(() => r()));
411
277
  if (generation !== this.restoreGeneration || !this.isConnected) return;
412
278
 
413
- this.restoreScrollPosition();
279
+ const root = this.scrollRoot ?? this.resolveScrollRoot();
280
+ watchScrollRoot(root);
281
+ restoreScrollRoot(root);
414
282
  })();
415
283
  }
416
284
 
417
285
  protected updated(_changedProperties: PropertyValues): void {
418
- if (this.isSafari()) {
419
- this.ensureScrollGuard();
420
- this.restoreScrollPosition();
286
+ if (isSafariScrollEngine()) {
287
+ this.ensureScrollSync();
288
+ const root = this.scrollRoot ?? this.resolveScrollRoot();
289
+ restoreScrollRoot(root);
421
290
  this.scheduleRestoreAfterChildren();
422
291
  }
423
292
 
@@ -533,7 +402,7 @@ export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
533
402
  // Ne pas écraser le stock avec un jump Safari : uniquement sync du root / guard.
534
403
  if (this.isSafari()) {
535
404
  this.scrollRoot = this.resolveScrollRoot();
536
- this.ensureScrollGuard();
405
+ this.ensureScrollSync();
537
406
  }
538
407
  if (!Array.isArray(this.props)) {
539
408
  return nothing;
@@ -1,4 +1,7 @@
1
1
  import { css } from "lit";
2
+ import { installSafariScrollGuard } from "@supersoniks/concorde/core/utils/SafariScrollGuard";
3
+
4
+ installSafariScrollGuard();
2
5
 
3
6
  export const customScroll = css`
4
7
  .custom-scroll {
@@ -5,7 +5,7 @@ import API, {
5
5
  type ApiResult,
6
6
  } from "@supersoniks/concorde/core/utils/api";
7
7
  import { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy";
8
- import { patch, post, put } from "./api";
8
+ import { patch, post, put, send } from "./api";
9
9
 
10
10
  async function flushSendFrames() {
11
11
  await new Promise<void>((resolve) => {
@@ -68,6 +68,84 @@ describe("post", () => {
68
68
  expect(el.payload?.result).toEqual({ ok: true });
69
69
  });
70
70
 
71
+ it("expose un send local avec le body courant", async () => {
72
+ const bodyPubId = "postSpecLocalSend";
73
+ const pub = PublisherManager.get(bodyPubId);
74
+ pub.set({ value: "first" });
75
+ const bodyKey = new DataProviderKey<{ value: string }>(bodyPubId);
76
+ const syncEp = new Endpoint<{ ok: boolean }>("items/local-send");
77
+ const localPost = post(syncEp, bodyKey);
78
+ const tag = "test-api-post-local-send";
79
+
80
+ class C extends HTMLElement {
81
+ @localPost
82
+ payload: ApiResult<{ ok: boolean }> | null = null;
83
+ }
84
+ if (!customElements.get(tag)) {
85
+ customElements.define(tag, C);
86
+ }
87
+
88
+ const wrap = document.createElement("div");
89
+ wrap.setAttribute("serviceURL", "https://api.example.test");
90
+ const el = document.createElement(tag) as InstanceType<typeof C>;
91
+ wrap.appendChild(el);
92
+ document.body.appendChild(wrap);
93
+
94
+ await flushSendFrames();
95
+ vi.mocked(API.prototype.postDetailed).mockClear();
96
+
97
+ pub.set({ value: "second" });
98
+ localPost.send(el);
99
+ await flushSendFrames();
100
+
101
+ expect(API.prototype.postDetailed).toHaveBeenCalledTimes(1);
102
+ expect(API.prototype.postDetailed).toHaveBeenCalledWith(
103
+ "items/local-send",
104
+ { value: "second" },
105
+ );
106
+ });
107
+
108
+ it("peut décorer une méthode avec send après sa résolution", async () => {
109
+ const bodyPubId = "postSpecMethodSend";
110
+ const pub = PublisherManager.get(bodyPubId);
111
+ pub.set({ value: "first" });
112
+ const bodyKey = new DataProviderKey<{ value: string }>(bodyPubId);
113
+ const syncEp = new Endpoint<{ ok: boolean }>("items/method-send");
114
+ const localPost = post(syncEp, bodyKey);
115
+ const tag = "test-api-post-method-send";
116
+
117
+ class C extends HTMLElement {
118
+ @localPost
119
+ payload: ApiResult<{ ok: boolean }> | null = null;
120
+
121
+ @send(localPost)
122
+ async save() {
123
+ await Promise.resolve();
124
+ pub.set({ value: "after" });
125
+ }
126
+ }
127
+ if (!customElements.get(tag)) {
128
+ customElements.define(tag, C);
129
+ }
130
+
131
+ const wrap = document.createElement("div");
132
+ wrap.setAttribute("serviceURL", "https://api.example.test");
133
+ const el = document.createElement(tag) as InstanceType<typeof C>;
134
+ wrap.appendChild(el);
135
+ document.body.appendChild(wrap);
136
+
137
+ await flushSendFrames();
138
+ vi.mocked(API.prototype.postDetailed).mockClear();
139
+
140
+ await el.save();
141
+ await flushSendFrames();
142
+
143
+ expect(API.prototype.postDetailed).toHaveBeenCalledWith(
144
+ "items/method-send",
145
+ { value: "after" },
146
+ );
147
+ });
148
+
71
149
  it("endpoint dynamique et relance quand le body publisher mute", async () => {
72
150
  const bodyPubId = "postSpecBody2";
73
151
  const pub = PublisherManager.get(bodyPubId);