@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.
- 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 +265 -93
- 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
|
@@ -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={
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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">
|
|
198
|
+
<span class="text-nowrap">{text.pageSize}:</span>
|
|
175
199
|
<select
|
|
176
200
|
class="select box-content w-[3ch] select-xs"
|
|
177
|
-
aria-label=
|
|
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
|
-
|
|
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>
|
|
@@ -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";
|
package/dist/components/index.js
CHANGED
|
@@ -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`
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
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;
|