@viniaraujo68/plinth 0.3.1 → 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.
Files changed (103) hide show
  1. package/README.md +189 -19
  2. package/dist/components/ErrorDisplay.svelte +30 -16
  3. package/dist/components/ErrorDisplay.svelte.d.ts +4 -0
  4. package/dist/components/JsonView.svelte +43 -0
  5. package/dist/components/JsonView.svelte.d.ts +4 -0
  6. package/dist/components/JsonViewFrame.svelte +654 -0
  7. package/dist/components/JsonViewFrame.svelte.d.ts +31 -0
  8. package/dist/components/Paginator.svelte +34 -8
  9. package/dist/components/Paginator.svelte.d.ts +10 -0
  10. package/dist/components/RequestErrorDisplay.svelte +43 -0
  11. package/dist/components/RequestErrorDisplay.svelte.d.ts +11 -0
  12. package/dist/components/Tabs.svelte +204 -0
  13. package/dist/components/Tabs.svelte.d.ts +76 -0
  14. package/dist/components/VirtualJsonView.svelte +67 -0
  15. package/dist/components/VirtualJsonView.svelte.d.ts +4 -0
  16. package/dist/components/index.d.ts +8 -1
  17. package/dist/components/index.js +11 -5
  18. package/dist/components/json-view.d.ts +70 -0
  19. package/dist/components/json-view.js +302 -0
  20. package/dist/components/request-error.d.ts +8 -0
  21. package/dist/components/request-error.js +55 -0
  22. package/dist/components/tabs.d.ts +5 -0
  23. package/dist/components/tabs.js +1 -0
  24. package/dist/http.d.ts +10 -5
  25. package/dist/http.js +34 -6
  26. package/dist/i18n/LocaleToggle.svelte +45 -0
  27. package/dist/i18n/LocaleToggle.svelte.d.ts +48 -0
  28. package/dist/i18n/i18n.svelte.d.ts +56 -0
  29. package/dist/i18n/i18n.svelte.js +94 -0
  30. package/dist/i18n/index.d.ts +3 -0
  31. package/dist/i18n/index.js +7 -0
  32. package/dist/i18n/translate.d.ts +30 -0
  33. package/dist/i18n/translate.js +38 -0
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/redirect.d.ts +40 -0
  37. package/dist/redirect.js +59 -0
  38. package/dist/routing.svelte.d.ts +1 -0
  39. package/dist/routing.svelte.js +3 -0
  40. package/dist/shell/AppShell.svelte +265 -93
  41. package/dist/shell/AppShell.svelte.d.ts +3 -2
  42. package/dist/shell/NavigationProgress.svelte +85 -0
  43. package/dist/shell/NavigationProgress.svelte.d.ts +31 -0
  44. package/dist/shell/TopBar.svelte +191 -0
  45. package/dist/shell/TopBar.svelte.d.ts +41 -0
  46. package/dist/shell/UserAvatar.svelte +50 -0
  47. package/dist/shell/UserAvatar.svelte.d.ts +14 -0
  48. package/dist/shell/index.d.ts +2 -0
  49. package/dist/shell/index.js +7 -3
  50. package/dist/shell/initials.d.ts +6 -0
  51. package/dist/shell/initials.js +13 -0
  52. package/dist/table/DataTable.svelte +199 -9
  53. package/dist/table/DataTable.svelte.d.ts +28 -5
  54. package/dist/table/FilterBar.svelte +417 -0
  55. package/dist/table/FilterBar.svelte.d.ts +34 -0
  56. package/dist/table/FilterRuleEditor.svelte +290 -0
  57. package/dist/table/FilterRuleEditor.svelte.d.ts +33 -0
  58. package/dist/table/filter-labels.d.ts +23 -0
  59. package/dist/table/filter-labels.js +30 -0
  60. package/dist/table/filter.d.ts +27 -0
  61. package/dist/table/filter.js +279 -0
  62. package/dist/table/index.d.ts +4 -1
  63. package/dist/table/index.js +5 -2
  64. package/dist/table/types.d.ts +49 -0
  65. package/dist/toast/Toaster.svelte +7 -3
  66. package/dist/toast/index.d.ts +1 -1
  67. package/dist/toast/toast-manager.svelte.d.ts +29 -1
  68. package/dist/toast/toast-manager.svelte.js +61 -1
  69. package/dist/url-state/codec.d.ts +15 -0
  70. package/dist/url-state/codec.js +1 -0
  71. package/dist/url-state/index.d.ts +5 -0
  72. package/dist/url-state/index.js +4 -0
  73. package/dist/url-state/params-codec.d.ts +73 -0
  74. package/dist/url-state/params-codec.js +116 -0
  75. package/dist/url-state/search-params.d.ts +4 -0
  76. package/dist/url-state/search-params.js +28 -0
  77. package/dist/url-state/synced-state.svelte.d.ts +20 -0
  78. package/dist/url-state/synced-state.svelte.js +88 -0
  79. package/dist/url-state/url-store.svelte.d.ts +12 -0
  80. package/dist/url-state/url-store.svelte.js +56 -0
  81. package/dist/virtual/DataList.svelte +215 -0
  82. package/dist/virtual/DataList.svelte.d.ts +52 -0
  83. package/dist/virtual/VirtualList.svelte +150 -0
  84. package/dist/virtual/VirtualList.svelte.d.ts +42 -0
  85. package/dist/virtual/index.d.ts +7 -0
  86. package/dist/virtual/index.js +6 -0
  87. package/dist/virtual/keyed-cache.d.ts +7 -0
  88. package/dist/virtual/keyed-cache.js +20 -0
  89. package/dist/virtual/labels.d.ts +12 -0
  90. package/dist/virtual/labels.js +6 -0
  91. package/dist/virtual/paging.d.ts +5 -0
  92. package/dist/virtual/paging.js +11 -0
  93. package/dist/virtual/range.d.ts +9 -0
  94. package/dist/virtual/range.js +31 -0
  95. package/dist/virtual/size-tree.d.ts +10 -0
  96. package/dist/virtual/size-tree.js +56 -0
  97. package/dist/virtual/virtualizer.svelte.d.ts +38 -0
  98. package/dist/virtual/virtualizer.svelte.js +295 -0
  99. package/dist/virtual/windowed-data.svelte.d.ts +38 -0
  100. package/dist/virtual/windowed-data.svelte.js +139 -0
  101. package/dist/virtual/windowed-virtualizer.svelte.d.ts +9 -0
  102. package/dist/virtual/windowed-virtualizer.svelte.js +22 -0
  103. 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;
@@ -8,19 +8,26 @@ export { default as Dropdown } from "./Dropdown.svelte";
8
8
  export { default as ErrorDisplay } from "./ErrorDisplay.svelte";
9
9
  export { default as ErrorPage } from "./ErrorPage.svelte";
10
10
  export { default as JsonInput } from "./JsonInput.svelte";
11
+ export { default as JsonView } from "./JsonView.svelte";
12
+ export { default as VirtualJsonView } from "./VirtualJsonView.svelte";
11
13
  export { default as Listbox } from "./Listbox.svelte";
12
14
  export { default as LoadingButton } from "./LoadingButton.svelte";
13
15
  export { default as Logo } from "./Logo.svelte";
14
16
  export { default as Modal } from "./Modal.svelte";
15
17
  export { default as MultiSelect } from "./MultiSelect.svelte";
16
- export { default as Paginator } from "./Paginator.svelte";
18
+ export { default as Paginator, DEFAULT_PAGINATOR_LABELS, type PaginatorLabels, } from "./Paginator.svelte";
17
19
  export { default as RefreshButton } from "./RefreshButton.svelte";
20
+ export { default as RequestErrorDisplay } from "./RequestErrorDisplay.svelte";
18
21
  export { default as SegmentedControl } from "./SegmentedControl.svelte";
19
22
  export { default as Select } from "./Select.svelte";
20
23
  export { default as Skeleton } from "./Skeleton.svelte";
24
+ export { default as Tabs } from "./Tabs.svelte";
21
25
  export { default as Tooltip } from "./Tooltip.svelte";
22
26
  export { PICK_CLIP_PATH, PICK_PATH } from "./pick.js";
23
27
  export { buildRange, CUSTOM_PRESET_ID, DEFAULT_PRESETS, isoToLocalInput, localInputToIso, type DateRange, type DateRangePreset, } from "./date-range.js";
24
28
  export { matchesSelectQuery, normalizeForSearch, type SelectOption } from "./select.js";
25
29
  export { formatJsonText, parseJsonText, type JsonTextOptions, type JsonTextResult, } from "./json-text.js";
30
+ export { DEFAULT_JSON_VIEW_LABELS, type JsonViewLabels } from "./json-view.js";
31
+ export { DEFAULT_REQUEST_ERROR_COPY, type RequestErrorCopy, type RequestErrorLabels, } from "./request-error.js";
26
32
  export type { SegmentedOption } from "./segmented.js";
33
+ export type { TabItem } from "./tabs.js";
@@ -8,21 +8,25 @@ export { default as Dropdown } from "./Dropdown.svelte";
8
8
  export { default as ErrorDisplay } from "./ErrorDisplay.svelte";
9
9
  export { default as ErrorPage } from "./ErrorPage.svelte";
10
10
  export { default as JsonInput } from "./JsonInput.svelte";
11
+ export { default as JsonView } from "./JsonView.svelte";
12
+ export { default as VirtualJsonView } from "./VirtualJsonView.svelte";
11
13
  export { default as Listbox } from "./Listbox.svelte";
12
14
  export { default as LoadingButton } from "./LoadingButton.svelte";
13
15
  export { default as Logo } from "./Logo.svelte";
14
16
  export { default as Modal } from "./Modal.svelte";
15
17
  export { default as MultiSelect } from "./MultiSelect.svelte";
16
- export { default as Paginator } from "./Paginator.svelte";
18
+ export { default as Paginator, DEFAULT_PAGINATOR_LABELS, } from "./Paginator.svelte";
17
19
  export { default as RefreshButton } from "./RefreshButton.svelte";
20
+ export { default as RequestErrorDisplay } from "./RequestErrorDisplay.svelte";
18
21
  export { default as SegmentedControl } from "./SegmentedControl.svelte";
19
22
  export { default as Select } from "./Select.svelte";
20
23
  export { default as Skeleton } from "./Skeleton.svelte";
24
+ export { default as Tabs } from "./Tabs.svelte";
21
25
  export { default as Tooltip } from "./Tooltip.svelte";
22
- // The mark's outline as data. `Logo` and the shell's avatar already read it from here, and an app
23
- // that wants the same silhouette -- a placeholder cut to the pick, an avatar of its own -- has no
24
- // other way to get it than copying the curve out of the library, which is how two versions of one
25
- // shape start existing.
26
+ // The mark's outline as data. `Logo` already reads it from here, and an app that wants the same
27
+ // silhouette -- a placeholder cut to the pick, a mark of its own -- has no other way to get it than
28
+ // copying the curve out of the library, which is how two versions of one shape start existing.
29
+ // Not for avatars: a profile picture cut to anything but a disc reads as a badge.
26
30
  export { PICK_CLIP_PATH, PICK_PATH } from "./pick.js";
27
31
  // The half of `date-range.js` a consumer of the picker has a use for: the two prop types, the
28
32
  // preset list and the reserved id they are written against, and the three conversions anyone
@@ -36,3 +40,5 @@ export { buildRange, CUSTOM_PRESET_ID, DEFAULT_PRESETS, isoToLocalInput, localIn
36
40
  // highlight walk stays private: it is how the two components drive themselves.
37
41
  export { matchesSelectQuery, normalizeForSearch } from "./select.js";
38
42
  export { formatJsonText, parseJsonText, } from "./json-text.js";
43
+ export { DEFAULT_JSON_VIEW_LABELS } from "./json-view.js";
44
+ export { DEFAULT_REQUEST_ERROR_COPY, } from "./request-error.js";
@@ -0,0 +1,70 @@
1
+ export type JsonLineKind = "open" | "close" | "value";
2
+ export type JsonTokenType = "string" | "number" | "boolean" | "null" | "circular";
3
+ export interface JsonLine {
4
+ kind: JsonLineKind;
5
+ pointer: string;
6
+ id: string;
7
+ depth: number;
8
+ parent: number;
9
+ end: number;
10
+ name: string | null;
11
+ text: string;
12
+ type: JsonTokenType | null;
13
+ size: number;
14
+ comma: boolean;
15
+ source: unknown;
16
+ }
17
+ export interface JsonFlattenOptions {
18
+ circular?: string;
19
+ }
20
+ export interface JsonMatch {
21
+ line: number;
22
+ field: "name" | "value";
23
+ start: number;
24
+ length: number;
25
+ }
26
+ export interface JsonMatchSegment {
27
+ text: string;
28
+ start: number;
29
+ match: boolean;
30
+ }
31
+ export interface JsonPoint {
32
+ line: number;
33
+ column: number;
34
+ }
35
+ export interface JsonViewLabels {
36
+ region: string;
37
+ search: string;
38
+ searchField: string;
39
+ previousMatch: string;
40
+ nextMatch: string;
41
+ closeSearch: string;
42
+ matchCount: (current: number, total: number) => string;
43
+ noMatches: string;
44
+ expandAll: string;
45
+ collapseAll: string;
46
+ copy: string;
47
+ copied: string;
48
+ expand: string;
49
+ collapse: string;
50
+ items: (count: number) => string;
51
+ circular: string;
52
+ empty: string;
53
+ }
54
+ export declare const DEFAULT_JSON_VIEW_LABELS: JsonViewLabels;
55
+ export declare const JSON_VIEW_GUTTER_CH = 2;
56
+ export declare const JSON_VIEW_INDENT_CH = 2;
57
+ export declare const flattenJson: (value: unknown, options?: JsonFlattenOptions) => JsonLine[];
58
+ export declare const isFoldable: (line: JsonLine) => boolean;
59
+ export declare const isFolded: (line: JsonLine, collapsed: ReadonlySet<string>) => boolean;
60
+ export declare const visibleLines: (lines: readonly JsonLine[], collapsed: ReadonlySet<string>) => number[];
61
+ export declare const ancestorLines: (lines: readonly JsonLine[], index: number) => number[];
62
+ export declare const searchJson: (lines: readonly JsonLine[], query: string) => JsonMatch[];
63
+ export declare const matchSegments: (text: string, query: string) => JsonMatchSegment[];
64
+ export declare const subtreeHasMatch: (matches: readonly JsonMatch[], open: number, end: number) => boolean;
65
+ export declare const lineWidthCh: (line: JsonLine) => number;
66
+ export declare const maxLineWidthCh: (lines: readonly JsonLine[]) => number;
67
+ export declare const matchColumnCh: (line: JsonLine, match: JsonMatch) => number;
68
+ export declare const lineText: (line: JsonLine) => string;
69
+ export declare const copyJson: (lines: readonly JsonLine[], collapsed: ReadonlySet<string>, from: JsonPoint, to: JsonPoint) => string;
70
+ export declare const copyAllJson: (lines: readonly JsonLine[]) => string;