@supersoniks/concorde 4.9.6 → 4.9.9

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 (31) hide show
  1. package/build-infos.json +1 -1
  2. package/concorde-core.bundle.js +190 -190
  3. package/concorde-core.es.js +4135 -3905
  4. package/dist/concorde-core.bundle.js +190 -190
  5. package/dist/concorde-core.es.js +4135 -3905
  6. package/dist/js/core/components/functional/queue/queue.js +71 -37
  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/utils/SafariScrollGuard.js +384 -0
  11. package/dist/js/core/utils/SafariScrollGuard.js.map +1 -0
  12. package/dist/js/utils.js +3 -0
  13. package/dist/js/utils.js.map +1 -1
  14. package/dist/types/core/components/functional/queue/queue.d.ts +17 -7
  15. package/dist/types/core/components/functional/queue/queue.d.ts.map +1 -1
  16. package/dist/types/core/components/ui/_css/scroll.d.ts.map +1 -1
  17. package/dist/types/core/utils/SafariScrollGuard.d.ts +36 -0
  18. package/dist/types/core/utils/SafariScrollGuard.d.ts.map +1 -0
  19. package/dist/types/utils.d.ts +1 -0
  20. package/dist/types/utils.d.ts.map +1 -1
  21. package/package.json +6 -2
  22. package/src/core/components/functional/queue/queue.ts +82 -40
  23. package/src/core/components/ui/_css/scroll.ts +3 -0
  24. package/src/core/utils/SafariScrollGuard.ts +429 -0
  25. package/src/docs/search/docs-search.json +28 -108
  26. package/src/docs/tailwind/css/tailwind.css +0 -0
  27. package/src/tsconfig.json +3 -0
  28. package/src/tsconfig.tsbuildinfo +1 -1
  29. package/src/utils.ts +13 -0
  30. package/vite.config.mts +4 -0
  31. package/vitest.setup.ts +22 -0
@@ -0,0 +1,429 @@
1
+ /**
2
+ * Garde Safari / WebKit : empêche les resets fantômes de scroll.
3
+ *
4
+ * - stock = dernière position user (intent + settle inertie)
5
+ * - jump brutal vers ~0 → JAMAIS capturé (même mid-gesture), restore
6
+ * - collapse Lit / slot light-DOM : observe aussi le host + sticky restore multi-frames
7
+ * - nested : intent uniquement sur le scroller le plus proche (composedPath)
8
+ */
9
+
10
+ export type ScrollRoot = HTMLElement | Window;
11
+
12
+ const SCROLL_SETTLE_MS = 150;
13
+ const RESTORE_EPS = 10;
14
+ /** Jump vers le haut considéré fantôme (pas une remontée progressive trackpad). */
15
+ const JUMP_EPS = 200;
16
+ /** Combien de frames on re-applique le stock après un fantôme / mutation. */
17
+ const STICKY_RESTORE_FRAMES = 16;
18
+
19
+ type RootState = {
20
+ x: number;
21
+ y: number;
22
+ restoring: boolean;
23
+ watching: boolean;
24
+ userEngaged: boolean;
25
+ settleTimer?: ReturnType<typeof setTimeout>;
26
+ raf: number;
27
+ stickyFrames: number;
28
+ resizeObserver?: ResizeObserver;
29
+ mutationObserver?: MutationObserver;
30
+ };
31
+
32
+ const states = new WeakMap<object, RootState>();
33
+ let installed = false;
34
+
35
+ export function isSafariScrollEngine(): boolean {
36
+ if (typeof navigator === "undefined") return false;
37
+ return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
38
+ }
39
+
40
+ function read(root: ScrollRoot): { x: number; y: number } {
41
+ if (root === window) {
42
+ return { x: window.scrollX, y: window.scrollY };
43
+ }
44
+ const el = root as HTMLElement;
45
+ return { x: el.scrollLeft, y: el.scrollTop };
46
+ }
47
+
48
+ function write(root: ScrollRoot, x: number, y: number): void {
49
+ if (root === window) {
50
+ window.scrollTo(x, y);
51
+ return;
52
+ }
53
+ const el = root as HTMLElement;
54
+ // scrollTo coupe mieux l’inertie iOS que le setter seul
55
+ if (typeof el.scrollTo === "function") {
56
+ el.scrollTo(x, y);
57
+ } else {
58
+ el.scrollLeft = x;
59
+ el.scrollTop = y;
60
+ }
61
+ }
62
+
63
+ function maxScrollY(root: ScrollRoot): number {
64
+ if (root === window) {
65
+ const doc = document.documentElement;
66
+ return Math.max(0, doc.scrollHeight - window.innerHeight);
67
+ }
68
+ const el = root as HTMLElement;
69
+ return Math.max(0, el.scrollHeight - el.clientHeight);
70
+ }
71
+
72
+ function getState(root: ScrollRoot): RootState {
73
+ let s = states.get(root);
74
+ if (!s) {
75
+ const pos = read(root);
76
+ s = {
77
+ x: pos.x,
78
+ y: pos.y,
79
+ restoring: false,
80
+ watching: false,
81
+ userEngaged: false,
82
+ raf: 0,
83
+ stickyFrames: 0,
84
+ };
85
+ states.set(root, s);
86
+ }
87
+ return s;
88
+ }
89
+
90
+ function markUserEngaged(root: ScrollRoot): void {
91
+ getState(root).userEngaged = true;
92
+ }
93
+
94
+ function armScrollSettle(root: ScrollRoot): void {
95
+ const s = getState(root);
96
+ clearTimeout(s.settleTimer);
97
+ s.settleTimer = setTimeout(() => {
98
+ s.userEngaged = false;
99
+ s.settleTimer = undefined;
100
+ }, SCROLL_SETTLE_MS);
101
+ }
102
+
103
+ /**
104
+ * Saut brutal vers le haut (pas seulement ~0) : Safari clampe souvent à une
105
+ * valeur intermédiaire (ex. 1716→225). Un `cap` sans `rst` = on avait accepté
106
+ * ce drop comme scroll user.
107
+ */
108
+ function looksLikePhantomDrop(currentY: number, storedY: number): boolean {
109
+ return storedY - currentY >= JUMP_EPS;
110
+ }
111
+
112
+ /**
113
+ * Mémorise la position courante — refuse tout gros drop d’un coup.
114
+ */
115
+ export function captureScrollRoot(root: ScrollRoot): void {
116
+ if (!isSafariScrollEngine()) return;
117
+ const s = getState(root);
118
+ if (s.restoring) return;
119
+ const pos = read(root);
120
+ if (looksLikePhantomDrop(pos.y, s.y)) {
121
+ stickyRestore(root);
122
+ return;
123
+ }
124
+ s.x = pos.x;
125
+ s.y = pos.y;
126
+ }
127
+
128
+ /**
129
+ * Ré-applique le stock. Ne pollue jamais le stock avec le scroll courant.
130
+ *
131
+ * Important : on peut clamper la *cible d’écriture* à maxScroll (le navigateur
132
+ * le ferait de toute façon), mais on ne doit JAMAIS abaisser `s.y` pour autant —
133
+ * pendant un re-render Lit maxScroll plonge souvent un instant (ex. 215→75) puis
134
+ * revient ; baisser le stock là, c’est perdre la position pour de bon.
135
+ */
136
+ export function restoreScrollRoot(root: ScrollRoot): void {
137
+ if (!isSafariScrollEngine()) return;
138
+ const s = getState(root);
139
+ const maxY = maxScrollY(root);
140
+ const targetX = s.x;
141
+ // maxY===0 : collapse total transitoire → tenter quand même s.y (no-op clamp)
142
+ const targetY = maxY > 0 ? Math.min(s.y, maxY) : s.y;
143
+
144
+ const { x, y } = read(root);
145
+ if (
146
+ Math.abs(targetX - x) <= RESTORE_EPS &&
147
+ Math.abs(targetY - y) <= RESTORE_EPS
148
+ ) {
149
+ return;
150
+ }
151
+
152
+ s.restoring = true;
153
+ try {
154
+ write(root, targetX, targetY);
155
+ } finally {
156
+ requestAnimationFrame(() => {
157
+ s.restoring = false;
158
+ });
159
+ }
160
+ }
161
+
162
+ /** @deprecated alias */
163
+ export function syncScrollRoot(root: ScrollRoot): void {
164
+ restoreScrollRoot(root);
165
+ }
166
+
167
+ export function resetScrollRoot(
168
+ root: ScrollRoot,
169
+ position: { x?: number; y?: number } | "current" = { x: 0, y: 0 },
170
+ ): void {
171
+ if (!isSafariScrollEngine()) return;
172
+ const s = getState(root);
173
+ if (position === "current") {
174
+ const pos = read(root);
175
+ s.x = pos.x;
176
+ s.y = pos.y;
177
+ return;
178
+ }
179
+ s.x = position.x ?? 0;
180
+ s.y = position.y ?? 0;
181
+ }
182
+
183
+ function stickyRestore(root: ScrollRoot): void {
184
+ const s = getState(root);
185
+ s.stickyFrames = STICKY_RESTORE_FRAMES;
186
+ if (s.raf) return;
187
+ const tick = () => {
188
+ s.raf = 0;
189
+ restoreScrollRoot(root);
190
+ if (s.stickyFrames > 0) {
191
+ s.stickyFrames--;
192
+ s.raf = requestAnimationFrame(tick);
193
+ }
194
+ };
195
+ s.raf = requestAnimationFrame(tick);
196
+ }
197
+
198
+ function scheduleRestore(root: ScrollRoot): void {
199
+ stickyRestore(root);
200
+ }
201
+
202
+ /**
203
+ * Branche écoute intent + scroll + resize (+ mutations).
204
+ * Idempotent.
205
+ */
206
+ export function watchScrollRoot(root: ScrollRoot): void {
207
+ if (!isSafariScrollEngine()) return;
208
+ const s = getState(root);
209
+ if (s.watching) return;
210
+ s.watching = true;
211
+
212
+ const target: EventTarget = root === window ? window : root;
213
+
214
+ const onScroll = () => {
215
+ if (s.restoring) return;
216
+ const { y } = read(root);
217
+
218
+ // Gros drop d’un coup (y compris vers une valeur intermédiaire) → restore
219
+ if (looksLikePhantomDrop(y, s.y)) {
220
+ stickyRestore(root);
221
+ return;
222
+ }
223
+
224
+ if (s.userEngaged) {
225
+ captureScrollRoot(root);
226
+ armScrollSettle(root);
227
+ return;
228
+ }
229
+
230
+ // Pas d’intent taggé : adopter la position courante (pas un gros drop).
231
+ captureScrollRoot(root);
232
+ };
233
+
234
+ const isGestureForThisRoot = (e: Event) =>
235
+ nearestScrollRootFromEvent(e) === root;
236
+
237
+ const onUserIntent = (e: Event) => {
238
+ if (!isGestureForThisRoot(e)) return;
239
+ // Snapshot immédiat avant mutation DOM déclenchée par le clic.
240
+ captureScrollRoot(root);
241
+ markUserEngaged(root);
242
+ };
243
+
244
+ const onUserIntentEnd = (e: Event) => {
245
+ if (!isGestureForThisRoot(e)) return;
246
+ armScrollSettle(root);
247
+ };
248
+
249
+ const onWheel = (e: Event) => {
250
+ if (!isGestureForThisRoot(e)) return;
251
+ captureScrollRoot(root);
252
+ markUserEngaged(root);
253
+ armScrollSettle(root);
254
+ };
255
+
256
+ target.addEventListener("scroll", onScroll, { passive: true });
257
+ target.addEventListener("touchstart", onUserIntent, { passive: true });
258
+ target.addEventListener("touchend", onUserIntentEnd, { passive: true });
259
+ target.addEventListener("touchcancel", onUserIntentEnd, { passive: true });
260
+ target.addEventListener("wheel", onWheel, { passive: true });
261
+ target.addEventListener("pointerdown", onUserIntent, { passive: true });
262
+ target.addEventListener("pointerup", onUserIntentEnd, { passive: true });
263
+ target.addEventListener("pointercancel", onUserIntentEnd, { passive: true });
264
+
265
+ const pos = read(root);
266
+ s.x = pos.x;
267
+ s.y = pos.y;
268
+
269
+ if (root === window) {
270
+ const ro = new ResizeObserver(() => scheduleRestore(window));
271
+ ro.observe(document.documentElement);
272
+ s.resizeObserver = ro;
273
+ return;
274
+ }
275
+
276
+ const el = root as HTMLElement;
277
+ const ro = new ResizeObserver(() => scheduleRestore(el));
278
+ ro.observe(el);
279
+ for (const child of Array.from(el.children)) {
280
+ ro.observe(child);
281
+ }
282
+ s.resizeObserver = ro;
283
+
284
+ const onDomChange = () => scheduleRestore(el);
285
+ const mo = new MutationObserver((mutations) => {
286
+ for (const mutation of mutations) {
287
+ if (mutation.type !== "childList") continue;
288
+ for (const node of Array.from(mutation.addedNodes)) {
289
+ if (node instanceof Element) ro.observe(node);
290
+ }
291
+ }
292
+ onDomChange();
293
+ });
294
+ // Shadow children
295
+ mo.observe(el, { childList: true, subtree: true, characterData: true });
296
+ // Light DOM du host (contenu <slot>) — sinon on rate suppressions / updates
297
+ const rootNode = el.getRootNode();
298
+ if (rootNode instanceof ShadowRoot && rootNode.host) {
299
+ mo.observe(rootNode.host, {
300
+ childList: true,
301
+ subtree: true,
302
+ characterData: true,
303
+ });
304
+ }
305
+ s.mutationObserver = mo;
306
+ }
307
+
308
+ function nearestScrollRootFromEvent(e: Event): ScrollRoot {
309
+ for (const node of e.composedPath()) {
310
+ if (node === window || node === document) continue;
311
+ if (node instanceof HTMLElement && isScrollContainer(node)) {
312
+ return node;
313
+ }
314
+ }
315
+ return window;
316
+ }
317
+
318
+ function isScrollContainer(el: Element): boolean {
319
+ if (!(el instanceof HTMLElement)) return false;
320
+ if (el === document.documentElement || el === document.body) return false;
321
+ const oy = getComputedStyle(el).overflowY;
322
+ return oy === "auto" || oy === "scroll" || oy === "overlay";
323
+ }
324
+
325
+ function discoverFromEvent(e: Event): void {
326
+ for (const node of e.composedPath()) {
327
+ if (node === window || node === document) continue;
328
+ if (node instanceof HTMLElement && isScrollContainer(node)) {
329
+ watchScrollRoot(node);
330
+ }
331
+ }
332
+
333
+ const engageRoot = nearestScrollRootFromEvent(e);
334
+ watchScrollRoot(engageRoot);
335
+
336
+ if (e.type === "wheel") {
337
+ captureScrollRoot(engageRoot);
338
+ markUserEngaged(engageRoot);
339
+ armScrollSettle(engageRoot);
340
+ } else if (e.type === "touchstart" || e.type === "pointerdown") {
341
+ captureScrollRoot(engageRoot);
342
+ markUserEngaged(engageRoot);
343
+ }
344
+ }
345
+
346
+ function watchScrollContainersInTree(root: ParentNode): void {
347
+ if (root instanceof Element && root.classList?.contains("custom-scroll")) {
348
+ watchScrollRoot(root as HTMLElement);
349
+ }
350
+ root.querySelectorAll?.(".custom-scroll").forEach((el) => {
351
+ if (el instanceof HTMLElement) watchScrollRoot(el);
352
+ });
353
+ }
354
+
355
+ function observeShadowRoot(shadow: ShadowRoot): void {
356
+ watchScrollContainersInTree(shadow);
357
+ const mo = new MutationObserver((mutations) => {
358
+ for (const mutation of mutations) {
359
+ for (const node of Array.from(mutation.addedNodes)) {
360
+ if (node instanceof Element) watchScrollContainersInTree(node);
361
+ }
362
+ }
363
+ });
364
+ mo.observe(shadow, { childList: true, subtree: true });
365
+ }
366
+
367
+ function walkExistingShadows(node: ParentNode): void {
368
+ node.querySelectorAll?.("*").forEach((el) => {
369
+ if (el.shadowRoot) {
370
+ observeShadowRoot(el.shadowRoot);
371
+ walkExistingShadows(el.shadowRoot);
372
+ }
373
+ });
374
+ }
375
+
376
+ function patchAttachShadow(): void {
377
+ const proto = Element.prototype as Element & {
378
+ __safariScrollGuardPatched?: boolean;
379
+ };
380
+ if (proto.__safariScrollGuardPatched) return;
381
+ proto.__safariScrollGuardPatched = true;
382
+ const nativeAttachShadow = proto.attachShadow;
383
+ proto.attachShadow = function attachShadowPatched(
384
+ this: Element,
385
+ init: ShadowRootInit,
386
+ ): ShadowRoot {
387
+ const shadow = nativeAttachShadow.call(this, init);
388
+ observeShadowRoot(shadow);
389
+ return shadow;
390
+ };
391
+ }
392
+
393
+ export function installSafariScrollGuard(): void {
394
+ if (typeof window === "undefined" || installed) return;
395
+ installed = true;
396
+
397
+ if (!isSafariScrollEngine()) return;
398
+
399
+ watchScrollRoot(window);
400
+ patchAttachShadow();
401
+
402
+ const opts: AddEventListenerOptions = { capture: true, passive: true };
403
+ window.addEventListener("wheel", discoverFromEvent, opts);
404
+ window.addEventListener("touchstart", discoverFromEvent, opts);
405
+ window.addEventListener("pointerdown", discoverFromEvent, opts);
406
+
407
+ const scan = () => {
408
+ watchScrollContainersInTree(document);
409
+ walkExistingShadows(document);
410
+ };
411
+
412
+ if (document.readyState === "loading") {
413
+ document.addEventListener("DOMContentLoaded", scan, { once: true });
414
+ } else {
415
+ scan();
416
+ }
417
+
418
+ const mo = new MutationObserver((mutations) => {
419
+ for (const mutation of mutations) {
420
+ for (const node of Array.from(mutation.addedNodes)) {
421
+ if (!(node instanceof Element)) continue;
422
+ watchScrollContainersInTree(node);
423
+ if (node.shadowRoot) observeShadowRoot(node.shadowRoot);
424
+ walkExistingShadows(node);
425
+ }
426
+ }
427
+ });
428
+ mo.observe(document.documentElement, { childList: true, subtree: true });
429
+ }
@@ -350,19 +350,14 @@
350
350
  "core/components/functional/queue/queue.md": {
351
351
  "title": "Queue",
352
352
  "hashes": {
353
- "lazy-load": {
353
+ "lazy-load--offset-and-limit": {
354
354
  "count": 1,
355
- "title": "Lazy load",
355
+ "title": "Lazy load — `$offset` and `$limit`",
356
356
  "type": "paragraph"
357
357
  },
358
- "manual-next-signal--supplement": {
358
+ "simple-batch-no-lazy-scroll": {
359
359
  "count": 1,
360
- "title": "Manual next (signal) — supplement",
361
- "type": "paragraph"
362
- },
363
- "simple-batch-no-pagination": {
364
- "count": 1,
365
- "title": "Simple batch (no pagination)",
360
+ "title": "Simple batch (no lazy scroll)",
366
361
  "type": "paragraph"
367
362
  }
368
363
  }
@@ -825,14 +820,14 @@
825
820
  }
826
821
  },
827
822
  {
828
- "search": "In Lit apps, use defineQueue + .config on &lt;sonic-queue&gt; — typed Endpoint, DataProviderKey, enums. See src/docs/example/docs-queue-demos.ts.\n",
823
+ "search": "When the expression contains $offset and $limit, the queue:\n",
829
824
  "files": {
830
825
  "core/components/functional/queue/queue.md": {
831
826
  "title": "Queue",
832
827
  "hashes": {
833
- "queue": {
828
+ "lazy-load--offset-and-limit": {
834
829
  "count": 1,
835
- "title": "",
830
+ "title": "Lazy load — `$offset` and `$limit`",
836
831
  "type": "paragraph"
837
832
  }
838
833
  }
@@ -840,14 +835,14 @@
840
835
  }
841
836
  },
842
837
  {
843
- "search": "Required: endpoint + items. Add rowsAt when rows live in a nested property. Everything else has sensible defaults (strategy: Lazy, pageSize: 10, offset/limit query names).\n",
838
+ "search": "The doc mock implements this on GET /docs-mock-api/api/users?offset=…&amp;per_page=… (paginateUsers in src/docs/mock-api/router.ts).\n",
844
839
  "files": {
845
840
  "core/components/functional/queue/queue.md": {
846
841
  "title": "Queue",
847
842
  "hashes": {
848
- "queue": {
843
+ "lazy-load--offset-and-limit": {
849
844
  "count": 1,
850
- "title": "",
845
+ "title": "Lazy load — `$offset` and `$limit`",
851
846
  "type": "paragraph"
852
847
  }
853
848
  }
@@ -855,14 +850,14 @@
855
850
  }
856
851
  },
857
852
  {
858
- "search": "Layout (class, id, …) stays on &lt;sonic-queue&gt; — not inside .config.\n",
853
+ "search": "Wrap the queue in a fixed height with overflow-y-auto so lazy load triggers when scrolling inside the box (same layout as the TS starter demo-queue-templates).\n",
859
854
  "files": {
860
855
  "core/components/functional/queue/queue.md": {
861
856
  "title": "Queue",
862
857
  "hashes": {
863
- "queue": {
858
+ "lazy-load--offset-and-limit": {
864
859
  "count": 1,
865
- "title": "",
860
+ "title": "Lazy load — `$offset` and `$limit`",
866
861
  "type": "paragraph"
867
862
  }
868
863
  }
@@ -870,14 +865,14 @@
870
865
  }
871
866
  },
872
867
  {
873
- "search": "When pagination is enabled (default QueueLoadStrategy.Lazy), the queue:\n",
868
+ "search": "Try scrolling after load — batches of 4 users. Search e.g. George, Bluth, or zzz for empty results.\n",
874
869
  "files": {
875
870
  "core/components/functional/queue/queue.md": {
876
871
  "title": "Queue",
877
872
  "hashes": {
878
- "lazy-load": {
873
+ "lazy-load--offset-and-limit": {
879
874
  "count": 1,
880
- "title": "Lazy load",
875
+ "title": "Lazy load — `$offset` and `$limit`",
881
876
  "type": "paragraph"
882
877
  }
883
878
  }
@@ -885,14 +880,14 @@
885
880
  }
886
881
  },
887
882
  {
888
- "search": "Mock: GET /docs-mock-api/api/users?offset=…&amp;limit=… (paginateUsers in src/docs/mock-api/router.ts).\n",
883
+ "search": "Without $offset in the expression, the queue behaves like a single list (one batch), e.g. geo communes below.\n",
889
884
  "files": {
890
885
  "core/components/functional/queue/queue.md": {
891
886
  "title": "Queue",
892
887
  "hashes": {
893
- "lazy-load": {
888
+ "expression-example": {
894
889
  "count": 1,
895
- "title": "Lazy load",
890
+ "title": "Expression example",
896
891
  "type": "paragraph"
897
892
  }
898
893
  }
@@ -900,37 +895,7 @@
900
895
  }
901
896
  },
902
897
  {
903
- "search": "Wrap the queue in a fixed height with overflow-y-auto so lazy load triggers when scrolling inside the box.\n",
904
- "files": {
905
- "core/components/functional/queue/queue.md": {
906
- "title": "Queue",
907
- "hashes": {
908
- "lazy-load": {
909
- "count": 1,
910
- "title": "Lazy load",
911
- "type": "paragraph"
912
- }
913
- }
914
- }
915
- }
916
- },
917
- {
918
- "search": "Without pagination offset ( $offset not generated), the queue behaves as a single batch — see geo communes below.\n",
919
- "files": {
920
- "core/components/functional/queue/queue.md": {
921
- "title": "Queue",
922
- "hashes": {
923
- "lazy-load": {
924
- "count": 1,
925
- "title": "Lazy load",
926
- "type": "paragraph"
927
- }
928
- }
929
- }
930
- }
931
- },
932
- {
933
- "search": "Pass a filter DataProviderKey in defineQueue (same publisher as formDataProvider on the search form). On change, the queue:\n",
898
+ "search": "One search field (name=&quot;q&quot;) on formDataProvider=&quot;filter&quot;. The queue listens via dataFilterProvider=&quot;filter&quot; and, on change:\n",
934
899
  "files": {
935
900
  "core/components/functional/queue/queue.md": {
936
901
  "title": "Queue",
@@ -945,7 +910,7 @@
945
910
  }
946
911
  },
947
912
  {
948
- "search": "Use filteredFields in the config only when the form has fields that must not be sent to the API.\n",
913
+ "search": "Use filteredFields only when the form has extra fields that must not be sent to the API (e.g. filteredFields=&quot;rememberMe internalId&quot;).\n",
949
914
  "files": {
950
915
  "core/components/functional/queue/queue.md": {
951
916
  "title": "Queue",
@@ -960,7 +925,7 @@
960
925
  }
961
926
  },
962
927
  {
963
- "search": "The users demo above wires docsQueueUsersFilterKey + sonic-input name=&quot;q&quot;. Mock filter: filterDocsUsers in src/docs/mock-api/fixtures.ts.\n",
928
+ "search": "The mock API filters before pagination — same haystack logic as the starter (filterDocsUsers in src/docs/mock-api/fixtures.ts, used by paginateUsers in router.ts / Service Worker):\n",
964
929
  "files": {
965
930
  "core/components/functional/queue/queue.md": {
966
931
  "title": "Queue",
@@ -975,14 +940,14 @@
975
940
  }
976
941
  },
977
942
  {
978
- "search": "Default lazy scroll loads the next batch automatically. For a button that triggers the next batch via publishers — without touching the queue instance — use QueueLoadStrategy.Signal and a void next key (invalidate(), same idea as triggerKey on @get).\n",
943
+ "search": "Geo communes: expression uses $limit only (no $offset) — one request, one batch.\n",
979
944
  "files": {
980
945
  "core/components/functional/queue/queue.md": {
981
946
  "title": "Queue",
982
947
  "hashes": {
983
- "manual-next-signal--supplement": {
948
+ "simple-batch-no-lazy-scroll": {
984
949
  "count": 1,
985
- "title": "Manual next (signal) — supplement",
950
+ "title": "Simple batch (no lazy scroll)",
986
951
  "type": "paragraph"
987
952
  }
988
953
  }
@@ -990,44 +955,14 @@
990
955
  }
991
956
  },
992
957
  {
993
- "search": "state exposes QueuePublisherState on the queue dataProvider: meta (e.g. total), loadedCount, hasMore. Wire sonic-button invalidate to the same next key.\n",
958
+ "search": "Optional for hosts without Lit — see HTML integration.\n",
994
959
  "files": {
995
960
  "core/components/functional/queue/queue.md": {
996
961
  "title": "Queue",
997
962
  "hashes": {
998
- "manual-next-signal--supplement": {
963
+ "html-template-children": {
999
964
  "count": 1,
1000
- "title": "Manual next (signal) — supplement",
1001
- "type": "paragraph"
1002
- }
1003
- }
1004
- }
1005
- }
1006
- },
1007
- {
1008
- "search": "Elsewhere: dp(usersNextKey).invalidate().\n",
1009
- "files": {
1010
- "core/components/functional/queue/queue.md": {
1011
- "title": "Queue",
1012
- "hashes": {
1013
- "manual-next-signal--supplement": {
1014
- "count": 1,
1015
- "title": "Manual next (signal) — supplement",
1016
- "type": "paragraph"
1017
- }
1018
- }
1019
- }
1020
- }
1021
- },
1022
- {
1023
- "search": "Geo communes: one request, one batch ($limit only).\n",
1024
- "files": {
1025
- "core/components/functional/queue/queue.md": {
1026
- "title": "Queue",
1027
- "hashes": {
1028
- "simple-batch-no-pagination": {
1029
- "count": 1,
1030
- "title": "Simple batch (no pagination)",
965
+ "title": "HTML `<template>` children",
1031
966
  "type": "paragraph"
1032
967
  }
1033
968
  }
@@ -2334,21 +2269,6 @@
2334
2269
  }
2335
2270
  }
2336
2271
  },
2337
- {
2338
- "search": "Emit a publisher signal on click (same idea as triggerKey on @get, or dataNextProvider on sonic-queue). Value is the publisher path.\n",
2339
- "files": {
2340
- "core/components/ui/button/button.md": {
2341
- "title": "Button",
2342
- "hashes": {
2343
- "invalidate": {
2344
- "count": 1,
2345
- "title": "Invalidate",
2346
- "type": "paragraph"
2347
- }
2348
- }
2349
- }
2350
- }
2351
- },
2352
2272
  {
2353
2273
  "search": "Captcha",
2354
2274
  "files": {
File without changes
package/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
  ],