@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.
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +265 -265
- package/concorde-core.es.js +4600 -4385
- package/dist/concorde-core.bundle.js +265 -265
- package/dist/concorde-core.es.js +4600 -4385
- package/dist/js/core/components/functional/queue/queue.js +25 -153
- package/dist/js/core/components/functional/queue/queue.js.map +1 -1
- package/dist/js/core/components/ui/_css/scroll.js +2 -0
- package/dist/js/core/components/ui/_css/scroll.js.map +1 -1
- package/dist/js/core/decorators/api.js +92 -2
- package/dist/js/core/decorators/api.js.map +1 -1
- package/dist/js/core/utils/SafariScrollGuard.js +384 -0
- package/dist/js/core/utils/SafariScrollGuard.js.map +1 -0
- package/dist/js/decorators.js +4 -0
- package/dist/js/decorators.js.map +1 -1
- package/dist/js/utils.js +3 -0
- package/dist/js/utils.js.map +1 -1
- package/dist/types/core/components/functional/queue/queue.d.ts +8 -24
- package/dist/types/core/components/functional/queue/queue.d.ts.map +1 -1
- package/dist/types/core/components/ui/_css/scroll.d.ts.map +1 -1
- package/dist/types/core/decorators/api.d.ts +82 -60
- package/dist/types/core/decorators/api.d.ts.map +1 -1
- package/dist/types/core/utils/SafariScrollGuard.d.ts +36 -0
- package/dist/types/core/utils/SafariScrollGuard.d.ts.map +1 -0
- package/dist/types/decorators.d.ts +3 -1
- package/dist/types/decorators.d.ts.map +1 -1
- package/dist/types/utils.d.ts +1 -0
- package/dist/types/utils.d.ts.map +1 -1
- package/docs/assets/index-BL6WVF7g.css +1 -0
- package/docs/assets/{index-hElUgV9_.js → index-DVn8AeyA.js} +572 -555
- package/docs/crawl/docs/_decorators/get.html +64 -81
- package/docs/crawl/docs/_decorators/patch.html +35 -23
- package/docs/crawl/docs/_decorators/post.html +72 -72
- package/docs/crawl/docs/_decorators/put.html +36 -21
- package/docs/crawl/docs/_getting-started/ai-agents.html +45 -6
- package/docs/crawl/hubs/decorators.html +141 -69
- package/docs/crawl/hubs/remote-data.html +141 -69
- package/docs/index.html +2 -2
- package/docs/llms-full.txt +203 -149
- package/docs/llms.txt +4 -4
- package/docs/src/core/components/functional/queue/queue.md +0 -0
- package/docs/src/core/components/ui/button/button.md +0 -0
- package/docs/src/docs/_decorators/get.md +55 -81
- package/docs/src/docs/_decorators/patch.md +36 -25
- package/docs/src/docs/_decorators/post.md +71 -62
- package/docs/src/docs/_decorators/put.md +37 -24
- package/docs/src/docs/_getting-started/ai-agents.md +24 -4
- package/docs/src/docs/search/docs-search.json +151 -1
- package/docs/src/tsconfig.json +3 -0
- package/package.json +6 -2
- package/src/core/components/functional/queue/queue.ts +33 -164
- package/src/core/components/ui/_css/scroll.ts +3 -0
- package/src/core/decorators/api.post.spec.ts +79 -1
- package/src/core/decorators/api.spec.ts +100 -1
- package/src/core/decorators/api.ts +252 -138
- package/src/core/utils/SafariScrollGuard.ts +429 -0
- package/src/decorators.ts +7 -0
- package/src/docs/_decorators/get.md +81 -80
- package/src/docs/_decorators/patch.md +36 -25
- package/src/docs/_decorators/post.md +99 -62
- package/src/docs/_decorators/put.md +37 -24
- package/src/docs/components/docs-demo-sources.ts +1 -6
- package/src/docs/example/decorators-demo-geo.ts +0 -21
- package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +41 -132
- package/src/docs/search/docs-search.json +156 -241
- package/src/docs/tailwind/css/tailwind.css +0 -0
- package/src/tsconfig.json +3 -0
- package/src/tsconfig.tsbuildinfo +1 -1
- package/src/utils.ts +13 -0
- package/vite.config.mts +4 -0
- package/vitest.setup.ts +22 -0
- 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": "
|
|
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": {
|
package/docs/src/tsconfig.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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é
|
|
197
|
-
*
|
|
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:
|
|
201
|
-
|
|
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():
|
|
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
|
|
222
|
+
return isSafariScrollEngine();
|
|
276
223
|
}
|
|
277
224
|
|
|
278
|
-
|
|
279
|
-
this.
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
230
|
+
this.scrollRoot = root;
|
|
231
|
+
watchScrollRoot(root);
|
|
232
|
+
if (this.scrollSyncActive) {
|
|
323
233
|
return;
|
|
324
234
|
}
|
|
325
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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 (!
|
|
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.
|
|
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 (
|
|
419
|
-
this.
|
|
420
|
-
this.
|
|
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.
|
|
405
|
+
this.ensureScrollSync();
|
|
537
406
|
}
|
|
538
407
|
if (!Array.isArray(this.props)) {
|
|
539
408
|
return nothing;
|
|
@@ -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);
|