@viniaraujo68/plinth 0.3.0 → 0.5.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.
- package/README.md +189 -19
- package/dist/components/ErrorDisplay.svelte +30 -16
- package/dist/components/ErrorDisplay.svelte.d.ts +4 -0
- package/dist/components/JsonView.svelte +43 -0
- package/dist/components/JsonView.svelte.d.ts +4 -0
- package/dist/components/JsonViewFrame.svelte +654 -0
- package/dist/components/JsonViewFrame.svelte.d.ts +31 -0
- package/dist/components/Paginator.svelte +34 -8
- package/dist/components/Paginator.svelte.d.ts +10 -0
- package/dist/components/RequestErrorDisplay.svelte +43 -0
- package/dist/components/RequestErrorDisplay.svelte.d.ts +11 -0
- package/dist/components/Tabs.svelte +204 -0
- package/dist/components/Tabs.svelte.d.ts +76 -0
- package/dist/components/VirtualJsonView.svelte +67 -0
- package/dist/components/VirtualJsonView.svelte.d.ts +4 -0
- package/dist/components/index.d.ts +8 -1
- package/dist/components/index.js +11 -5
- package/dist/components/json-view.d.ts +70 -0
- package/dist/components/json-view.js +302 -0
- package/dist/components/request-error.d.ts +8 -0
- package/dist/components/request-error.js +55 -0
- package/dist/components/tabs.d.ts +5 -0
- package/dist/components/tabs.js +1 -0
- package/dist/http.d.ts +10 -5
- package/dist/http.js +34 -6
- package/dist/i18n/LocaleToggle.svelte +45 -0
- package/dist/i18n/LocaleToggle.svelte.d.ts +48 -0
- package/dist/i18n/i18n.svelte.d.ts +56 -0
- package/dist/i18n/i18n.svelte.js +94 -0
- package/dist/i18n/index.d.ts +3 -0
- package/dist/i18n/index.js +7 -0
- package/dist/i18n/translate.d.ts +30 -0
- package/dist/i18n/translate.js +38 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/redirect.d.ts +40 -0
- package/dist/redirect.js +59 -0
- package/dist/routing.svelte.d.ts +1 -0
- package/dist/routing.svelte.js +3 -0
- package/dist/shell/AppShell.svelte +269 -95
- package/dist/shell/AppShell.svelte.d.ts +3 -2
- package/dist/shell/NavigationProgress.svelte +85 -0
- package/dist/shell/NavigationProgress.svelte.d.ts +31 -0
- package/dist/shell/TopBar.svelte +191 -0
- package/dist/shell/TopBar.svelte.d.ts +41 -0
- package/dist/shell/UserAvatar.svelte +50 -0
- package/dist/shell/UserAvatar.svelte.d.ts +14 -0
- package/dist/shell/index.d.ts +2 -0
- package/dist/shell/index.js +7 -3
- package/dist/shell/initials.d.ts +6 -0
- package/dist/shell/initials.js +13 -0
- package/dist/table/DataTable.svelte +199 -9
- package/dist/table/DataTable.svelte.d.ts +28 -5
- package/dist/table/FilterBar.svelte +417 -0
- package/dist/table/FilterBar.svelte.d.ts +34 -0
- package/dist/table/FilterRuleEditor.svelte +290 -0
- package/dist/table/FilterRuleEditor.svelte.d.ts +33 -0
- package/dist/table/filter-labels.d.ts +23 -0
- package/dist/table/filter-labels.js +30 -0
- package/dist/table/filter.d.ts +27 -0
- package/dist/table/filter.js +279 -0
- package/dist/table/index.d.ts +4 -1
- package/dist/table/index.js +5 -2
- package/dist/table/types.d.ts +49 -0
- package/dist/toast/Toaster.svelte +7 -3
- package/dist/toast/index.d.ts +1 -1
- package/dist/toast/toast-manager.svelte.d.ts +29 -1
- package/dist/toast/toast-manager.svelte.js +61 -1
- package/dist/url-state/codec.d.ts +15 -0
- package/dist/url-state/codec.js +1 -0
- package/dist/url-state/index.d.ts +5 -0
- package/dist/url-state/index.js +4 -0
- package/dist/url-state/params-codec.d.ts +73 -0
- package/dist/url-state/params-codec.js +116 -0
- package/dist/url-state/search-params.d.ts +4 -0
- package/dist/url-state/search-params.js +28 -0
- package/dist/url-state/synced-state.svelte.d.ts +20 -0
- package/dist/url-state/synced-state.svelte.js +88 -0
- package/dist/url-state/url-store.svelte.d.ts +12 -0
- package/dist/url-state/url-store.svelte.js +56 -0
- package/dist/virtual/DataList.svelte +215 -0
- package/dist/virtual/DataList.svelte.d.ts +52 -0
- package/dist/virtual/VirtualList.svelte +150 -0
- package/dist/virtual/VirtualList.svelte.d.ts +42 -0
- package/dist/virtual/index.d.ts +7 -0
- package/dist/virtual/index.js +6 -0
- package/dist/virtual/keyed-cache.d.ts +7 -0
- package/dist/virtual/keyed-cache.js +20 -0
- package/dist/virtual/labels.d.ts +12 -0
- package/dist/virtual/labels.js +6 -0
- package/dist/virtual/paging.d.ts +5 -0
- package/dist/virtual/paging.js +11 -0
- package/dist/virtual/range.d.ts +9 -0
- package/dist/virtual/range.js +31 -0
- package/dist/virtual/size-tree.d.ts +10 -0
- package/dist/virtual/size-tree.js +56 -0
- package/dist/virtual/virtualizer.svelte.d.ts +38 -0
- package/dist/virtual/virtualizer.svelte.js +295 -0
- package/dist/virtual/windowed-data.svelte.d.ts +38 -0
- package/dist/virtual/windowed-data.svelte.js +139 -0
- package/dist/virtual/windowed-virtualizer.svelte.d.ts +9 -0
- package/dist/virtual/windowed-virtualizer.svelte.js +22 -0
- package/package.json +13 -1
package/README.md
CHANGED
|
@@ -169,6 +169,16 @@ theme context. `AppShell` fills its parent, so give it one with a height. Its na
|
|
|
169
169
|
titled first-level routes that have a page of their own; a titled route with no page stays a heading
|
|
170
170
|
in the breadcrumb trail and is left out of the navigation, which has nothing to link it to.
|
|
171
171
|
|
|
172
|
+
One level of nesting is read off the same tree: a first-level route whose children are titled,
|
|
173
|
+
static and have pages of their own becomes a **group**, a header that folds those children away.
|
|
174
|
+
It opens by itself when the current page is inside it and then stays wherever the reader leaves it;
|
|
175
|
+
the group's own page, when it has one, leads the fold under `overviewLabel` ("Overview"). Children
|
|
176
|
+
go through the same role gate as everything else, and a group left with no children in scope falls
|
|
177
|
+
back to a plain entry — or vanishes, when it has no page either. In the collapsed rail a group is
|
|
178
|
+
its icon, and clicking it expands the sidebar onto the open group. On the bottom bar a group with a
|
|
179
|
+
page takes a slot and links there, one without takes none, and every group's children go into the
|
|
180
|
+
"More" sheet under its heading. Routes with no such children render exactly as before.
|
|
181
|
+
|
|
172
182
|
An app that cannot render the controller in the right state on the server — a prerendered site, a
|
|
173
183
|
client-only app — usually stamps `data-theme="plinth-light"` or `data-theme="plinth-dark"` on
|
|
174
184
|
`<html>` from a head script, so the very first paint is already right. That stamp is a **seed for
|
|
@@ -251,36 +261,71 @@ arrangement across reloads. `createFormatters` decides the
|
|
|
251
261
|
locale once, and `createHttpClient` takes the three things that actually differ between apps — the
|
|
252
262
|
base URL, how auth is injected, and how the backend spells an error body.
|
|
253
263
|
|
|
264
|
+
Work the reader waits on goes through **`toast.promise`**: a spinning `loading` toast that turns into
|
|
265
|
+
a success or an error in place once the work settles. It never rejects — it resolves to the value,
|
|
266
|
+
or to `undefined` on failure — and hands the failure to `onerror`, or to `reportError` when none is
|
|
267
|
+
given, the same rule `AsyncButton` follows. `toast.update(id, patch)` changes a live toast in place.
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
const saved = await toast.promise(api.post("/orders", draft), {
|
|
271
|
+
loading: "Saving…",
|
|
272
|
+
success: (order) => `Order ${order.id} saved.`,
|
|
273
|
+
error: (error) => errorMessage(error),
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
A request that failed in the page rather than in a toast renders **`RequestErrorDisplay`**, which
|
|
278
|
+
sorts the error with **`classifyError`** — offline, unauthorized, forbidden, not found, server or
|
|
279
|
+
unknown — and gives each kind its own icon and wording, all of it overridable through `labels`. It
|
|
280
|
+
never rethrows, so an `{:catch error}` branch that renders it is safe on the server too.
|
|
281
|
+
|
|
254
282
|
## Entry points
|
|
255
283
|
|
|
256
|
-
| Import | What it carries
|
|
257
|
-
| ---------------------------------- |
|
|
258
|
-
| `@viniaraujo68/plinth` | `AsyncAction`, the error reporter, `LIBRARY_VERSION`
|
|
259
|
-
| `@viniaraujo68/plinth/theme` | `ThemeContext`, `ThemeController`, `ThemeToggle`, the cookie helpers, `createCssColorReader`
|
|
260
|
-
| `@viniaraujo68/plinth/theme.css` | The stylesheet the tokens are declared in
|
|
261
|
-
| `@viniaraujo68/plinth/attachments` | The `tooltip` attachment, usable on any tag, and `sortable`, which reorders a list from its drag handles
|
|
262
|
-
| `@viniaraujo68/plinth/components` | The primitives, the `DateRange` and option-matching helpers
|
|
263
|
-
| `@viniaraujo68/plinth/confirm` | `confirm`, `Confirmer`, the challenge helpers
|
|
264
|
-
| `@viniaraujo68/plinth/shell` | `AppShell`, `Breadcrumbs`
|
|
265
|
-
| `@viniaraujo68/plinth/table` | `DataTable`, the sorting and column-layout functions behind it, `competitionRanks`, the sort ↔ URL helpers
|
|
266
|
-
| `@viniaraujo68/plinth/toast` | The toast queue and its host
|
|
267
|
-
| `@viniaraujo68/plinth/routing` | `RoutingConfig`, `RoutingContext`, `resolvePathname`, `withSearchParams`
|
|
268
|
-
| `@viniaraujo68/plinth/user` | `UserContext`, `ScopedComponent`, the anonymous default
|
|
269
|
-
| `@viniaraujo68/plinth/http` | `createHttpClient`, `ApiError`, `errorMessage`, `errorStatus`
|
|
270
|
-
| `@viniaraujo68/plinth/formatters` | `createFormatters`, over `Intl`
|
|
284
|
+
| Import | What it carries |
|
|
285
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
286
|
+
| `@viniaraujo68/plinth` | `AsyncAction`, the error reporter, `LIBRARY_VERSION` |
|
|
287
|
+
| `@viniaraujo68/plinth/theme` | `ThemeContext`, `ThemeController`, `ThemeToggle`, the cookie helpers, `createCssColorReader` |
|
|
288
|
+
| `@viniaraujo68/plinth/theme.css` | The stylesheet the tokens are declared in |
|
|
289
|
+
| `@viniaraujo68/plinth/attachments` | The `tooltip` attachment, usable on any tag, and `sortable`, which reorders a list from its drag handles |
|
|
290
|
+
| `@viniaraujo68/plinth/components` | The primitives, the `DateRange` and option-matching helpers |
|
|
291
|
+
| `@viniaraujo68/plinth/confirm` | `confirm`, `Confirmer`, the challenge helpers |
|
|
292
|
+
| `@viniaraujo68/plinth/shell` | `AppShell`, `TopBar`, `Breadcrumbs`, `NavigationProgress` |
|
|
293
|
+
| `@viniaraujo68/plinth/table` | `DataTable`, the sorting and column-layout functions behind it, `competitionRanks`, the sort ↔ URL helpers |
|
|
294
|
+
| `@viniaraujo68/plinth/toast` | The toast queue, `toast.loading`, `toast.promise`, `toast.update`, and its host |
|
|
295
|
+
| `@viniaraujo68/plinth/routing` | `RoutingConfig`, `RoutingContext`, `resolvePathname`, `withSearchParams`, `safeNext`, `loginUrl`, `withNext` |
|
|
296
|
+
| `@viniaraujo68/plinth/user` | `UserContext`, `ScopedComponent`, the anonymous default |
|
|
297
|
+
| `@viniaraujo68/plinth/http` | `createHttpClient`, `ApiError`, `errorMessage`, `errorStatus`, `classifyError` |
|
|
298
|
+
| `@viniaraujo68/plinth/formatters` | `createFormatters`, over `Intl` |
|
|
299
|
+
| `@viniaraujo68/plinth/i18n` | `createI18n`, `LocaleToggle`, the interpolation and plural helpers |
|
|
300
|
+
| `@viniaraujo68/plinth/virtual` | `Virtualizer`, `VirtualList`, `WindowedData`, `WindowedVirtualizer`, `DataList` |
|
|
301
|
+
| `@viniaraujo68/plinth/url-state` | `createUrlState`, `urlStore`, `SyncedState`, `createParamsCodec` and `param`, the codec contracts |
|
|
271
302
|
|
|
272
303
|
The components entry point ships `AsyncButton`, `Combobox`, `Copyable`, `DateRangePicker`,
|
|
273
|
-
`Dialog`, `DragHandle`, `Dropdown`, `ErrorDisplay`, `ErrorPage`, `JsonInput`, `Listbox`, `LoadingButton`, `Logo`,
|
|
274
|
-
`Modal`, `MultiSelect`, `Paginator`, `RefreshButton`, `SegmentedControl`, `Select`, `Skeleton`
|
|
275
|
-
`
|
|
304
|
+
`Dialog`, `DragHandle`, `Dropdown`, `ErrorDisplay`, `ErrorPage`, `JsonInput`, `JsonView`, `Listbox`, `LoadingButton`, `Logo`,
|
|
305
|
+
`Modal`, `MultiSelect`, `Paginator`, `RefreshButton`, `RequestErrorDisplay`, `SegmentedControl`, `Select`, `Skeleton`, `Tabs`,
|
|
306
|
+
`VirtualJsonView`
|
|
307
|
+
and `Tooltip`. Every one of them has a page in the showcase.
|
|
276
308
|
It also exports the mark's outline as data — `PICK_PATH` and `PICK_CLIP_PATH` — for anything
|
|
277
|
-
that wants the pick as a silhouette of its own.
|
|
309
|
+
that wants the pick as a silhouette of its own. Not for avatars: the shell draws a profile picture as
|
|
310
|
+
a disc, and an app's should be one too.
|
|
311
|
+
|
|
312
|
+
**`Tabs`** switches what one panel shows: a `tablist` with a roving tabindex, where the arrow keys,
|
|
313
|
+
Home and End move the selection and the selection follows the focus. It scrolls sideways on a phone
|
|
314
|
+
instead of wrapping, fading at the edge while tabs are hidden past it. `value` is bindable; a page
|
|
315
|
+
that keeps its section in the URL passes it in and navigates on `onchange`, and `idPrefix` makes the
|
|
316
|
+
tab ids predictable so the panel can name its tab through `aria-labelledby`.
|
|
278
317
|
|
|
279
318
|
`JsonInput` edits a JSON document as text: it reports through a bindable `invalid` whether the text
|
|
280
319
|
parses, shows the parser's message after the first blur, can reindent on blur, and sets the
|
|
281
320
|
textarea's custom validity so a form will not submit a broken document. `parseJsonText` and
|
|
282
321
|
`formatJsonText` are the same rules, for reading the text once it is sent.
|
|
283
322
|
|
|
323
|
+
`JsonView` shows a value read-only, as `JSON.stringify(value, null, 2)` would print it: `toJSON` is
|
|
324
|
+
honoured, so a `Date` shows as its ISO string, and a reference back to an ancestor shows a marker
|
|
325
|
+
instead of recursing. Nodes fold one by one, all at once or from `collapseDepth` down; the search
|
|
326
|
+
(Ctrl/Cmd+F while the viewer has the focus) reaches inside folds and unfolds what it lands on; and
|
|
327
|
+
copying — the whole document or a selection — writes the expanded text, never the fold summaries.
|
|
328
|
+
|
|
284
329
|
Two of those pick one thing from a list, and which one to reach for is decided by the length of
|
|
285
330
|
the list rather than by a flag:
|
|
286
331
|
|
|
@@ -337,6 +382,131 @@ Enter confirms only once it matches. The library ships no translations, so `conf
|
|
|
337
382
|
`cancelLabel`, `closeLabel` and `challengeLabel` on the host translate every confirm at once, and
|
|
338
383
|
any call overrides them through its own options.
|
|
339
384
|
|
|
385
|
+
## Top bar, progress line and the sign-in round trip
|
|
386
|
+
|
|
387
|
+
`AppShell` is built for an app with a navigation to show. One with two or three pages and an
|
|
388
|
+
account puts **`TopBar`** over the page instead: the brand at the start, the app's controls at the
|
|
389
|
+
end, and — once the user context says someone is signed in — their avatar, opening an account menu
|
|
390
|
+
with their name, the app's own `<li>` entries and signing out. Choosing an entry closes the menu,
|
|
391
|
+
and `--plinth-top-bar-width` caps the bar's content to the width of the page under it.
|
|
392
|
+
|
|
393
|
+
```svelte
|
|
394
|
+
<TopBar menuLabel={t("nav.accountMenu")} logoutLabel={t("nav.logout")}>
|
|
395
|
+
{#snippet brand()}<a href="/">Acme</a>{/snippet}
|
|
396
|
+
{#snippet actions()}
|
|
397
|
+
<LocaleToggle {i18n} label={t("nav.language")} />
|
|
398
|
+
<ThemeToggle iconOnly />
|
|
399
|
+
{/snippet}
|
|
400
|
+
{#snippet menu()}<li><a href="/account">{t("nav.account")}</a></li>{/snippet}
|
|
401
|
+
</TopBar>
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**`NavigationProgress`** goes with either shell: a line across the top of the window while a
|
|
405
|
+
navigation takes longer than `delay` (150ms). The library imports nothing from SvelteKit, so the app
|
|
406
|
+
says when one is under way — `<NavigationProgress active={navigating.to !== null} />`.
|
|
407
|
+
|
|
408
|
+
The routing entry point also carries the `?next=` round trip of a sign-in page. A guard sends a
|
|
409
|
+
visitor to **`loginUrl(page.url)`** — `/login?next=/where/they/were`, path only, since the query
|
|
410
|
+
string is where share tokens live. The sign-in page reads the parameter back through
|
|
411
|
+
**`safeNext`**, which keeps it only if the browser would resolve it to the same site:
|
|
412
|
+
`//evil.example`, `/\evil.example` and the spellings the URL parser turns into them come back
|
|
413
|
+
`null`, so a crafted link cannot send someone who just typed their password elsewhere.
|
|
414
|
+
**`withNext`** carries the same path across the link between sign-in and sign-up.
|
|
415
|
+
|
|
416
|
+
## Languages
|
|
417
|
+
|
|
418
|
+
`@viniaraujo68/plinth/i18n` is for the app's own text — the library still ships no translations,
|
|
419
|
+
and its components keep taking every word through props. **`createI18n`** takes a flat dictionary
|
|
420
|
+
per locale and returns the current locale as `$state`, a `t()` that reads it, and `tag`, the BCP 47
|
|
421
|
+
tag for `Intl`:
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
// src/lib/i18n.svelte.ts
|
|
425
|
+
import { createI18n } from "@viniaraujo68/plinth/i18n";
|
|
426
|
+
import pt from "./i18n/pt.js";
|
|
427
|
+
import en from "./i18n/en.js";
|
|
428
|
+
|
|
429
|
+
export const i18n = createI18n({
|
|
430
|
+
dictionaries: { pt, en },
|
|
431
|
+
tags: { pt: "pt-BR", en: "en-US" },
|
|
432
|
+
fallback: "pt",
|
|
433
|
+
unmatched: "en",
|
|
434
|
+
storageKey: "acme.locale",
|
|
435
|
+
});
|
|
436
|
+
export const { t } = i18n;
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
`t("days", { count: 3 })` fills `{count}` and picks between a `{ one, other }` pair — exactly one
|
|
440
|
+
takes `one`, everything else, zero included, takes `other`, which is not what CLDR says for
|
|
441
|
+
Portuguese and is what its readers expect. A key missing from the current dictionary is read from the
|
|
442
|
+
fallback one. The first visit starts on the browser's language, matched by language when the region
|
|
443
|
+
differs, or on `unmatched` when nothing matches; a later one starts on the pick
|
|
444
|
+
**`LocaleToggle`** remembered. Switching relabels `<html lang>` too. The server renders the fallback
|
|
445
|
+
locale and never switches — the instance is shared by every request — and the browser corrects the
|
|
446
|
+
text during hydration.
|
|
447
|
+
|
|
448
|
+
## URL state
|
|
449
|
+
|
|
450
|
+
`@viniaraujo68/plinth/url-state` keeps a screen's filters, sorting and paging in the query string,
|
|
451
|
+
so a link reopens what its sender was looking at and the back button undoes a search. It is state,
|
|
452
|
+
not a query layer: nothing in it fetches or caches.
|
|
453
|
+
|
|
454
|
+
```svelte
|
|
455
|
+
<script lang="ts">
|
|
456
|
+
import { createParamsCodec, createUrlState, param } from "@viniaraujo68/plinth/url-state";
|
|
457
|
+
|
|
458
|
+
const catalog = createParamsCodec({
|
|
459
|
+
q: param.string({ default: "" }),
|
|
460
|
+
sort: param.string({ oneOf: ["asc", "desc"], default: "asc" }),
|
|
461
|
+
tags: param.list(param.string()),
|
|
462
|
+
page: param.number({ default: 1, min: 1, integer: true }),
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const filters = createUrlState(catalog, { resetOnCommit: ["page"] });
|
|
466
|
+
</script>
|
|
467
|
+
|
|
468
|
+
<input bind:value={filters.draft.q} />
|
|
469
|
+
<button disabled={!filters.isDirty} onclick={filters.commit}>Apply</button>
|
|
470
|
+
<button onclick={() => filters.update((s) => ({ ...s, page: s.page + 1 }))}>Next</button>
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
A form edits **`draft`**; **`commit()`** normalizes it into **`committed`** and writes the owned
|
|
474
|
+
params to the URL, leaving every other param alone. **`update(fn)`** commits one change eagerly —
|
|
475
|
+
a page turn — without dragging half-typed filters along; **`revert()`** drops the draft's edits and
|
|
476
|
+
**`reset()`** returns both to the defaults. **`resetOnCommit`** names the fields that go back to
|
|
477
|
+
their defaults whenever a commit changes anything else, which is what returns a new search to page 1.
|
|
478
|
+
A URL change that does not change **`key()`** — a foreign param, a reorder — is ignored, so it never
|
|
479
|
+
clobbers a draft; a real one, such as the back button, replaces both halves and calls `onSynced`.
|
|
480
|
+
|
|
481
|
+
The handlers never throw on a hand-edited URL: a value they cannot read falls back to the field's
|
|
482
|
+
default — `number` rejects `NaN` and empty values and honours `min`, `max` and `integer`, `boolean`
|
|
483
|
+
takes only `"true"` and `"false"`, `date` reads `YYYY-MM-DD` as local midnight and refuses
|
|
484
|
+
impossible days, and an invalid `Date` is never written. A value equal to its default stays out of
|
|
485
|
+
the URL. `list` reads a repeated param, and its default is always the empty list.
|
|
486
|
+
|
|
487
|
+
Every commit is a `goto` with focus and scroll kept, and pushes a history entry. **`replace`**
|
|
488
|
+
rewrites the current entry instead — `true` for search-as-you-type, or a function of the previous
|
|
489
|
+
and next owned params, where `changedParams` helps:
|
|
490
|
+
`replace: (prev, next) => changedParams(prev, next).every((name) => name === "q")`. Several states
|
|
491
|
+
on one page can commit in the same tick; the writes merge into one navigation.
|
|
492
|
+
|
|
493
|
+
Create it while a component initializes: it follows the URL through an `$effect`. Effects do not
|
|
494
|
+
run on the server, so a server-rendered or prerendered page renders the defaults and picks up the
|
|
495
|
+
URL once it hydrates. A page whose first paint has to reflect the query string reads it in its own
|
|
496
|
+
`load` and is not prerendered.
|
|
497
|
+
|
|
498
|
+
`key()` is what a fetch is keyed by. With TanStack Query it goes straight into the key:
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
const books = createQuery(() => ({
|
|
502
|
+
queryKey: ["books", filters.key()],
|
|
503
|
+
queryFn: () => api.get(`/books?${catalog.serialize(filters.normalized())}`),
|
|
504
|
+
}));
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
`SyncedState` works without the URL too, and takes any `ExternalStore` — `read`, `write` and a
|
|
508
|
+
`subscribe` that must not call back synchronously — for state kept somewhere else.
|
|
509
|
+
|
|
340
510
|
## Development
|
|
341
511
|
|
|
342
512
|
```sh
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
|
|
2
4
|
interface Props {
|
|
3
5
|
/** Wire the `reset` a `svelte:boundary` hands to its `failed` snippet. Omitted, no button. */
|
|
4
6
|
reset?: () => void;
|
|
5
7
|
title?: string;
|
|
6
8
|
message?: string;
|
|
9
|
+
eyebrow?: string;
|
|
10
|
+
retryLabel?: string;
|
|
11
|
+
icon?: Snippet;
|
|
7
12
|
}
|
|
8
13
|
|
|
9
14
|
let {
|
|
10
15
|
reset,
|
|
11
16
|
title = "Something went wrong",
|
|
12
17
|
message = "The application ran into an unexpected problem.",
|
|
18
|
+
eyebrow = "Error",
|
|
19
|
+
retryLabel = "Try again",
|
|
20
|
+
icon,
|
|
13
21
|
}: Props = $props();
|
|
14
22
|
</script>
|
|
15
23
|
|
|
@@ -34,26 +42,32 @@ counted once per render. Wire `reportError` on the boundary's `onerror` instead.
|
|
|
34
42
|
class="flex h-full w-full flex-col items-center justify-center gap-2 p-8 text-center"
|
|
35
43
|
role="alert"
|
|
36
44
|
>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
{#if icon}
|
|
46
|
+
{@render icon()}
|
|
47
|
+
{:else}
|
|
48
|
+
<svg
|
|
49
|
+
class="size-12 text-base-content/40"
|
|
50
|
+
viewBox="0 0 24 24"
|
|
51
|
+
fill="none"
|
|
52
|
+
stroke="currentColor"
|
|
53
|
+
stroke-width="1.5"
|
|
54
|
+
stroke-linecap="round"
|
|
55
|
+
stroke-linejoin="round"
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
>
|
|
58
|
+
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
|
59
|
+
<path d="M12 9v4" />
|
|
60
|
+
<path d="M12 17h.01" />
|
|
61
|
+
</svg>
|
|
62
|
+
{/if}
|
|
63
|
+
{#if eyebrow}
|
|
64
|
+
<p class="text-xs font-medium tracking-widest text-base-content/55 uppercase">{eyebrow}</p>
|
|
65
|
+
{/if}
|
|
52
66
|
<p class="text-xl font-semibold">{title}</p>
|
|
53
67
|
<p class="max-w-md text-sm text-base-content/60">{message}</p>
|
|
54
68
|
{#if reset}
|
|
55
69
|
<div class="mt-4">
|
|
56
|
-
<button type="button" class="btn btn-primary" onclick={reset}>
|
|
70
|
+
<button type="button" class="btn btn-primary" onclick={reset}>{retryLabel}</button>
|
|
57
71
|
</div>
|
|
58
72
|
{/if}
|
|
59
73
|
</div>
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
1
2
|
interface Props {
|
|
2
3
|
/** Wire the `reset` a `svelte:boundary` hands to its `failed` snippet. Omitted, no button. */
|
|
3
4
|
reset?: () => void;
|
|
4
5
|
title?: string;
|
|
5
6
|
message?: string;
|
|
7
|
+
eyebrow?: string;
|
|
8
|
+
retryLabel?: string;
|
|
9
|
+
icon?: Snippet;
|
|
6
10
|
}
|
|
7
11
|
/**
|
|
8
12
|
* Calm in-place fallback for a crashed subtree: it says what happened and offers the way out.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import JsonViewFrame, {
|
|
3
|
+
type JsonViewProps,
|
|
4
|
+
type JsonViewRevealTarget,
|
|
5
|
+
} from "./JsonViewFrame.svelte";
|
|
6
|
+
|
|
7
|
+
const props: JsonViewProps = $props();
|
|
8
|
+
|
|
9
|
+
let scroller = $state<HTMLDivElement>();
|
|
10
|
+
|
|
11
|
+
const reveal = ({ line }: JsonViewRevealTarget) => {
|
|
12
|
+
const row = scroller?.querySelector<HTMLElement>(`[data-line="${line}"]`);
|
|
13
|
+
const target = row?.querySelector<HTMLElement>("[data-active]") ?? row;
|
|
14
|
+
target?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
15
|
+
};
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<JsonViewFrame {...props} {reveal}>
|
|
19
|
+
{#snippet viewport({ visible, line, rowId, widthCh })}
|
|
20
|
+
<div bind:this={scroller} class="plinth-json-scroller">
|
|
21
|
+
<div class="plinth-json-lines" style:min-width="max(100%, {widthCh}ch)">
|
|
22
|
+
{#each visible as index (rowId(index))}
|
|
23
|
+
{@render line(index)}
|
|
24
|
+
{/each}
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
{/snippet}
|
|
28
|
+
</JsonViewFrame>
|
|
29
|
+
|
|
30
|
+
<style>
|
|
31
|
+
.plinth-json-scroller {
|
|
32
|
+
flex: 1;
|
|
33
|
+
min-height: 0;
|
|
34
|
+
overflow: auto;
|
|
35
|
+
padding-block: 0.25rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.plinth-json-lines {
|
|
39
|
+
width: max-content;
|
|
40
|
+
min-width: 100%;
|
|
41
|
+
padding-inline-end: 1ch;
|
|
42
|
+
}
|
|
43
|
+
</style>
|