@viniaraujo68/plinth 0.3.1 → 0.6.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 (107) hide show
  1. package/README.md +197 -19
  2. package/dist/components/DateRangePicker.svelte +93 -57
  3. package/dist/components/DateRangePicker.svelte.d.ts +32 -15
  4. package/dist/components/ErrorDisplay.svelte +30 -16
  5. package/dist/components/ErrorDisplay.svelte.d.ts +4 -0
  6. package/dist/components/JsonView.svelte +43 -0
  7. package/dist/components/JsonView.svelte.d.ts +4 -0
  8. package/dist/components/JsonViewFrame.svelte +654 -0
  9. package/dist/components/JsonViewFrame.svelte.d.ts +31 -0
  10. package/dist/components/Paginator.svelte +34 -8
  11. package/dist/components/Paginator.svelte.d.ts +10 -0
  12. package/dist/components/RequestErrorDisplay.svelte +43 -0
  13. package/dist/components/RequestErrorDisplay.svelte.d.ts +11 -0
  14. package/dist/components/Tabs.svelte +204 -0
  15. package/dist/components/Tabs.svelte.d.ts +76 -0
  16. package/dist/components/VirtualJsonView.svelte +67 -0
  17. package/dist/components/VirtualJsonView.svelte.d.ts +4 -0
  18. package/dist/components/date-range.d.ts +82 -14
  19. package/dist/components/date-range.js +86 -5
  20. package/dist/components/index.d.ts +9 -2
  21. package/dist/components/index.js +15 -9
  22. package/dist/components/json-view.d.ts +70 -0
  23. package/dist/components/json-view.js +302 -0
  24. package/dist/components/request-error.d.ts +8 -0
  25. package/dist/components/request-error.js +55 -0
  26. package/dist/components/tabs.d.ts +5 -0
  27. package/dist/components/tabs.js +1 -0
  28. package/dist/http.d.ts +10 -5
  29. package/dist/http.js +34 -6
  30. package/dist/i18n/LocaleToggle.svelte +45 -0
  31. package/dist/i18n/LocaleToggle.svelte.d.ts +48 -0
  32. package/dist/i18n/i18n.svelte.d.ts +56 -0
  33. package/dist/i18n/i18n.svelte.js +94 -0
  34. package/dist/i18n/index.d.ts +3 -0
  35. package/dist/i18n/index.js +7 -0
  36. package/dist/i18n/translate.d.ts +30 -0
  37. package/dist/i18n/translate.js +38 -0
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.js +1 -1
  40. package/dist/redirect.d.ts +40 -0
  41. package/dist/redirect.js +59 -0
  42. package/dist/routing.svelte.d.ts +1 -0
  43. package/dist/routing.svelte.js +3 -0
  44. package/dist/shell/AppShell.svelte +265 -93
  45. package/dist/shell/AppShell.svelte.d.ts +3 -2
  46. package/dist/shell/NavigationProgress.svelte +85 -0
  47. package/dist/shell/NavigationProgress.svelte.d.ts +31 -0
  48. package/dist/shell/TopBar.svelte +191 -0
  49. package/dist/shell/TopBar.svelte.d.ts +41 -0
  50. package/dist/shell/UserAvatar.svelte +50 -0
  51. package/dist/shell/UserAvatar.svelte.d.ts +14 -0
  52. package/dist/shell/index.d.ts +2 -0
  53. package/dist/shell/index.js +7 -3
  54. package/dist/shell/initials.d.ts +6 -0
  55. package/dist/shell/initials.js +13 -0
  56. package/dist/table/DataTable.svelte +199 -9
  57. package/dist/table/DataTable.svelte.d.ts +28 -5
  58. package/dist/table/FilterBar.svelte +417 -0
  59. package/dist/table/FilterBar.svelte.d.ts +34 -0
  60. package/dist/table/FilterRuleEditor.svelte +290 -0
  61. package/dist/table/FilterRuleEditor.svelte.d.ts +33 -0
  62. package/dist/table/filter-labels.d.ts +23 -0
  63. package/dist/table/filter-labels.js +30 -0
  64. package/dist/table/filter.d.ts +27 -0
  65. package/dist/table/filter.js +279 -0
  66. package/dist/table/index.d.ts +4 -1
  67. package/dist/table/index.js +5 -2
  68. package/dist/table/types.d.ts +49 -0
  69. package/dist/toast/Toaster.svelte +7 -3
  70. package/dist/toast/index.d.ts +1 -1
  71. package/dist/toast/toast-manager.svelte.d.ts +29 -1
  72. package/dist/toast/toast-manager.svelte.js +61 -1
  73. package/dist/url-state/codec.d.ts +15 -0
  74. package/dist/url-state/codec.js +1 -0
  75. package/dist/url-state/index.d.ts +5 -0
  76. package/dist/url-state/index.js +4 -0
  77. package/dist/url-state/params-codec.d.ts +73 -0
  78. package/dist/url-state/params-codec.js +116 -0
  79. package/dist/url-state/search-params.d.ts +4 -0
  80. package/dist/url-state/search-params.js +28 -0
  81. package/dist/url-state/synced-state.svelte.d.ts +20 -0
  82. package/dist/url-state/synced-state.svelte.js +88 -0
  83. package/dist/url-state/url-store.svelte.d.ts +12 -0
  84. package/dist/url-state/url-store.svelte.js +56 -0
  85. package/dist/virtual/DataList.svelte +215 -0
  86. package/dist/virtual/DataList.svelte.d.ts +52 -0
  87. package/dist/virtual/VirtualList.svelte +150 -0
  88. package/dist/virtual/VirtualList.svelte.d.ts +42 -0
  89. package/dist/virtual/index.d.ts +7 -0
  90. package/dist/virtual/index.js +6 -0
  91. package/dist/virtual/keyed-cache.d.ts +7 -0
  92. package/dist/virtual/keyed-cache.js +20 -0
  93. package/dist/virtual/labels.d.ts +12 -0
  94. package/dist/virtual/labels.js +6 -0
  95. package/dist/virtual/paging.d.ts +5 -0
  96. package/dist/virtual/paging.js +11 -0
  97. package/dist/virtual/range.d.ts +9 -0
  98. package/dist/virtual/range.js +31 -0
  99. package/dist/virtual/size-tree.d.ts +10 -0
  100. package/dist/virtual/size-tree.js +56 -0
  101. package/dist/virtual/virtualizer.svelte.d.ts +38 -0
  102. package/dist/virtual/virtualizer.svelte.js +295 -0
  103. package/dist/virtual/windowed-data.svelte.d.ts +38 -0
  104. package/dist/virtual/windowed-data.svelte.js +139 -0
  105. package/dist/virtual/windowed-virtualizer.svelte.d.ts +9 -0
  106. package/dist/virtual/windowed-virtualizer.svelte.js +22 -0
  107. package/package.json +13 -1
@@ -1,3 +1,23 @@
1
+ <script lang="ts" module>
2
+ export interface PaginatorLabels {
3
+ navigation: string;
4
+ previous: string;
5
+ next: string;
6
+ page: (page: number) => string;
7
+ pageSize: string;
8
+ summary: (first: number, last: number, total: number) => string;
9
+ }
10
+
11
+ export const DEFAULT_PAGINATOR_LABELS: PaginatorLabels = {
12
+ navigation: "Pagination",
13
+ previous: "Previous page",
14
+ next: "Next page",
15
+ page: (page) => `Page ${page}`,
16
+ pageSize: "Items per page",
17
+ summary: (first, last, total) => `Showing ${first} to ${last} of ${total} items`,
18
+ };
19
+ </script>
20
+
1
21
  <script lang="ts">
2
22
  import type { ClassValue } from "svelte/elements";
3
23
 
@@ -32,6 +52,7 @@
32
52
  * and the reset back to page 1, since a new size invalidates the old page arithmetic.
33
53
  */
34
54
  onPageSizeChange?: (size: number) => void;
55
+ labels?: Partial<PaginatorLabels>;
35
56
  } & (CountedPages | UncountedPages);
36
57
 
37
58
  let {
@@ -44,8 +65,11 @@
44
65
  totalPages,
45
66
  totalItems,
46
67
  hasNextPage,
68
+ labels,
47
69
  }: Props = $props();
48
70
 
71
+ const text = $derived({ ...DEFAULT_PAGINATOR_LABELS, ...labels });
72
+
49
73
  const range = (start: number, end: number): number[] =>
50
74
  Array.from({ length: Math.max(0, end - start + 1) }, (_, i) => start + i);
51
75
 
@@ -123,7 +147,7 @@ that means. Sizing is in `em`, so the whole strip scales with the font size arou
123
147
  class="btn h-[1.75em] min-w-[1.75em] px-2 tabular-nums btn-primary aria-[current=false]:btn-ghost"
124
148
  onclick={() => setPage(page)}
125
149
  aria-current={page === pageNumber}
126
- aria-label={`Page ${page}`}
150
+ aria-label={text.page(page)}
127
151
  >
128
152
  {page}
129
153
  </button>
@@ -133,7 +157,7 @@ that means. Sizing is in `em`, so the whole strip scales with the font size arou
133
157
  <button
134
158
  type="button"
135
159
  class="btn size-[1.75em] btn-ghost px-1"
136
- aria-label="Previous page"
160
+ aria-label={text.previous}
137
161
  disabled={pageNumber <= 1}
138
162
  onclick={() => setPage(pageNumber - 1)}
139
163
  >
@@ -153,7 +177,7 @@ that means. Sizing is in `em`, so the whole strip scales with the font size arou
153
177
  <button
154
178
  type="button"
155
179
  class="btn size-[1.75em] btn-ghost px-1"
156
- aria-label="Next page"
180
+ aria-label={text.next}
157
181
  disabled={!canGoNext}
158
182
  onclick={() => setPage(pageNumber + 1)}
159
183
  >
@@ -162,19 +186,19 @@ that means. Sizing is in `em`, so the whole strip scales with the font size arou
162
186
  {/snippet}
163
187
 
164
188
  {#if totalItems === undefined || pageSize === undefined}
165
- <nav class={["flex place-content-center gap-px", cls]} aria-label="Pagination">
189
+ <nav class={["flex place-content-center gap-px", cls]} aria-label={text.navigation}>
166
190
  {@render pageSelector()}
167
191
  </nav>
168
192
  {:else}
169
- <nav class={["@container", cls]} aria-label="Pagination">
193
+ <nav class={["@container", cls]} aria-label={text.navigation}>
170
194
  <div class="grid items-center justify-items-center gap-1 @min-[45rem]:grid-cols-3">
171
195
  <!-- Narrow, the summary and the size picker fall below the buttons: the control the user
172
196
  came for stays at the top of the block rather than being pushed off by chrome. -->
173
197
  <div class="flex items-center gap-1 @max-[45rem]:order-last @min-[45rem]:mr-auto">
174
- <span class="text-nowrap">Items per page:</span>
198
+ <span class="text-nowrap">{text.pageSize}:</span>
175
199
  <select
176
200
  class="select box-content w-[3ch] select-xs"
177
- aria-label="Items per page"
201
+ aria-label={text.pageSize}
178
202
  value={pageSize}
179
203
  onchange={({ currentTarget }) => onPageSizeChange?.(Number(currentTarget.value))}
180
204
  >
@@ -185,7 +209,9 @@ that means. Sizing is in `em`, so the whole strip scales with the font size arou
185
209
  </div>
186
210
 
187
211
  <div class="text-center tabular-nums" data-testid="paginator-summary">
188
- Showing {firstShown} to {lastShown} of {totalItems} items
212
+ {#if firstShown !== undefined && lastShown !== undefined}
213
+ {text.summary(firstShown, lastShown, totalItems)}
214
+ {/if}
189
215
  </div>
190
216
 
191
217
  <div class="flex items-center gap-px @max-[45rem]:order-first @min-[45rem]:ml-auto">
@@ -1,3 +1,12 @@
1
+ export interface PaginatorLabels {
2
+ navigation: string;
3
+ previous: string;
4
+ next: string;
5
+ page: (page: number) => string;
6
+ pageSize: string;
7
+ summary: (first: number, last: number, total: number) => string;
8
+ }
9
+ export declare const DEFAULT_PAGINATOR_LABELS: PaginatorLabels;
1
10
  import type { ClassValue } from "svelte/elements";
2
11
  /** The server answered with a count: the last page is reachable and the summary is exact. */
3
12
  interface CountedPages {
@@ -28,6 +37,7 @@ type Props = {
28
37
  * and the reset back to page 1, since a new size invalidates the old page arithmetic.
29
38
  */
30
39
  onPageSizeChange?: (size: number) => void;
40
+ labels?: Partial<PaginatorLabels>;
31
41
  } & (CountedPages | UncountedPages);
32
42
  /**
33
43
  * Page selector for a list, in two flavours picked by which props are passed: `totalPages`
@@ -0,0 +1,43 @@
1
+ <script lang="ts">
2
+ import { classifyError } from "../http.js";
3
+ import ErrorDisplay from "./ErrorDisplay.svelte";
4
+ import {
5
+ DEFAULT_REQUEST_ERROR_COPY,
6
+ REQUEST_ERROR_ICONS,
7
+ type RequestErrorLabels,
8
+ } from "./request-error.js";
9
+
10
+ interface Props {
11
+ error: unknown;
12
+ reset?: () => void;
13
+ labels?: RequestErrorLabels;
14
+ eyebrow?: string;
15
+ retryLabel?: string;
16
+ }
17
+
18
+ let { error, reset, labels, eyebrow = "Error", retryLabel = "Try again" }: Props = $props();
19
+
20
+ const kind = $derived(classifyError(error));
21
+ const copy = $derived({ ...DEFAULT_REQUEST_ERROR_COPY[kind], ...labels?.[kind] });
22
+ </script>
23
+
24
+ <div class="contents" data-kind={kind}>
25
+ <ErrorDisplay {reset} title={copy.title} message={copy.message} {eyebrow} {retryLabel}>
26
+ {#snippet icon()}
27
+ <svg
28
+ class="size-12 text-base-content/40"
29
+ viewBox="0 0 24 24"
30
+ fill="none"
31
+ stroke="currentColor"
32
+ stroke-width="1.5"
33
+ stroke-linecap="round"
34
+ stroke-linejoin="round"
35
+ aria-hidden="true"
36
+ >
37
+ {#each REQUEST_ERROR_ICONS[kind] as d (d)}
38
+ <path {d} />
39
+ {/each}
40
+ </svg>
41
+ {/snippet}
42
+ </ErrorDisplay>
43
+ </div>
@@ -0,0 +1,11 @@
1
+ import { type RequestErrorLabels } from "./request-error.js";
2
+ interface Props {
3
+ error: unknown;
4
+ reset?: () => void;
5
+ labels?: RequestErrorLabels;
6
+ eyebrow?: string;
7
+ retryLabel?: string;
8
+ }
9
+ declare const RequestErrorDisplay: import("svelte").Component<Props, {}, "">;
10
+ type RequestErrorDisplay = ReturnType<typeof RequestErrorDisplay>;
11
+ export default RequestErrorDisplay;
@@ -0,0 +1,204 @@
1
+ <script lang="ts" generics="Id extends string">
2
+ import { tick } from "svelte";
3
+ import type { ClassValue } from "svelte/elements";
4
+ import type { TabItem } from "./tabs.js";
5
+
6
+ interface Props {
7
+ /** The tabs, in the order they are shown. `id` is the value, so it has to be unique. */
8
+ tabs: readonly TabItem<Id>[];
9
+ /** The selected tab's `id`. Bindable, and also reported through {@link Props.onchange}. */
10
+ value: Id;
11
+ /** Accessible name of the tab list. A prop because the library ships no translations. */
12
+ label: string;
13
+ /**
14
+ * The `id` of the panel the tabs drive. One panel whose content changes with the selection,
15
+ * which is how a page with a tab per section is usually built; leave it out when there is no
16
+ * single panel to point at.
17
+ */
18
+ controls?: string;
19
+ /**
20
+ * Prefix of each tab's element id: the tab for `ranking` is `${idPrefix}-ranking`, which is
21
+ * what the panel's `aria-labelledby` points at. Unique per instance by default; pass one when
22
+ * the panel has to name its tab.
23
+ */
24
+ idPrefix?: string;
25
+ /** Centres the tabs while they fit. Once they overflow they scroll from the first one. */
26
+ center?: boolean;
27
+ /** Classes for the wrapper — spacing, mostly. */
28
+ class?: ClassValue;
29
+ /** Called with the new `id` only when it actually changes; re-picking the current tab is silent. */
30
+ onchange?: (id: Id) => void;
31
+ }
32
+
33
+ const uid = $props.id();
34
+
35
+ let {
36
+ tabs,
37
+ value = $bindable(),
38
+ label,
39
+ controls,
40
+ idPrefix = uid,
41
+ center = false,
42
+ class: className,
43
+ onchange,
44
+ }: Props = $props();
45
+
46
+ const tabId = (id: Id) => `${idPrefix}-${id}`;
47
+
48
+ const select = (id: Id) => {
49
+ if (id === value) return;
50
+ value = id;
51
+ onchange?.(id);
52
+ };
53
+
54
+ const STEPS: Record<string, (from: number, count: number) => number> = {
55
+ ArrowRight: (from, count) => (from + 1) % count,
56
+ ArrowLeft: (from, count) => (from - 1 + count) % count,
57
+ Home: () => 0,
58
+ End: (_, count) => count - 1,
59
+ };
60
+
61
+ const onkeydown = async (event: KeyboardEvent) => {
62
+ const step = STEPS[event.key];
63
+ if (!step || tabs.length === 0) return;
64
+ event.preventDefault();
65
+ const from = Math.max(
66
+ tabs.findIndex((tab) => tab.id === value),
67
+ 0,
68
+ );
69
+ const id = tabs[step(from, tabs.length)].id;
70
+ select(id);
71
+ // Selection follows focus, and a caller that keeps the selection in the URL navigates on
72
+ // `onchange` — which leaves the focus wherever it was. Walk it to the tab that is now selected.
73
+ await tick();
74
+ document.getElementById(tabId(id))?.focus();
75
+ };
76
+
77
+ let strip = $state<HTMLElement>();
78
+ let overflowing = $state(false);
79
+
80
+ const measure = () => {
81
+ if (!strip) return;
82
+ overflowing = strip.scrollWidth - strip.clientWidth - strip.scrollLeft > 4;
83
+ };
84
+
85
+ $effect(() => {
86
+ // Re-measured whenever the labels change — a language switch can push the last tab out.
87
+ void tabs.map((tab) => tab.label);
88
+ measure();
89
+ });
90
+ </script>
91
+
92
+ <!--
93
+ @component
94
+ A row of tabs that switches what one panel shows: a real `tablist` with a roving tabindex, so Tab
95
+ lands on the selected tab and the arrow keys, Home and End move the selection along the row. The
96
+ selection follows focus, the pattern for panels that are cheap to show.
97
+
98
+ On a narrow screen the row scrolls sideways instead of wrapping, and while tabs are hidden past the
99
+ right edge it fades out there — at phone width, the fade is the only hint that a fifth tab exists.
100
+
101
+ The selection is the caller's: bind `value`, or pass it in and act on `onchange` — a page that
102
+ keeps its tab in the URL navigates there, and the tab moves when the URL does.
103
+
104
+ ```svelte
105
+ <Tabs
106
+ bind:value={tab}
107
+ label="Sections"
108
+ controls="panel"
109
+ idPrefix="tab"
110
+ tabs={[
111
+ { id: "overview", label: "Overview" },
112
+ { id: "settings", label: "Settings" },
113
+ ]}
114
+ />
115
+ <div id="panel" role="tabpanel" aria-labelledby="tab-{tab}">…</div>
116
+ ```
117
+ -->
118
+ <div class={["plinth-tabs", className]} class:fade={overflowing}>
119
+ <div
120
+ class="plinth-tabs-strip tabs tabs-border"
121
+ class:center
122
+ role="tablist"
123
+ aria-label={label}
124
+ bind:this={strip}
125
+ onscroll={measure}
126
+ >
127
+ {#each tabs as tab (tab.id)}
128
+ <button
129
+ type="button"
130
+ class="tab"
131
+ role="tab"
132
+ id={tabId(tab.id)}
133
+ aria-selected={tab.id === value}
134
+ aria-controls={controls}
135
+ tabindex={tab.id === value ? 0 : -1}
136
+ onclick={() => select(tab.id)}
137
+ {onkeydown}
138
+ >
139
+ {tab.label}
140
+ </button>
141
+ {/each}
142
+ </div>
143
+ </div>
144
+
145
+ <svelte:window onresize={measure} />
146
+
147
+ <style>
148
+ .plinth-tabs {
149
+ position: relative;
150
+ }
151
+
152
+ /* `display` stated here rather than left to daisyUI's `.tabs`: the one-row layout is what the
153
+ overflow and the fade are built on, so it cannot depend on a class the app might restyle. */
154
+ .plinth-tabs-strip {
155
+ display: flex;
156
+ flex-wrap: nowrap;
157
+ overflow-x: auto;
158
+ border-bottom: 1px solid color-mix(in oklch, var(--color-base-content) 12%, transparent);
159
+ -webkit-overflow-scrolling: touch;
160
+ scrollbar-width: none;
161
+ }
162
+
163
+ .plinth-tabs-strip::-webkit-scrollbar {
164
+ display: none;
165
+ }
166
+
167
+ /* `safe` keeps the first tab reachable once the row overflows; plain `center` would push it off
168
+ the left edge, where no scroll can reach. The first declaration is the fallback. */
169
+ .plinth-tabs-strip.center {
170
+ justify-content: center;
171
+ justify-content: safe center;
172
+ }
173
+
174
+ /* A mask rather than a gradient laid over the strip, so it fades into whatever surface the tabs
175
+ sit on. Only while something is actually hidden past the edge. */
176
+ .plinth-tabs.fade .plinth-tabs-strip {
177
+ -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2.75rem), transparent);
178
+ mask-image: linear-gradient(to right, #000 calc(100% - 2.75rem), transparent);
179
+ }
180
+
181
+ .tab {
182
+ flex: 0 0 auto;
183
+ min-height: 2.75rem;
184
+ white-space: nowrap;
185
+ }
186
+
187
+ /* Inside a horizontal scroller an outset ring is clipped; drawn inside, it shows whole. */
188
+ .tab:focus-visible {
189
+ outline-offset: -2px;
190
+ }
191
+
192
+ .tab[aria-selected="true"] {
193
+ color: var(--color-base-content);
194
+ font-weight: 600;
195
+ }
196
+
197
+ .tab:not([aria-selected="true"]) {
198
+ color: color-mix(in oklch, var(--color-base-content) 65%, transparent);
199
+ }
200
+
201
+ .tab:not([aria-selected="true"]):hover {
202
+ color: var(--color-base-content);
203
+ }
204
+ </style>
@@ -0,0 +1,76 @@
1
+ import type { ClassValue } from "svelte/elements";
2
+ import type { TabItem } from "./tabs.js";
3
+ declare function $$render<Id extends string>(): {
4
+ props: {
5
+ /** The tabs, in the order they are shown. `id` is the value, so it has to be unique. */
6
+ tabs: readonly TabItem<Id>[];
7
+ /** The selected tab's `id`. Bindable, and also reported through {@link Props.onchange}. */
8
+ value: Id;
9
+ /** Accessible name of the tab list. A prop because the library ships no translations. */
10
+ label: string;
11
+ /**
12
+ * The `id` of the panel the tabs drive. One panel whose content changes with the selection,
13
+ * which is how a page with a tab per section is usually built; leave it out when there is no
14
+ * single panel to point at.
15
+ */
16
+ controls?: string;
17
+ /**
18
+ * Prefix of each tab's element id: the tab for `ranking` is `${idPrefix}-ranking`, which is
19
+ * what the panel's `aria-labelledby` points at. Unique per instance by default; pass one when
20
+ * the panel has to name its tab.
21
+ */
22
+ idPrefix?: string;
23
+ /** Centres the tabs while they fit. Once they overflow they scroll from the first one. */
24
+ center?: boolean;
25
+ /** Classes for the wrapper — spacing, mostly. */
26
+ class?: ClassValue;
27
+ /** Called with the new `id` only when it actually changes; re-picking the current tab is silent. */
28
+ onchange?: (id: Id) => void;
29
+ };
30
+ exports: {};
31
+ bindings: "value";
32
+ slots: {};
33
+ events: {};
34
+ };
35
+ declare class __sveltets_Render<Id extends string> {
36
+ props(): ReturnType<typeof $$render<Id>>['props'];
37
+ events(): ReturnType<typeof $$render<Id>>['events'];
38
+ slots(): ReturnType<typeof $$render<Id>>['slots'];
39
+ bindings(): "value";
40
+ exports(): {};
41
+ }
42
+ interface $$IsomorphicComponent {
43
+ new <Id extends string>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Id>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Id>['props']>, ReturnType<__sveltets_Render<Id>['events']>, ReturnType<__sveltets_Render<Id>['slots']>> & {
44
+ $$bindings?: ReturnType<__sveltets_Render<Id>['bindings']>;
45
+ } & ReturnType<__sveltets_Render<Id>['exports']>;
46
+ <Id extends string>(internal: unknown, props: ReturnType<__sveltets_Render<Id>['props']> & {}): ReturnType<__sveltets_Render<Id>['exports']>;
47
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
48
+ }
49
+ /**
50
+ * A row of tabs that switches what one panel shows: a real `tablist` with a roving tabindex, so Tab
51
+ * lands on the selected tab and the arrow keys, Home and End move the selection along the row. The
52
+ * selection follows focus, the pattern for panels that are cheap to show.
53
+ *
54
+ * On a narrow screen the row scrolls sideways instead of wrapping, and while tabs are hidden past the
55
+ * right edge it fades out there — at phone width, the fade is the only hint that a fifth tab exists.
56
+ *
57
+ * The selection is the caller's: bind `value`, or pass it in and act on `onchange` — a page that
58
+ * keeps its tab in the URL navigates there, and the tab moves when the URL does.
59
+ *
60
+ * ```svelte
61
+ * <Tabs
62
+ * bind:value={tab}
63
+ * label="Sections"
64
+ * controls="panel"
65
+ * idPrefix="tab"
66
+ * tabs={[
67
+ * { id: "overview", label: "Overview" },
68
+ * { id: "settings", label: "Settings" },
69
+ * ]}
70
+ * />
71
+ * <div id="panel" role="tabpanel" aria-labelledby="tab-{tab}">…</div>
72
+ * ```
73
+ */
74
+ declare const Tabs: $$IsomorphicComponent;
75
+ type Tabs<Id extends string> = InstanceType<typeof Tabs<Id>>;
76
+ export default Tabs;
@@ -0,0 +1,67 @@
1
+ <script lang="ts">
2
+ import VirtualList from "../virtual/VirtualList.svelte";
3
+ import JsonViewFrame, {
4
+ type JsonViewProps,
5
+ type JsonViewRevealTarget,
6
+ } from "./JsonViewFrame.svelte";
7
+
8
+ const props: JsonViewProps = $props();
9
+
10
+ let list = $state<ReturnType<typeof VirtualList>>();
11
+ let host = $state<HTMLDivElement>();
12
+ let probe = $state<HTMLSpanElement>();
13
+
14
+ const reveal = ({ visibleIndex, columnCh }: JsonViewRevealTarget) => {
15
+ list?.scrollToIndex(visibleIndex, "center");
16
+ const scroller = host?.querySelector<HTMLElement>(".plinth-virtual");
17
+ const ch = probe?.getBoundingClientRect().width ?? 0;
18
+ if (!scroller || ch === 0) return;
19
+ const target = columnCh * ch;
20
+ const start = scroller.scrollLeft;
21
+ const end = start + scroller.clientWidth;
22
+ if (target < start + ch * 4 || target > end - ch * 4)
23
+ scroller.scrollLeft = Math.max(0, target - scroller.clientWidth / 2);
24
+ };
25
+ </script>
26
+
27
+ <JsonViewFrame {...props} {reveal}>
28
+ {#snippet viewport({ visible, line, rowId, rowHeight, widthCh })}
29
+ <div bind:this={host} class="plinth-json-virtual">
30
+ <span bind:this={probe} class="plinth-json-probe" aria-hidden="true"></span>
31
+ <VirtualList
32
+ bind:this={list}
33
+ class="plinth-json-virtual-list"
34
+ count={visible.length}
35
+ itemSize={rowHeight}
36
+ getItemKey={(index) => rowId(visible[index])}
37
+ crossSize="max(100%, {widthCh + 1}ch)"
38
+ >
39
+ {#snippet item(index)}
40
+ {@render line(visible[index])}
41
+ {/snippet}
42
+ </VirtualList>
43
+ </div>
44
+ {/snippet}
45
+ </JsonViewFrame>
46
+
47
+ <style>
48
+ .plinth-json-virtual {
49
+ position: relative;
50
+ display: flex;
51
+ flex: 1;
52
+ flex-direction: column;
53
+ min-height: 0;
54
+ }
55
+
56
+ .plinth-json-probe {
57
+ position: absolute;
58
+ width: 1ch;
59
+ height: 0;
60
+ visibility: hidden;
61
+ }
62
+
63
+ :global(.plinth-json-virtual-list) {
64
+ flex: 1;
65
+ min-height: 0;
66
+ }
67
+ </style>
@@ -0,0 +1,4 @@
1
+ import { type JsonViewProps } from "./JsonViewFrame.svelte";
2
+ declare const VirtualJsonView: import("svelte").Component<JsonViewProps, {}, "">;
3
+ type VirtualJsonView = ReturnType<typeof VirtualJsonView>;
4
+ export default VirtualJsonView;
@@ -1,30 +1,61 @@
1
1
  /**
2
2
  * The pair a range picker emits, and the whole of what it emits.
3
3
  *
4
- * Both ends are ISO-8601 instants in UTC (`2026-08-29T18:04:05.123Z`), the form `toISOString()`
5
- * produces. A string rather than a `Date` because this value's job is to travel: into a fetch
6
- * body, a query string, a store, a load function's return — all of which serialise a `Date` to
7
- * exactly this string anyway, while an ISO string additionally survives the round trip back and
8
- * sorts correctly with a plain `<`. UTC rather than a local offset because two clients in
9
- * different zones must be able to name the same window with the same characters.
4
+ * In the default `"datetime"` mode both ends are ISO-8601 instants in UTC
5
+ * (`2026-08-29T18:04:05.123Z`), the form `toISOString()` produces. A string rather than a `Date`
6
+ * because this value's job is to travel: into a fetch body, a query string, a store, a load
7
+ * function's return — all of which serialise a `Date` to exactly this string anyway, while an ISO
8
+ * string additionally survives the round trip back and sorts correctly with a plain `<`. UTC rather
9
+ * than a local offset because two clients in different zones must be able to name the same window
10
+ * with the same characters.
11
+ *
12
+ * In `"date"` mode both ends are calendar days, `2026-08-29` — the value of an
13
+ * `<input type="date">`, with no time and no zone, because a day is what the reader picked. An
14
+ * empty string leaves that side open: `{ from: "2026-03-01", to: "" }` is "since March", and two
15
+ * empty ends are all time.
10
16
  */
11
17
  export interface DateRange {
12
18
  from: string;
13
19
  to: string;
14
20
  }
21
+ /**
22
+ * What the picker works in: `"datetime"` for a window of instants (a dashboard's last 24 hours),
23
+ * `"date"` for a span of calendar days (a history filtered to last month).
24
+ */
25
+ export type DateRangeMode = "datetime" | "date";
26
+ interface PresetBase {
27
+ /** Stable across renders — it is what the picker's selection prop holds. */
28
+ id: string;
29
+ label: string;
30
+ }
15
31
  /**
16
32
  * One relative window offered as a button — a fixed duration measured back from "now".
17
33
  *
18
34
  * `durationMs` is a duration and not a calendar step on purpose: "30d" means 30×24h, so the
19
35
  * window keeps its length across a DST boundary instead of silently growing or shrinking an hour
20
- * twice a year. A caller who really wants "this month" wants a custom range, not a preset.
36
+ * twice a year. A window that follows the calendar — this month, last month — is a
37
+ * {@link CalendarPreset}.
38
+ *
39
+ * In `"date"` mode the window becomes the days it touches: from the day `durationMs` ago to today.
21
40
  */
22
- export interface DateRangePreset {
23
- /** Stable across renders — it is what the picker's selection prop holds. */
24
- id: string;
25
- label: string;
41
+ export interface DurationPreset extends PresetBase {
26
42
  durationMs: number;
43
+ range?: never;
27
44
  }
45
+ /**
46
+ * One window offered as a button and computed from the current moment — for the windows a
47
+ * duration cannot name: this month, last month, the last six months, all time. `range` returns the
48
+ * pair in the picker's mode, and runs again on every re-anchor, so "this month" turns into the
49
+ * next one when the month does.
50
+ *
51
+ * {@link thisMonth}, {@link lastMonth}, {@link lastMonths} and {@link allTime} build the usual
52
+ * ones for `"date"` mode.
53
+ */
54
+ export interface CalendarPreset extends PresetBase {
55
+ range: (now: Date) => DateRange;
56
+ durationMs?: never;
57
+ }
58
+ export type DateRangePreset = DurationPreset | CalendarPreset;
28
59
  /**
29
60
  * The selection value that means "the user typed the two ends themselves".
30
61
  *
@@ -33,8 +64,10 @@ export interface DateRangePreset {
33
64
  export declare const CUSTOM_PRESET_ID = "custom";
34
65
  /** The four windows a dashboard actually gets asked for. Replace the list, not the component. */
35
66
  export declare const DEFAULT_PRESETS: readonly DateRangePreset[];
36
- /** Builds the window a preset names, ending at `now`. */
37
- export declare const buildRange: (preset: DateRangePreset, now?: Date | number) => DateRange;
67
+ /** A local calendar day as `YYYY-MM-DD`, the value an `<input type="date">` holds. */
68
+ export declare const toDay: (date: Date) => string;
69
+ /** Builds the window a preset names as of `now`, in the picker's `mode`. */
70
+ export declare const buildRange: (preset: DateRangePreset, now?: Date | number, mode?: DateRangeMode) => DateRange;
38
71
  /**
39
72
  * Resolves a selection into the window it stands for right now, or `null` when there is nothing
40
73
  * to build — an unknown id, or {@link CUSTOM_PRESET_ID}.
@@ -43,7 +76,41 @@ export declare const buildRange: (preset: DateRangePreset, now?: Date | number)
43
76
  * range is never re-anchored" a property of the data rather than of the timer's wiring: even a
44
77
  * tick that somehow fired while the user was in custom mode would have nothing to emit.
45
78
  */
46
- export declare const anchorSelection: (selection: string, presets: readonly DateRangePreset[], now?: Date | number) => DateRange | null;
79
+ export declare const anchorSelection: (selection: string, presets: readonly DateRangePreset[], now?: Date | number, mode?: DateRangeMode) => DateRange | null;
80
+ /**
81
+ * The selection a picker handed `value` (and no `preset`) should open on: the preset whose window
82
+ * right now is exactly that pair, or {@link CUSTOM_PRESET_ID}. A page that remounts its picker —
83
+ * one per tab, say — gets its "Last month" button back instead of a custom pair spelling it out.
84
+ *
85
+ * A duration preset in `"datetime"` mode only matches to the millisecond, so in practice a seeded
86
+ * instant pair still opens as custom, as it always did.
87
+ */
88
+ export declare const selectionFor: (value: DateRange, presets: readonly DateRangePreset[], now?: Date | number, mode?: DateRangeMode) => string;
89
+ /**
90
+ * Whether a pair of days is in order. An empty end leaves that side open, so it can never put the
91
+ * pair out of order.
92
+ */
93
+ export declare const isValidDayRange: (from: string, to: string) => boolean;
94
+ /**
95
+ * Reads an `<input type="date">` value back: the day itself, `""` for an empty field — no bound on
96
+ * that side — or `null` for anything that is not yet a real day.
97
+ *
98
+ * A year below 1000 counts as not yet a day. Chrome reports a value on every digit of the year as
99
+ * it is typed — `0002-03-01`, `0020-03-01`, `0202-03-01` — and each of those would otherwise be
100
+ * emitted, and fetched, on the way to `2026`.
101
+ */
102
+ export declare const parseDay: (value: string | null | undefined) => string | null;
103
+ /** "This month", for `"date"` mode: from the 1st, open at the end. */
104
+ export declare const thisMonth: (label: string, id?: string) => CalendarPreset;
105
+ /** "Last month", for `"date"` mode: its first day to its last. */
106
+ export declare const lastMonth: (label: string, id?: string) => CalendarPreset;
107
+ /**
108
+ * The last `months` months, for `"date"` mode: from the same day that many months ago — or the
109
+ * last day that month has — open at the end.
110
+ */
111
+ export declare const lastMonths: (months: number, label: string, id?: string) => CalendarPreset;
112
+ /** "All time", for `"date"` mode: both ends open. */
113
+ export declare const allTime: (label: string, id?: string) => CalendarPreset;
47
114
  /** Whether the pair is in order. An empty end is not a range, so it is not valid either. */
48
115
  export declare const isValidRange: (from: string | null, to: string | null) => boolean;
49
116
  /**
@@ -99,3 +166,4 @@ export declare const createReAnchor: (options: {
99
166
  intervalMs: number;
100
167
  onAnchor: () => void;
101
168
  }) => ReAnchor;
169
+ export {};