firstly 0.10.8 → 0.12.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 (48) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/esm/changeLog/index.d.ts +1 -0
  3. package/esm/changeLog/server/index.d.ts +2 -2
  4. package/esm/changeLog/server/index.js +2 -2
  5. package/esm/core/FF_Allow.d.ts +3 -3
  6. package/esm/core/FF_Allow.js +3 -3
  7. package/esm/core/FF_Filter.d.ts +3 -3
  8. package/esm/core/FF_Filter.js +3 -3
  9. package/esm/core/FF_Validators.d.ts +2 -2
  10. package/esm/core/FF_Validators.js +2 -2
  11. package/esm/core/index.d.ts +1 -0
  12. package/esm/core/index.js +1 -1
  13. package/esm/cron/server/index.d.ts +1 -1
  14. package/esm/cron/server/index.js +1 -1
  15. package/esm/mail/ui/WriteMail.svelte +1 -1
  16. package/esm/sqlAdmin/index.d.ts +1 -0
  17. package/esm/sqlAdmin/index.js +1 -0
  18. package/esm/sqlAdmin/presetQueries.d.ts +8 -0
  19. package/esm/sqlAdmin/presetQueries.js +26 -0
  20. package/esm/sqlAdmin/server/index.d.ts +2 -2
  21. package/esm/sqlAdmin/server/index.js +2 -2
  22. package/esm/sqlAdmin/ui/SqlAdmin.svelte +12 -41
  23. package/esm/sqlAdmin/ui/SqlAdmin.svelte.d.ts +5 -1
  24. package/esm/sqlAdmin/ui/SqlDrift.svelte +41 -41
  25. package/esm/svelte/FF_DialogManager.svelte +4 -4
  26. package/esm/svelte/FF_PromptDefault.svelte +6 -6
  27. package/esm/svelte/class/SP.svelte.d.ts +1 -0
  28. package/esm/svelte/class/SP.svelte.js +14 -15
  29. package/esm/svelte/dialog.svelte.d.ts +1 -1
  30. package/esm/svelte/grid/FF_Grid.svelte +11 -8
  31. package/esm/svelte/grid/buildCells.js +6 -0
  32. package/esm/svelte/grid/cellTypes.d.ts +4 -0
  33. package/esm/svelte/handleError.d.ts +8 -2
  34. package/esm/svelte/handleError.js +24 -2
  35. package/esm/svelte/index.d.ts +1 -1
  36. package/esm/svelte/index.js +1 -1
  37. package/esm/svelte/query/index.svelte.d.ts +40 -0
  38. package/esm/svelte/query/index.svelte.js +112 -0
  39. package/esm/svelte/query/read.remote.d.ts +7 -0
  40. package/esm/svelte/query/read.remote.js +27 -0
  41. package/esm/svelte/remultApiLoad.d.ts +21 -9
  42. package/esm/svelte/remultApiLoad.js +21 -9
  43. package/esm/svelte/server/handleCaching.d.ts +2 -2
  44. package/esm/svelte/server/handleCaching.js +1 -1
  45. package/esm/svelte/ui/Icon.svelte +0 -1
  46. package/esm/vite/index.js +11 -1
  47. package/package.json +8 -3
  48. package/bin/args.d.ts +0 -13
@@ -188,7 +188,7 @@
188
188
  type="button"
189
189
  {onclick}
190
190
  disabled={loading}
191
- class="bg-secondary text-secondary-foreground hover:bg-secondary/80 px-3 py-1.5 text-sm font-medium disabled:opacity-50"
191
+ class="bg-secondary px-3 py-1.5 text-sm font-medium text-secondary-foreground hover:bg-secondary/80 disabled:opacity-50"
192
192
  >
193
193
  {loading ? 'Running…' : label}
194
194
  </button>
@@ -199,19 +199,19 @@
199
199
  type="button"
200
200
  {onclick}
201
201
  {disabled}
202
- class="bg-primary text-primary-foreground hover:bg-primary/90 px-3 py-1.5 text-sm font-medium disabled:opacity-50"
202
+ class="bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
203
203
  >
204
204
  {label}
205
205
  </button>
206
206
  {/snippet}
207
207
 
208
208
  {#snippet resultTable(headers: string[], children: import('svelte').Snippet)}
209
- <div class="border-border overflow-auto border">
209
+ <div class="overflow-auto border border-border">
210
210
  <table class="w-full border-collapse text-sm">
211
211
  <thead class="bg-muted">
212
212
  <tr>
213
213
  {#each headers as h (h)}
214
- <th class="border-border border-b px-3 py-2 text-left font-semibold">{h}</th>
214
+ <th class="border-b border-border px-3 py-2 text-left font-semibold">{h}</th>
215
215
  {/each}
216
216
  </tr>
217
217
  </thead>
@@ -222,18 +222,18 @@
222
222
  </div>
223
223
  {/snippet}
224
224
 
225
- <div class="border-border bg-card text-card-foreground border">
225
+ <div class="border border-border bg-card text-card-foreground">
226
226
  <header class="px-5 py-4">
227
227
  <h2 class="text-lg font-semibold">Schema drift</h2>
228
- <p class="text-muted-foreground mt-1 text-sm">
228
+ <p class="mt-1 text-sm text-muted-foreground">
229
229
  Remult creates tables and adds columns, but never revisits them. Each check below is a dry run
230
230
  first; applying runs in one transaction.
231
231
  </p>
232
232
  {#if flagsError}
233
233
  <pre
234
- class="border-destructive/40 bg-destructive/10 text-destructive mt-3 overflow-auto border p-3 text-sm">{flagsError}</pre>
234
+ class="mt-3 overflow-auto border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive">{flagsError}</pre>
235
235
  {:else if flags && !Object.values(flags).some(Boolean)}
236
- <p class="text-muted-foreground mt-3 text-sm">
236
+ <p class="mt-3 text-sm text-muted-foreground">
237
237
  No check enabled. Opt in with <code class="font-mono"
238
238
  >sqlAdmin(&#123; drift: &#123; entities, relationIndexes: true, ... &#125; &#125;)</code
239
239
  >.
@@ -243,9 +243,9 @@
243
243
 
244
244
  <!-- Flow 1: primary keys -->
245
245
  {#if flags?.primaryKeys}
246
- <section class="border-border border-t p-5">
246
+ <section class="border-t border-border p-5">
247
247
  <h3 class="text-sm font-semibold">Primary key drift</h3>
248
- <p class="text-muted-foreground mt-1 mb-4 max-w-[80ch] text-sm">
248
+ <p class="mt-1 mb-4 max-w-[80ch] text-sm text-muted-foreground">
249
249
  Postgres only sets a PK on first table creation and never ALTERs it, so a later
250
250
  <code class="font-mono">id</code> change is silently ignored. Applying runs every migration atomically.
251
251
  </p>
@@ -261,12 +261,12 @@
261
261
 
262
262
  {#if pkError}
263
263
  <pre
264
- class="border-destructive/40 bg-destructive/10 text-destructive mb-4 overflow-auto border p-3 text-sm">{pkError}</pre>
264
+ class="mb-4 overflow-auto border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive">{pkError}</pre>
265
265
  {/if}
266
266
 
267
267
  {#if pkResult}
268
268
  {#if pkResult.applied}
269
- <p class="border-primary/30 bg-primary/10 text-primary mb-3 border p-2 text-sm">
269
+ <p class="mb-3 border border-primary/30 bg-primary/10 p-2 text-sm text-primary">
270
270
  Applied · {pkDrift} table{pkDrift === 1 ? '' : 's'} migrated
271
271
  </p>
272
272
  {/if}
@@ -274,15 +274,15 @@
274
274
  {#snippet pkRows()}
275
275
  {#each pkPlans as p (p.table)}
276
276
  <tr class="even:bg-muted/40" class:opacity-50={p.action === 'ok'}>
277
- <td class="border-border border-b px-3 py-2 font-mono">{p.table}</td>
278
- <td class="border-border text-muted-foreground border-b px-3 py-2 font-mono">
277
+ <td class="border-b border-border px-3 py-2 font-mono">{p.table}</td>
278
+ <td class="border-b border-border px-3 py-2 font-mono text-muted-foreground">
279
279
  {p.before.join(', ') || '-'}
280
280
  </td>
281
- <td class="border-border text-muted-foreground border-b px-3 py-2 font-mono">
281
+ <td class="border-b border-border px-3 py-2 font-mono text-muted-foreground">
282
282
  {p.after.join(', ')}
283
283
  </td>
284
284
  <td
285
- class="border-border border-b px-3 py-2 font-mono"
285
+ class="border-b border-border px-3 py-2 font-mono"
286
286
  class:text-primary={p.action === 'migrate' || p.action === 'create'}
287
287
  class:text-muted-foreground={p.action === 'missing'}
288
288
  >
@@ -298,9 +298,9 @@
298
298
 
299
299
  <!-- Flow 2: relation indexes -->
300
300
  {#if flags?.relationIndexes}
301
- <section class="border-border border-t p-5">
301
+ <section class="border-t border-border p-5">
302
302
  <h3 class="text-sm font-semibold">Relation indexes</h3>
303
- <p class="text-muted-foreground mt-1 mb-4 max-w-[80ch] text-sm">
303
+ <p class="mt-1 mb-4 max-w-[80ch] text-sm text-muted-foreground">
304
304
  Postgres doesn't auto-index foreign keys. This indexes the FK columns of
305
305
  <code class="font-mono">toOne</code> relations - unless they're already a leftmost prefix of an
306
306
  existing index (the PK counts), so <code class="font-mono">a</code> on a
@@ -320,12 +320,12 @@
320
320
 
321
321
  {#if relError}
322
322
  <pre
323
- class="border-destructive/40 bg-destructive/10 text-destructive mb-4 overflow-auto border p-3 text-sm">{relError}</pre>
323
+ class="mb-4 overflow-auto border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive">{relError}</pre>
324
324
  {/if}
325
325
 
326
326
  {#if relResult}
327
327
  {#if relResult.applied}
328
- <p class="border-primary/30 bg-primary/10 text-primary mb-3 border p-2 text-sm">
328
+ <p class="mb-3 border border-primary/30 bg-primary/10 p-2 text-sm text-primary">
329
329
  Created · {relMissing} index{relMissing === 1 ? '' : 'es'}
330
330
  </p>
331
331
  {/if}
@@ -333,12 +333,12 @@
333
333
  {#snippet relRows()}
334
334
  {#each relPlans as p (p.name)}
335
335
  <tr class="even:bg-muted/40" class:opacity-50={p.action === 'ok'}>
336
- <td class="border-border border-b px-3 py-2 font-mono">{p.name}</td>
337
- <td class="border-border text-muted-foreground border-b px-3 py-2 font-mono">
336
+ <td class="border-b border-border px-3 py-2 font-mono">{p.name}</td>
337
+ <td class="border-b border-border px-3 py-2 font-mono text-muted-foreground">
338
338
  {p.table} ({p.columns.join(', ')})
339
339
  </td>
340
340
  <td
341
- class="border-border border-b px-3 py-2 font-mono"
341
+ class="border-b border-border px-3 py-2 font-mono"
342
342
  class:text-primary={p.action === 'create'}
343
343
  >
344
344
  {p.action === 'ok'
@@ -357,9 +357,9 @@
357
357
 
358
358
  <!-- Flow 3: drop orphan columns -->
359
359
  {#if flags?.orphanColumns}
360
- <section class="border-border border-t p-5">
360
+ <section class="border-t border-border p-5">
361
361
  <h3 class="text-sm font-semibold">Orphan columns</h3>
362
- <p class="text-muted-foreground mt-1 mb-4 max-w-[80ch] text-sm">
362
+ <p class="mt-1 mb-4 max-w-[80ch] text-sm text-muted-foreground">
363
363
  Remult only ever ADDs columns, never drops them, so a field removed from an entity leaves its
364
364
  column behind. This lists live columns no entity field declares - e.g. a
365
365
  <code class="font-mono">createdAt</code> now covered by the changelog. Destructive, so nothing
@@ -377,23 +377,23 @@
377
377
 
378
378
  {#if dropError}
379
379
  <pre
380
- class="border-destructive/40 bg-destructive/10 text-destructive mb-4 overflow-auto border p-3 text-sm">{dropError}</pre>
380
+ class="mb-4 overflow-auto border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive">{dropError}</pre>
381
381
  {/if}
382
382
 
383
383
  {#if dropResult}
384
384
  {#if dropResult.applied}
385
- <p class="border-primary/30 bg-primary/10 text-primary mb-3 border p-2 text-sm">
385
+ <p class="mb-3 border border-primary/30 bg-primary/10 p-2 text-sm text-primary">
386
386
  Dropped · {dropResult.dropped.length} column{dropResult.dropped.length === 1 ? '' : 's'}
387
387
  </p>
388
388
  {/if}
389
389
 
390
390
  {#if dropPlans.length === 0}
391
- <p class="text-muted-foreground text-sm">No orphan columns - every column maps to a field.</p>
391
+ <p class="text-sm text-muted-foreground">No orphan columns - every column maps to a field.</p>
392
392
  {:else}
393
393
  {#snippet dropRows()}
394
394
  {#each dropPlans as p (keyOf(p))}
395
395
  <tr class="even:bg-muted/40">
396
- <td class="border-border border-b px-3 py-2">
396
+ <td class="border-b border-border px-3 py-2">
397
397
  <input
398
398
  type="checkbox"
399
399
  checked={selected.has(keyOf(p))}
@@ -401,8 +401,8 @@
401
401
  aria-label={`Drop ${keyOf(p)}`}
402
402
  />
403
403
  </td>
404
- <td class="border-border border-b px-3 py-2 font-mono">{p.table}</td>
405
- <td class="border-border text-primary border-b px-3 py-2 font-mono">{p.column}</td>
404
+ <td class="border-b border-border px-3 py-2 font-mono">{p.table}</td>
405
+ <td class="border-b border-border px-3 py-2 font-mono text-primary">{p.column}</td>
406
406
  </tr>
407
407
  {/each}
408
408
  {/snippet}
@@ -414,9 +414,9 @@
414
414
 
415
415
  <!-- Flow 4: nullability drift -->
416
416
  {#if flags?.nullable}
417
- <section class="border-border border-t p-5">
417
+ <section class="border-t border-border p-5">
418
418
  <h3 class="text-sm font-semibold">Nullability drift</h3>
419
- <p class="text-muted-foreground mt-1 mb-4 max-w-[80ch] text-sm">
419
+ <p class="mt-1 mb-4 max-w-[80ch] text-sm text-muted-foreground">
420
420
  A field that is not <code class="font-mono">allowNull</code> makes Remult create the column
421
421
  <code class="font-mono">default '' not null</code>, and it never relaxes that afterwards. So a
422
422
  column added before its field learned <code class="font-mono">allowNull</code> still hands every
@@ -439,25 +439,25 @@
439
439
 
440
440
  {#if nullError}
441
441
  <pre
442
- class="border-destructive/40 bg-destructive/10 text-destructive mb-4 overflow-auto border p-3 text-sm">{nullError}</pre>
442
+ class="mb-4 overflow-auto border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive">{nullError}</pre>
443
443
  {/if}
444
444
 
445
445
  {#if nullResult}
446
446
  {#if nullResult.applied}
447
- <p class="border-primary/30 bg-primary/10 text-primary mb-3 border p-2 text-sm">
447
+ <p class="mb-3 border border-primary/30 bg-primary/10 p-2 text-sm text-primary">
448
448
  Fixed · {nullResult.fixed.length} column{nullResult.fixed.length === 1 ? '' : 's'}
449
449
  </p>
450
450
  {/if}
451
451
 
452
452
  {#if nullPlans.length === 0}
453
- <p class="text-muted-foreground text-sm">
453
+ <p class="text-sm text-muted-foreground">
454
454
  No drift - every nullable field has a plain nullable column.
455
455
  </p>
456
456
  {:else}
457
457
  {#snippet nullRows()}
458
458
  {#each nullPlans as p (keyOf(p))}
459
459
  <tr class="even:bg-muted/40">
460
- <td class="border-border border-b px-3 py-2">
460
+ <td class="border-b border-border px-3 py-2">
461
461
  <input
462
462
  type="checkbox"
463
463
  checked={nullSelected.has(keyOf(p))}
@@ -465,14 +465,14 @@
465
465
  aria-label={`Fix ${keyOf(p)}`}
466
466
  />
467
467
  </td>
468
- <td class="border-border border-b px-3 py-2 font-mono">{p.table}</td>
469
- <td class="border-border text-primary border-b px-3 py-2 font-mono">{p.column}</td>
470
- <td class="border-border border-b px-3 py-2 text-sm">
468
+ <td class="border-b border-border px-3 py-2 font-mono">{p.table}</td>
469
+ <td class="border-b border-border px-3 py-2 font-mono text-primary">{p.column}</td>
470
+ <td class="border-b border-border px-3 py-2 text-sm">
471
471
  {[p.dropDefault && 'drop default', p.dropNotNull && 'drop not null']
472
472
  .filter(Boolean)
473
473
  .join(' · ')}
474
474
  </td>
475
- <td class="border-border border-b px-3 py-2 text-sm">
475
+ <td class="border-b border-border px-3 py-2 text-sm">
476
476
  {p.blankToNull ? "'' → NULL" : 'rows kept'}
477
477
  </td>
478
478
  </tr>
@@ -196,7 +196,7 @@
196
196
  role="dialog"
197
197
  aria-modal="true"
198
198
  tabindex="-1"
199
- class="bg-background text-foreground border-border relative z-[1] max-h-[90vh] w-full {widthClass[
199
+ class="relative z-[1] max-h-[90vh] w-full border-border bg-background text-foreground {widthClass[
200
200
  width
201
201
  ]} overflow-auto rounded-lg border p-5 shadow-xl"
202
202
  >
@@ -207,7 +207,7 @@
207
207
  <button
208
208
  type="button"
209
209
  aria-label="Close"
210
- class="text-muted-foreground hover:text-foreground absolute top-3 right-3 inline-flex size-7 items-center justify-center rounded-md"
210
+ class="absolute top-3 right-3 inline-flex size-7 items-center justify-center rounded-md text-muted-foreground hover:text-foreground"
211
211
  onclick={dismiss}
212
212
  >
213
213
  ✕
@@ -242,7 +242,7 @@
242
242
  aria-labelledby={title ? `ff-dlg-title-${id}` : undefined}
243
243
  aria-label={title ? undefined : message}
244
244
  aria-describedby="ff-dlg-desc-{id}"
245
- class="bg-background text-foreground border-border relative z-[1] w-full max-w-sm rounded-lg border p-5 shadow-xl"
245
+ class="relative z-[1] w-full max-w-sm rounded-lg border border-border bg-background p-5 text-foreground shadow-xl"
246
246
  >
247
247
  {#if title}
248
248
  <h2 id="ff-dlg-title-{id}" class="mb-2 text-lg font-semibold">{title}</h2>
@@ -251,7 +251,7 @@
251
251
  <div class="mt-5 flex justify-end gap-2">
252
252
  <button
253
253
  type="button"
254
- class="border-border hover:bg-muted rounded-md border px-3 py-1.5 text-sm"
254
+ class="rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted"
255
255
  onclick={cancel}
256
256
  >
257
257
  {cancelLabel}
@@ -38,7 +38,7 @@
38
38
  tabindex="-1"
39
39
  aria-labelledby={item.title ? `ff-prompt-title-${item.id}` : undefined}
40
40
  aria-label={item.title ? undefined : item.label ? resolveMessage(item.label) : undefined}
41
- class="bg-background text-foreground border-border relative z-[1] w-full max-w-sm rounded-lg border p-5 shadow-xl"
41
+ class="relative z-[1] w-full max-w-sm rounded-lg border border-border bg-background p-5 text-foreground shadow-xl"
42
42
  >
43
43
  <form
44
44
  onsubmit={(e) => {
@@ -52,7 +52,7 @@
52
52
  </h2>
53
53
  {/if}
54
54
  {#if item.label}
55
- <label class="text-muted-foreground mb-1 block text-sm" for="ff-prompt-{item.id}">
55
+ <label class="mb-1 block text-sm text-muted-foreground" for="ff-prompt-{item.id}">
56
56
  {resolveMessage(item.label)}
57
57
  </label>
58
58
  {/if}
@@ -60,22 +60,22 @@
60
60
  id="ff-prompt-{item.id}"
61
61
  bind:value
62
62
  placeholder={item.placeholder}
63
- class="border-border bg-card focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:outline-none"
63
+ class="w-full rounded-md border border-border bg-card px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
64
64
  />
65
65
  {#if item.hint}
66
- <p class="text-muted-foreground mt-1 text-xs">{item.hint(value)}</p>
66
+ <p class="mt-1 text-xs text-muted-foreground">{item.hint(value)}</p>
67
67
  {/if}
68
68
  <div class="mt-5 flex justify-end gap-2">
69
69
  <button
70
70
  type="button"
71
- class="border-border hover:bg-muted rounded-md border px-3 py-1.5 text-sm"
71
+ class="rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted"
72
72
  onclick={oncancel}
73
73
  >
74
74
  {resolveMessage(item.cancelLabel ?? cfg.messages.cancel)}
75
75
  </button>
76
76
  <button
77
77
  type="submit"
78
- class="bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-3 py-1.5 text-sm font-medium"
78
+ class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
79
79
  >
80
80
  {resolveMessage(item.confirmLabel ?? cfg.messages.ok)}
81
81
  </button>
@@ -22,6 +22,7 @@ export declare class SP<T extends Record<string, any>> {
22
22
  private debouncedValues;
23
23
  computing: boolean;
24
24
  private debouncedToURL;
25
+ private lastNavigation;
25
26
  private sharedDebouncePending;
26
27
  private activeDebounces;
27
28
  private longestDebounceTime;
@@ -1,9 +1,4 @@
1
- // FIXME
2
- // Why packages/ui pnpm check fails with goto and page?
3
- // - OK it's not a sveltekit project... But this is never used! So why is it checking it ?!
4
- // @ts-ignore
5
1
  import { goto } from '$app/navigation';
6
- // @ts-ignore
7
2
  import { page } from '$app/state';
8
3
  import { debounce } from '../helpers/debounce.js';
9
4
  const CONFIG_DELIMITER = ';';
@@ -23,6 +18,7 @@ export class SP {
23
18
  computing = $state(false);
24
19
  // Store debounced toURL functions for each parameter
25
20
  debouncedToURL = {};
21
+ lastNavigation;
26
22
  // Shared debounce mechanism for all parameters
27
23
  sharedDebouncePending = $state(false);
28
24
  activeDebounces = $state({});
@@ -37,9 +33,8 @@ export class SP {
37
33
  for (const key of Object.keys(this.paramValues)) {
38
34
  this.debouncedValues[key] = this.paramValues[key];
39
35
  }
40
- // Update URL and reset flags
36
+ // `computing` stays on until toURL's navigation lands
41
37
  this.sharedDebouncePending = false;
42
- this.computing = false;
43
38
  this.longestDebounceTime = 0;
44
39
  this.toURL();
45
40
  };
@@ -393,16 +388,21 @@ export class SP {
393
388
  }
394
389
  const strSearch = params.toString() ? '?' + params.toString() : '';
395
390
  // Don't do the goto if the search params haven't changed!
396
- if (strSearch === window.location.search)
391
+ if (strSearch === window.location.search) {
392
+ this.computing = false;
397
393
  return;
398
- goto(`${window.location.pathname}${strSearch}`, {
399
- keepFocus: true,
400
- replaceState: true,
401
- noScroll: true,
394
+ }
395
+ const navigation = goto(`${window.location.pathname}${strSearch}`, {
396
+ replace: true,
397
+ reset: false,
402
398
  ...this.options.gotoOpts,
403
399
  });
404
- // Reset computing flag after URL update is complete
405
- this.computing = false;
400
+ this.lastNavigation = navigation;
401
+ // A newer navigation or a pending debounce keeps `computing` on
402
+ void navigation.finally(() => {
403
+ if (this.lastNavigation === navigation && !this.sharedDebouncePending)
404
+ this.computing = false;
405
+ });
406
406
  }
407
407
  /**
408
408
  * Reset all parameters to their default values
@@ -418,7 +418,6 @@ export class SP {
418
418
  this.sharedDebouncePending = false;
419
419
  this.longestDebounceTime = 0;
420
420
  // Update URL immediately without debounce
421
- this.computing = false;
422
421
  this.toURL();
423
422
  }
424
423
  }
@@ -15,7 +15,7 @@ export { resolveMessage } from '../core/FF_Validators.js';
15
15
  * cancelled/dismissed - no per-method `boolean` / `null` special-casing.
16
16
  *
17
17
  * ```svelte
18
- * import { dialog } from './'
18
+ * import { dialog } from 'firstly/svelte'
19
19
  *
20
20
  * {#snippet body(close)}
21
21
  * <input bind:value={name} />
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" generics="T extends object">
2
- // PUBLISHED batteries-included grid — `import { FF_Grid } from '..'` and go, zero setup:
2
+ // PUBLISHED batteries-included grid — `import { FF_Grid } from 'firstly/svelte'` and go, zero setup:
3
3
  // it bundles a default input + a neutral default skin. Reads the entity `hub` (every prop overrides).
4
4
  // For full control over markup + look, copy the boutique <App_Grid> instead (degit src/boutique/grid).
5
5
  import { untrack } from 'svelte'
@@ -9,10 +9,10 @@
9
9
 
10
10
  import { errorMessage } from '../../core/helper.js'
11
11
  import type { DialogClose } from '../dialog.svelte.js'
12
- import FF_Config from '../FF_Config.svelte'
13
- import { ffConfig } from '../FF_Config.svelte.js'
14
12
  import { ff } from '../ff.svelte.js'
15
13
  import type { FF_Many, ManyStrategy } from '../ff.svelte.js'
14
+ import FF_Config from '../FF_Config.svelte'
15
+ import { ffConfig } from '../FF_Config.svelte.js'
16
16
  import Icon from '../ui/Icon.svelte'
17
17
  import { LibIcon_Add, LibIcon_Edit } from '../ui/LibIcon.js'
18
18
  import { buildCells } from './buildCells.js'
@@ -61,7 +61,10 @@
61
61
  const strategy = untrack(() => strategyProp ?? hub.strategy ?? 'paginate')
62
62
  const pageSize = untrack(() => pageSizeProp ?? hub.pageSize ?? 25)
63
63
 
64
- let sort = $state<EntityOrderBy<T> | undefined>(untrack(() => orderBy ?? hub.orderBy))
64
+ // fall back to the entity default so the header arrow shows what remult applies on landing
65
+ let sort = $state<EntityOrderBy<T> | undefined>(
66
+ untrack(() => orderBy ?? hub.orderBy ?? repo(entity).metadata.options.defaultOrderBy),
67
+ )
65
68
 
66
69
  const m = untrack(() =>
67
70
  ff(entity).many(
@@ -101,9 +104,9 @@
101
104
  const newIcon = $derived(insertCfg !== false ? (insertCfg.icon ?? LibIcon_Add) : LibIcon_Add)
102
105
  const editIcon = $derived(updateCfg !== false ? (updateCfg.icon ?? LibIcon_Edit) : LibIcon_Edit)
103
106
 
104
- function toggleSort(key: string) {
107
+ function toggleSort(key: string, first: 'asc' | 'desc') {
105
108
  const cur = (sort as Record<string, 'asc' | 'desc'> | undefined)?.[key]
106
- sort = { [key]: cur === 'asc' ? 'desc' : 'asc' } as EntityOrderBy<T>
109
+ sort = { [key]: cur ? (cur === 'asc' ? 'desc' : 'asc') : first } as EntityOrderBy<T>
107
110
  }
108
111
  const sortDir = (key: string) => (sort as Record<string, string> | undefined)?.[key]
109
112
 
@@ -185,9 +188,9 @@
185
188
  data-sortable={cell.sortable || undefined}
186
189
  style:text-align={cell.align}
187
190
  class={cell.class}
188
- onclick={() => cell.sortable && cell.col && toggleSort(cell.col)}
191
+ onclick={() => cell.sortable && cell.col && toggleSort(cell.col, cell.firstSort)}
189
192
  >
190
- {cell.caption}{#if cell.col && sortDir(cell.col)}<span data-sort
193
+ {cell.caption}{#if cell.sortable && cell.col && sortDir(cell.col)}<span data-sort
191
194
  >{sortDir(cell.col) === 'asc' ? ' ▲' : ' ▼'}</span
192
195
  >{/if}
193
196
  </th>
@@ -15,6 +15,11 @@ function alignFor(field, ui) {
15
15
  return 'right';
16
16
  return 'left';
17
17
  }
18
+ // newest / biggest first is the useful first view for dates + numbers
19
+ function firstSortFor(field) {
20
+ const t = field?.valueType;
21
+ return t === Date || t === Number ? 'desc' : 'asc';
22
+ }
18
23
  /** Resolve the render kind: explicit > href(field_link) > metaKind(relation/enum) > field. */
19
24
  function resolveKind(field, explicit) {
20
25
  if (explicit)
@@ -58,6 +63,7 @@ export function buildCells(meta, cells, opts) {
58
63
  inputType: field ? getInputType(field, ui) : 'text',
59
64
  align: alignFor(field, ui),
60
65
  sortable,
66
+ firstSort: (isObj && item.firstSort) || firstSortFor(field),
61
67
  class: isObj ? item.class : undefined,
62
68
  cellSnippet: isObj ? item.cellSnippet : undefined,
63
69
  component: isObj ? item.component : undefined,
@@ -41,6 +41,8 @@ export interface Cell<E = any> {
41
41
  align: 'left' | 'center' | 'right';
42
42
  /** Whether this column is sortable by header click (resolved; opt in via `sortable: true`). */
43
43
  sortable: boolean;
44
+ /** Direction of the first header click on an unsorted column. */
45
+ firstSort: 'asc' | 'desc';
44
46
  /** Tailwind/CSS passthrough (e.g. 'col-span-2'). */
45
47
  class?: string;
46
48
  cellSnippet?: Snippet<[{
@@ -62,6 +64,8 @@ export type CellInput<E> = (keyof E & string) | {
62
64
  class?: string;
63
65
  /** Set true to make this column sortable by header click (default: not sortable). */
64
66
  sortable?: boolean;
67
+ /** First header click direction. Default: `desc` for Date/Number fields, else `asc`. */
68
+ firstSort?: 'asc' | 'desc';
65
69
  cellSnippet?: Snippet<[{
66
70
  row: E;
67
71
  cell: Cell<E>;
@@ -1,4 +1,4 @@
1
- import type { HandleClientError } from '@sveltejs/kit';
1
+ import type { HandleClientError } from '@sveltejs/kit/hooks';
2
2
  export type HandleClientErrorMiddleware = (next: HandleClientError) => HandleClientError;
3
3
  /**
4
4
  * Compose `handleError` middlewares into a single `hooks.client.js` handler.
@@ -8,7 +8,7 @@ export type HandleClientErrorMiddleware = (next: HandleClientError) => HandleCli
8
8
  *
9
9
  * ```js
10
10
  * // src/hooks.client.js
11
- * import { stackHandleClientError, withStaleDeployReload } from './'
11
+ * import { stackHandleClientError, withStaleDeployReload } from 'firstly/svelte'
12
12
  *
13
13
  * export const handleError = stackHandleClientError(
14
14
  * withStaleDeployReload(),
@@ -29,3 +29,9 @@ export declare function stackHandleClientError(...middlewares: HandleClientError
29
29
  * falls through to `next`.
30
30
  */
31
31
  export declare function withStaleDeployReload(): HandleClientErrorMiddleware;
32
+ /**
33
+ * Recovers what Kit leaves out after a deploy: a lazy `import()` inside a component. Reloads
34
+ * only when `updated.check()` confirms a new version, so a flaky network can't loop. Call it
35
+ * at the top of `hooks.client.js`.
36
+ */
37
+ export declare function reloadOnStaleChunk(): void;
@@ -1,3 +1,4 @@
1
+ import { navigating, updated } from '$app/state';
1
2
  /**
2
3
  * Compose `handleError` middlewares into a single `hooks.client.js` handler.
3
4
  * Middlewares run outermost-first (the first argument wraps the rest). A middleware
@@ -6,7 +7,7 @@
6
7
  *
7
8
  * ```js
8
9
  * // src/hooks.client.js
9
- * import { stackHandleClientError, withStaleDeployReload } from './'
10
+ * import { stackHandleClientError, withStaleDeployReload } from 'firstly/svelte'
10
11
  *
11
12
  * export const handleError = stackHandleClientError(
12
13
  * withStaleDeployReload(),
@@ -22,6 +23,8 @@ export function stackHandleClientError(...middlewares) {
22
23
  }
23
24
  // Errors SvelteKit/Vite throw when a lazy chunk 404s after a deploy.
24
25
  const CHUNK_LOAD_RE = /Failed to fetch dynamically imported module|error loading dynamically imported module|Importing a module script failed/;
26
+ // Kit recovers route chunks itself, and one failing on hover must not reload the page.
27
+ const ROUTE_CHUNK_RE = /\/immutable\/nodes\//;
25
28
  const STORAGE_KEY = 'ff-reload';
26
29
  // A URL that failed again within this window is a genuine break, not a stale deploy.
27
30
  const RETRY_WINDOW_MS = 10_000;
@@ -35,12 +38,19 @@ const RETRY_WINDOW_MS = 10_000;
35
38
  */
36
39
  export function withStaleDeployReload() {
37
40
  return (next) => (input) => {
38
- if (CHUNK_LOAD_RE.test(String(input.message)) && reloadOnce(input.event.url.href)) {
41
+ if (input.kind === 'unknown' &&
42
+ CHUNK_LOAD_RE.test(String(input.error)) &&
43
+ isVisited(input.event.url) &&
44
+ reloadOnce(input.event.url.href)) {
39
45
  return; // page is unloading; no error surfaces
40
46
  }
41
47
  return next(input);
42
48
  };
43
49
  }
50
+ // A hovered link preloads through handleError too, and must not navigate on its own.
51
+ function isVisited(url) {
52
+ return url.href === location.href || url.href === navigating.to?.url.href;
53
+ }
44
54
  /** Hard-reload `href` unless it already failed within the guard window. Returns true if reloading. */
45
55
  function reloadOnce(href) {
46
56
  try {
@@ -60,3 +70,15 @@ function reloadOnce(href) {
60
70
  return false; // no sessionStorage (private mode) -> don't risk a loop
61
71
  }
62
72
  }
73
+ /**
74
+ * Recovers what Kit leaves out after a deploy: a lazy `import()` inside a component. Reloads
75
+ * only when `updated.check()` confirms a new version, so a flaky network can't loop. Call it
76
+ * at the top of `hooks.client.js`.
77
+ */
78
+ export function reloadOnStaleChunk() {
79
+ addEventListener('vite:preloadError', (event) => {
80
+ if (ROUTE_CHUNK_RE.test(String(event.payload)))
81
+ return;
82
+ void updated.check().then((stale) => stale && location.reload());
83
+ });
84
+ }
@@ -3,7 +3,7 @@ export { ff } from './ff.svelte.js';
3
3
  export type { FF_Many, FF_One, FF_Builder, FF_RepoOptions, FF_OneOptions, FF_RepoLoading, FF_Issue, ManyStrategy, AggregateOptions, QueryOptionsHelper, } from './ff.svelte.js';
4
4
  export { infiniteScroll } from './infiniteScroll.js';
5
5
  export type { InfiniteScrollOptions } from './infiniteScroll.js';
6
- export { stackHandleClientError, withStaleDeployReload } from './handleError.js';
6
+ export { reloadOnStaleChunk, stackHandleClientError, withStaleDeployReload } from './handleError.js';
7
7
  export type { HandleClientErrorMiddleware } from './handleError.js';
8
8
  export { dialog, ffAutofocus, ffTrapFocus, resolveMessage } from './dialog.svelte.js';
9
9
  export type { DialogResult, DialogClose, DialogOptions, DialogItem, DialogRender, ConfirmItem, PromptItem, DialogShellArgs, DialogConfirmArgs, DialogPromptArgs, } from './dialog.svelte.js';
@@ -1,7 +1,7 @@
1
1
  export { errorMessage, isError } from '../core/helper.js';
2
2
  export { ff } from './ff.svelte.js';
3
3
  export { infiniteScroll } from './infiniteScroll.js';
4
- export { stackHandleClientError, withStaleDeployReload } from './handleError.js';
4
+ export { reloadOnStaleChunk, stackHandleClientError, withStaleDeployReload } from './handleError.js';
5
5
  export { dialog, ffAutofocus, ffTrapFocus, resolveMessage } from './dialog.svelte.js';
6
6
  export { default as FF_DialogManager } from './FF_DialogManager.svelte';
7
7
  export { default as FF_Config } from './FF_Config.svelte';