@viniaraujo68/plinth 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +361 -2
- package/dist/anchoring.d.ts +61 -0
- package/dist/anchoring.js +151 -0
- package/dist/async-action.svelte.d.ts +37 -0
- package/dist/async-action.svelte.js +64 -0
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/attachments/tooltip.d.ts +37 -0
- package/dist/attachments/tooltip.js +166 -0
- package/dist/components/AsyncButton.svelte +49 -0
- package/dist/components/AsyncButton.svelte.d.ts +32 -0
- package/dist/components/Combobox.svelte +380 -0
- package/dist/components/Combobox.svelte.d.ts +74 -0
- package/dist/components/Copyable.svelte +116 -0
- package/dist/components/Copyable.svelte.d.ts +37 -0
- package/dist/components/DateRangePicker.svelte +266 -0
- package/dist/components/DateRangePicker.svelte.d.ts +66 -0
- package/dist/components/Dialog.svelte +102 -0
- package/dist/components/Dialog.svelte.d.ts +38 -0
- package/dist/components/Dropdown.svelte +204 -0
- package/dist/components/Dropdown.svelte.d.ts +49 -0
- package/dist/components/ErrorDisplay.svelte +59 -0
- package/dist/components/ErrorDisplay.svelte.d.ts +24 -0
- package/dist/components/ErrorPage.svelte +119 -0
- package/dist/components/ErrorPage.svelte.d.ts +52 -0
- package/dist/components/JsonInput.svelte +106 -0
- package/dist/components/JsonInput.svelte.d.ts +40 -0
- package/dist/components/Listbox.svelte +257 -0
- package/dist/components/Listbox.svelte.d.ts +78 -0
- package/dist/components/LoadingButton.svelte +36 -0
- package/dist/components/LoadingButton.svelte.d.ts +22 -0
- package/dist/components/Logo.svelte +112 -0
- package/dist/components/Logo.svelte.d.ts +29 -0
- package/dist/components/Modal.svelte +187 -0
- package/dist/components/Modal.svelte.d.ts +62 -0
- package/dist/components/MultiSelect.svelte +256 -0
- package/dist/components/MultiSelect.svelte.d.ts +89 -0
- package/dist/components/OptionPanel.svelte +241 -0
- package/dist/components/OptionPanel.svelte.d.ts +51 -0
- package/dist/components/Paginator.svelte +205 -0
- package/dist/components/Paginator.svelte.d.ts +48 -0
- package/dist/components/RefreshButton.svelte +246 -0
- package/dist/components/RefreshButton.svelte.d.ts +46 -0
- package/dist/components/SegmentedControl.svelte +147 -0
- package/dist/components/SegmentedControl.svelte.d.ts +71 -0
- package/dist/components/Select.svelte +312 -0
- package/dist/components/Select.svelte.d.ts +68 -0
- package/dist/components/Skeleton.svelte +97 -0
- package/dist/components/Skeleton.svelte.d.ts +32 -0
- package/dist/components/Tooltip.svelte +60 -0
- package/dist/components/Tooltip.svelte.d.ts +58 -0
- package/dist/components/date-range.d.ts +101 -0
- package/dist/components/date-range.js +144 -0
- package/dist/components/index.d.ts +25 -0
- package/dist/components/index.js +37 -0
- package/dist/components/json-text.d.ts +22 -0
- package/dist/components/json-text.js +22 -0
- package/dist/components/pick.d.ts +18 -0
- package/dist/components/pick.js +18 -0
- package/dist/components/segmented.d.ts +7 -0
- package/dist/components/segmented.js +1 -0
- package/dist/components/select.d.ts +66 -0
- package/dist/components/select.js +90 -0
- package/dist/confirm/Confirmer.svelte +194 -0
- package/dist/confirm/Confirmer.svelte.d.ts +45 -0
- package/dist/confirm/challenge.d.ts +16 -0
- package/dist/confirm/challenge.js +16 -0
- package/dist/confirm/confirm-manager.svelte.d.ts +91 -0
- package/dist/confirm/confirm-manager.svelte.js +149 -0
- package/dist/confirm/index.d.ts +3 -0
- package/dist/confirm/index.js +3 -0
- package/dist/error.d.ts +25 -0
- package/dist/error.js +39 -0
- package/dist/formatters.d.ts +95 -0
- package/dist/formatters.js +146 -0
- package/dist/http.d.ts +68 -0
- package/dist/http.js +138 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +14 -0
- package/dist/routing.svelte.d.ts +169 -0
- package/dist/routing.svelte.js +208 -0
- package/dist/shell/AppShell.svelte +874 -0
- package/dist/shell/AppShell.svelte.d.ts +93 -0
- package/dist/shell/Breadcrumbs.svelte +78 -0
- package/dist/shell/Breadcrumbs.svelte.d.ts +25 -0
- package/dist/shell/crumbs.d.ts +23 -0
- package/dist/shell/crumbs.js +1 -0
- package/dist/shell/index.d.ts +3 -0
- package/dist/shell/index.js +6 -0
- package/dist/table/ColumnMenu.svelte +249 -0
- package/dist/table/ColumnMenu.svelte.d.ts +47 -0
- package/dist/table/DataTable.svelte +643 -0
- package/dist/table/DataTable.svelte.d.ts +153 -0
- package/dist/table/columns.d.ts +55 -0
- package/dist/table/columns.js +153 -0
- package/dist/table/index.d.ts +4 -0
- package/dist/table/index.js +8 -0
- package/dist/table/sort.d.ts +48 -0
- package/dist/table/sort.js +139 -0
- package/dist/table/types.d.ts +93 -0
- package/dist/table/types.js +1 -0
- package/dist/theme/ThemeController.svelte +54 -0
- package/dist/theme/ThemeController.svelte.d.ts +27 -0
- package/dist/theme/ThemeToggle.svelte +119 -0
- package/dist/theme/ThemeToggle.svelte.d.ts +52 -0
- package/dist/theme/css-color.d.ts +23 -0
- package/dist/theme/css-color.js +31 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.js +5 -0
- package/dist/theme/theme.css +410 -0
- package/dist/theme/theme.svelte.d.ts +54 -0
- package/dist/theme/theme.svelte.js +58 -0
- package/dist/toast/Toaster.svelte +257 -0
- package/dist/toast/Toaster.svelte.d.ts +35 -0
- package/dist/toast/index.d.ts +2 -0
- package/dist/toast/index.js +2 -0
- package/dist/toast/toast-manager.svelte.d.ts +99 -0
- package/dist/toast/toast-manager.svelte.js +176 -0
- package/dist/user/ScopedComponent.svelte +47 -0
- package/dist/user/ScopedComponent.svelte.d.ts +29 -0
- package/dist/user/anonymous.d.ts +21 -0
- package/dist/user/anonymous.js +26 -0
- package/dist/user/context.d.ts +55 -0
- package/dist/user/context.js +13 -0
- package/dist/user/index.d.ts +3 -0
- package/dist/user/index.js +6 -0
- package/package.json +133 -4
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type Snippet } from "svelte";
|
|
2
|
+
import type { ClassValue } from "svelte/elements";
|
|
3
|
+
import { type ResolvedRoute } from "../routing.svelte.js";
|
|
4
|
+
interface Props {
|
|
5
|
+
/** The routed page. The shell owns the scroll container around it. */
|
|
6
|
+
children: Snippet;
|
|
7
|
+
/**
|
|
8
|
+
* The sidebar header, and the bottom sheet's header on narrow screens. Receives the collapse
|
|
9
|
+
* state so a caller can swap a wordmark for a mark instead of letting it be clipped.
|
|
10
|
+
*/
|
|
11
|
+
brand?: Snippet<[{
|
|
12
|
+
collapsed: boolean;
|
|
13
|
+
}]>;
|
|
14
|
+
/** Extra sidebar footer content, above the user block. A theme picker usually goes here. */
|
|
15
|
+
footer?: Snippet;
|
|
16
|
+
/**
|
|
17
|
+
* Renders one entry's icon. Without it the shell emits `<span class={meta.icon}>`, which is
|
|
18
|
+
* what an icon font or an Iconify/Tailwind class wants; supply it to render SVG components.
|
|
19
|
+
*/
|
|
20
|
+
icon?: Snippet<[ResolvedRoute]>;
|
|
21
|
+
/** Extra classes for the shell root — sizing, mostly. */
|
|
22
|
+
class?: ClassValue;
|
|
23
|
+
/**
|
|
24
|
+
* How many items the bottom bar renders, the trailing "More" item counted among them. Entries
|
|
25
|
+
* that do not fit collapse into the sheet "More" opens — and so does everything the bar has no
|
|
26
|
+
* room for at all: the brand, the footer snippet and the user block. "More" therefore appears
|
|
27
|
+
* with nothing overflowing whenever any of those exist, and costs a nav slot only once the bar
|
|
28
|
+
* is genuinely full: three entries in five slots render three plus "More", five render four
|
|
29
|
+
* plus "More" and put the fifth in the sheet.
|
|
30
|
+
*/
|
|
31
|
+
bottomBarSlots?: number;
|
|
32
|
+
/** Where the collapse state is remembered. `null` turns persistence off. */
|
|
33
|
+
storageKey?: string | null;
|
|
34
|
+
/** Whether the sidebar is collapsed to icons. Bindable, so a page can drive it too. */
|
|
35
|
+
collapsed?: boolean;
|
|
36
|
+
/** Accessible name of both navigations. A prop because the library ships no translations. */
|
|
37
|
+
navLabel?: string;
|
|
38
|
+
/** Accessible name of the collapse button. */
|
|
39
|
+
collapseLabel?: string;
|
|
40
|
+
/** Label of the bottom bar's overflow item, and heading of the sheet it opens. */
|
|
41
|
+
moreLabel?: string;
|
|
42
|
+
/** Accessible name of the sheet's close button. */
|
|
43
|
+
closeLabel?: string;
|
|
44
|
+
/** Label of the sign-out control, which only appears when the user context can sign out. */
|
|
45
|
+
logoutLabel?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The application shell: a collapsible sidebar on wide viewports, a fixed bottom bar on narrow ones,
|
|
49
|
+
* both fed by the same `RoutingContext`. The route tree is declared once, by the filesystem and the
|
|
50
|
+
* routing config; nothing here re-states it.
|
|
51
|
+
*
|
|
52
|
+
* Which form shows is decided by a **container query on the shell root**, not by JavaScript. There is
|
|
53
|
+
* no resize listener and no `matchMedia`, so nothing flashes the wrong layout on the first paint —
|
|
54
|
+
* and because the root is its own container, the shell can be dropped into a narrow box to render its
|
|
55
|
+
* mobile form on a desktop, which is exactly what the showcase does.
|
|
56
|
+
*
|
|
57
|
+
* Entries come from `routing.children("/")`, minus plumbing (no `title`), minus any route with no page
|
|
58
|
+
* of its own to link to, and minus anything the user context gates out. Home, when declared, is derived off the trail and rendered as the sidebar's
|
|
59
|
+
* header entry and as the bottom bar's first item. It is current on the exact root and nowhere else —
|
|
60
|
+
* every route is below it — while every other entry is current for its whole subtree, so exactly one
|
|
61
|
+
* entry is ever marked, in either form.
|
|
62
|
+
*
|
|
63
|
+
* The bar form has no sidebar, and so no brand, no footer and no user block. All three move into the
|
|
64
|
+
* sheet behind the "More" item, which is therefore shown whenever there is anything to put there —
|
|
65
|
+
* overflowing entries, a `brand` snippet, a `footer` snippet or a signed-in user — and not only on an
|
|
66
|
+
* overflow. The sheet reads brand, then the entries that did not fit, then `footer`, then the user
|
|
67
|
+
* block with its sign-out control. Choosing an entry dismisses it; using a footer control does not.
|
|
68
|
+
*
|
|
69
|
+
* Icons: `meta.icon` is an uninterpreted string, so by default it is handed straight to markup as
|
|
70
|
+
* `<span class={meta.icon}>` — the shape an icon font or an Iconify/Tailwind class wants. Pass the
|
|
71
|
+
* `icon` snippet to render something else, an SVG component for instance, and `meta.icon` becomes
|
|
72
|
+
* whatever the app wants it to mean.
|
|
73
|
+
*
|
|
74
|
+
* The shell fills its parent, so give it one with a height (`<div class="h-dvh">`, or `html, body {
|
|
75
|
+
* height: 100% }`). It owns the page's scroll container — the bar is pinned to the bottom of the
|
|
76
|
+
* shell rather than to the window, and the content area reserves its height plus
|
|
77
|
+
* `env(safe-area-inset-bottom)` so nothing ends up underneath it.
|
|
78
|
+
*
|
|
79
|
+
* ```svelte
|
|
80
|
+
* <AppShell>
|
|
81
|
+
* {#snippet brand({ collapsed })}
|
|
82
|
+
* <a href="/">{collapsed ? "A" : "Acme"}</a>
|
|
83
|
+
* {/snippet}
|
|
84
|
+
* {#snippet footer()}
|
|
85
|
+
* <ThemeToggle />
|
|
86
|
+
* {/snippet}
|
|
87
|
+
* {@render page()}
|
|
88
|
+
* </AppShell>
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare const AppShell: import("svelte").Component<Props, {}, "collapsed">;
|
|
92
|
+
type AppShell = ReturnType<typeof AppShell>;
|
|
93
|
+
export default AppShell;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/* Same reason as in `AppShell`: the lint rule can only see a literal `resolve()`, and a runtime
|
|
3
|
+
pathname has no literal route to give it. */
|
|
4
|
+
/* eslint-disable svelte/no-navigation-without-resolve */
|
|
5
|
+
import { getRoutingContext, resolvePathname } from "../routing.svelte.js";
|
|
6
|
+
import type { Crumb } from "./crumbs.js";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/**
|
|
10
|
+
* A trail to render verbatim, root first. Nothing is filtered — the caller already chose what
|
|
11
|
+
* to show — so keep `routeId` unique across entries. Omit it and the trail comes from the
|
|
12
|
+
* routing context, which then has to be set.
|
|
13
|
+
*/
|
|
14
|
+
crumbs?: Crumb[];
|
|
15
|
+
/** Accessible name of the landmark. A prop because the library ships no translations. */
|
|
16
|
+
label?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { crumbs: custom, label = "Breadcrumb" }: Props = $props();
|
|
20
|
+
|
|
21
|
+
// The context getter THROWS when nothing set one, so it is only reached for when the caller did
|
|
22
|
+
// not bring their own trail. That is what lets custom breadcrumbs work in an app with no routing
|
|
23
|
+
// context at all. Whether an instance is custom or context-driven is settled at init, on purpose:
|
|
24
|
+
// swapping a live component between the two modes is not a thing worth supporting.
|
|
25
|
+
// svelte-ignore state_referenced_locally
|
|
26
|
+
const routing = custom === undefined ? getRoutingContext() : null;
|
|
27
|
+
|
|
28
|
+
// Plumbing entries stay in `matched` so a role gate declared halfway up the tree remains
|
|
29
|
+
// reachable; they have no business being a crumb.
|
|
30
|
+
const trail = $derived(custom ?? routing!.matched.filter((r) => r.meta.title !== undefined));
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<!--
|
|
34
|
+
@component
|
|
35
|
+
The page's breadcrumb trail, derived from the routing context: every titled entry on the current
|
|
36
|
+
match, root first, each one a link except the last and except any section with no page behind it.
|
|
37
|
+
|
|
38
|
+
Pass `crumbs` to render a trail of your own instead — same grammar, caller-owned content. An entry
|
|
39
|
+
only needs `routeId` and `label`, so appending a leaf the route tree cannot know about is cheap:
|
|
40
|
+
|
|
41
|
+
```svelte
|
|
42
|
+
<Breadcrumbs crumbs={[...routing.matched, { routeId: "leaf", label: order.reference }]} />
|
|
43
|
+
```
|
|
44
|
+
-->
|
|
45
|
+
|
|
46
|
+
<nav class="breadcrumbs py-1 text-sm" aria-label={label}>
|
|
47
|
+
<ul>
|
|
48
|
+
{#each trail as crumb, index (crumb.routeId)}
|
|
49
|
+
{@const isLast = index === trail.length - 1}
|
|
50
|
+
{@const isLink = !isLast && crumb.navigable === true && crumb.pathname !== undefined}
|
|
51
|
+
<li>
|
|
52
|
+
{#if isLink}
|
|
53
|
+
<a class="crumb gap-1.5 text-base-content/50" href={resolvePathname(crumb.pathname!)}>
|
|
54
|
+
{#if crumb.routeId === "/" && crumb.meta?.icon}
|
|
55
|
+
<span class="size-4 {crumb.meta.icon}" aria-hidden="true"></span>
|
|
56
|
+
{/if}
|
|
57
|
+
{crumb.label}
|
|
58
|
+
</a>
|
|
59
|
+
{:else}
|
|
60
|
+
<!-- The last crumb names the page the user is already on, which is what `aria-current`
|
|
61
|
+
is for; the intermediate non-links are headings and get neither. -->
|
|
62
|
+
<span
|
|
63
|
+
class={[
|
|
64
|
+
"crumb cursor-default gap-1.5 no-underline",
|
|
65
|
+
isLast ? "text-base-content" : "text-base-content/50",
|
|
66
|
+
]}
|
|
67
|
+
aria-current={isLast ? "page" : undefined}
|
|
68
|
+
>
|
|
69
|
+
{#if crumb.routeId === "/" && crumb.meta?.icon}
|
|
70
|
+
<span class="size-4 {crumb.meta.icon}" aria-hidden="true"></span>
|
|
71
|
+
{/if}
|
|
72
|
+
{crumb.label}
|
|
73
|
+
</span>
|
|
74
|
+
{/if}
|
|
75
|
+
</li>
|
|
76
|
+
{/each}
|
|
77
|
+
</ul>
|
|
78
|
+
</nav>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Crumb } from "./crumbs.js";
|
|
2
|
+
interface Props {
|
|
3
|
+
/**
|
|
4
|
+
* A trail to render verbatim, root first. Nothing is filtered — the caller already chose what
|
|
5
|
+
* to show — so keep `routeId` unique across entries. Omit it and the trail comes from the
|
|
6
|
+
* routing context, which then has to be set.
|
|
7
|
+
*/
|
|
8
|
+
crumbs?: Crumb[];
|
|
9
|
+
/** Accessible name of the landmark. A prop because the library ships no translations. */
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The page's breadcrumb trail, derived from the routing context: every titled entry on the current
|
|
14
|
+
* match, root first, each one a link except the last and except any section with no page behind it.
|
|
15
|
+
*
|
|
16
|
+
* Pass `crumbs` to render a trail of your own instead — same grammar, caller-owned content. An entry
|
|
17
|
+
* only needs `routeId` and `label`, so appending a leaf the route tree cannot know about is cheap:
|
|
18
|
+
*
|
|
19
|
+
* ```svelte
|
|
20
|
+
* <Breadcrumbs crumbs={[...routing.matched, { routeId: "leaf", label: order.reference }]} />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
declare const Breadcrumbs: import("svelte").Component<Props, {}, "">;
|
|
24
|
+
type Breadcrumbs = ReturnType<typeof Breadcrumbs>;
|
|
25
|
+
export default Breadcrumbs;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RouteMeta } from "../routing.svelte.js";
|
|
2
|
+
/**
|
|
3
|
+
* One step of a breadcrumb trail.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately a subset of {@link ResolvedRoute} rather than that type itself, so a page can
|
|
6
|
+
* append a leaf the route tree knows nothing about — a record's name it just fetched — with two
|
|
7
|
+
* fields instead of five.
|
|
8
|
+
*
|
|
9
|
+
* It lives in its own module rather than inside `Breadcrumbs.svelte` because a type declared in a
|
|
10
|
+
* component's instance script cannot be re-exported from the package's entry point, and a consumer
|
|
11
|
+
* building a custom trail needs to name this.
|
|
12
|
+
*/
|
|
13
|
+
export interface Crumb {
|
|
14
|
+
/** Keys the entry; `"/"` additionally marks the root, the only crumb that shows an icon. */
|
|
15
|
+
routeId: string;
|
|
16
|
+
/** The visible text. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Where a navigable, non-final crumb links. */
|
|
19
|
+
pathname?: string;
|
|
20
|
+
/** Whether this crumb links at all. A section with no page of its own is a heading. */
|
|
21
|
+
navigable?: boolean;
|
|
22
|
+
meta?: Pick<RouteMeta, "icon">;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The application chrome around a routed page. Everything here reads the routing context rather
|
|
2
|
+
// than a tree of its own, so the navigation an app declares once in `routes.ts` is the navigation
|
|
3
|
+
// that renders — on a sidebar, on a bottom bar and in a breadcrumb trail alike.
|
|
4
|
+
// `@viniaraujo68/plinth/shell`.
|
|
5
|
+
export { default as AppShell } from "./AppShell.svelte";
|
|
6
|
+
export { default as Breadcrumbs } from "./Breadcrumbs.svelte";
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
import Dropdown from "../components/Dropdown.svelte";
|
|
3
|
+
import {
|
|
4
|
+
canHideColumn,
|
|
5
|
+
isColumnHideable,
|
|
6
|
+
isColumnStateEmpty,
|
|
7
|
+
isColumnVisible,
|
|
8
|
+
moveColumn,
|
|
9
|
+
orderColumns,
|
|
10
|
+
setColumnVisible,
|
|
11
|
+
} from "./columns.js";
|
|
12
|
+
import type { Column, ColumnState } from "./types.js";
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
columns: readonly Column<T>[];
|
|
16
|
+
layout: ColumnState | undefined;
|
|
17
|
+
onchange: (layout: ColumnState | undefined) => void;
|
|
18
|
+
/** The trigger's text, and the menu's accessible name. */
|
|
19
|
+
label: string;
|
|
20
|
+
resetLabel: string;
|
|
21
|
+
moveLabel: (column: Column<T>, direction: "up" | "down") => string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let { columns, layout, onchange, label, resetLabel, moveLabel }: Props = $props();
|
|
25
|
+
|
|
26
|
+
const ordered = $derived(orderColumns(columns, layout));
|
|
27
|
+
|
|
28
|
+
let dragging = $state<string | null>(null);
|
|
29
|
+
let target = $state<{ key: string; after: boolean } | null>(null);
|
|
30
|
+
|
|
31
|
+
const move = (key: string, to: number) => onchange(moveColumn(columns, layout, key, to));
|
|
32
|
+
|
|
33
|
+
const toggle = (column: Column<T>, visible: boolean) =>
|
|
34
|
+
onchange(setColumnVisible(layout, column.key, visible));
|
|
35
|
+
|
|
36
|
+
const onDragStart = (event: DragEvent, key: string) => {
|
|
37
|
+
dragging = key;
|
|
38
|
+
// Firefox starts no drag at all without data on the transfer, whatever the data is.
|
|
39
|
+
event.dataTransfer?.setData("text/plain", key);
|
|
40
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const onDragOver = (event: DragEvent, key: string) => {
|
|
44
|
+
if (dragging === null) return;
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
const row = event.currentTarget as HTMLElement;
|
|
47
|
+
const box = row.getBoundingClientRect();
|
|
48
|
+
target = { key, after: event.clientY > box.top + box.height / 2 };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const onDrop = (event: DragEvent) => {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
if (dragging !== null && target !== null && target.key !== dragging) {
|
|
54
|
+
const keys = ordered.map((column) => column.key);
|
|
55
|
+
const from = keys.indexOf(dragging);
|
|
56
|
+
let to = keys.indexOf(target.key) + (target.after ? 1 : 0);
|
|
57
|
+
// The dragged row leaves the list before it is put back, so every slot past it moves up one.
|
|
58
|
+
if (from < to) to -= 1;
|
|
59
|
+
move(dragging, to);
|
|
60
|
+
}
|
|
61
|
+
onDragEnd();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const onDragEnd = () => {
|
|
65
|
+
dragging = null;
|
|
66
|
+
target = null;
|
|
67
|
+
};
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<!--
|
|
71
|
+
@component
|
|
72
|
+
Internal to `DataTable`: the menu that shows, hides and reorders its columns. It is a `Dropdown`,
|
|
73
|
+
so Escape, a click outside and the trigger itself all close it without a listener of this file's
|
|
74
|
+
own.
|
|
75
|
+
|
|
76
|
+
Reordering is offered twice. Dragging a row is the quick way with a mouse, and it is native HTML
|
|
77
|
+
drag and drop rather than pointer bookkeeping, so the browser draws the ghost and scrolls the list.
|
|
78
|
+
The two arrow buttons on every row do the same one step at a time, and they are the only way to
|
|
79
|
+
reorder from a keyboard or a screen reader — drag and drop has no keyboard story of its own.
|
|
80
|
+
|
|
81
|
+
A column that is not hideable is listed without a checkbox, so the reader still sees it and can
|
|
82
|
+
still move it; the last visible column's checkbox is disabled rather than hidden, for the same
|
|
83
|
+
reason.
|
|
84
|
+
-->
|
|
85
|
+
|
|
86
|
+
<Dropdown
|
|
87
|
+
class="plinth-columns-trigger btn btn-ghost btn-sm"
|
|
88
|
+
panelClass="plinth-columns-panel rounded-box border border-base-content/10 bg-base-100 p-2 shadow-lg"
|
|
89
|
+
aria-label={label}
|
|
90
|
+
>
|
|
91
|
+
{label}
|
|
92
|
+
<span class="plinth-columns-caret" aria-hidden="true">▾</span>
|
|
93
|
+
{#snippet content()}
|
|
94
|
+
<ul class="plinth-columns-list" aria-label={label}>
|
|
95
|
+
{#each ordered as column, index (column.key)}
|
|
96
|
+
{@const visible = isColumnVisible(column, layout)}
|
|
97
|
+
<li
|
|
98
|
+
class="plinth-columns-row"
|
|
99
|
+
class:plinth-dragging={dragging === column.key}
|
|
100
|
+
class:plinth-drop-before={target?.key === column.key && !target.after}
|
|
101
|
+
class:plinth-drop-after={target?.key === column.key && target.after}
|
|
102
|
+
draggable="true"
|
|
103
|
+
ondragstart={(event) => onDragStart(event, column.key)}
|
|
104
|
+
ondragover={(event) => onDragOver(event, column.key)}
|
|
105
|
+
ondrop={onDrop}
|
|
106
|
+
ondragend={onDragEnd}
|
|
107
|
+
>
|
|
108
|
+
<span class="plinth-columns-grip" aria-hidden="true">⠿</span>
|
|
109
|
+
{#if isColumnHideable(column)}
|
|
110
|
+
<label class="plinth-columns-label">
|
|
111
|
+
<input
|
|
112
|
+
type="checkbox"
|
|
113
|
+
class="checkbox checkbox-xs"
|
|
114
|
+
checked={visible}
|
|
115
|
+
disabled={visible && !canHideColumn(columns, layout, column.key)}
|
|
116
|
+
onchange={(event) => toggle(column, event.currentTarget.checked)}
|
|
117
|
+
/>
|
|
118
|
+
{column.label}
|
|
119
|
+
</label>
|
|
120
|
+
{:else}
|
|
121
|
+
<span class="plinth-columns-label plinth-columns-locked">{column.label}</span>
|
|
122
|
+
{/if}
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
class="plinth-columns-move"
|
|
126
|
+
aria-label={moveLabel(column, "up")}
|
|
127
|
+
disabled={index === 0}
|
|
128
|
+
onclick={() => move(column.key, index - 1)}
|
|
129
|
+
>
|
|
130
|
+
↑
|
|
131
|
+
</button>
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
class="plinth-columns-move"
|
|
135
|
+
aria-label={moveLabel(column, "down")}
|
|
136
|
+
disabled={index === ordered.length - 1}
|
|
137
|
+
onclick={() => move(column.key, index + 1)}
|
|
138
|
+
>
|
|
139
|
+
↓
|
|
140
|
+
</button>
|
|
141
|
+
</li>
|
|
142
|
+
{/each}
|
|
143
|
+
</ul>
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
class="plinth-columns-reset btn btn-ghost btn-xs"
|
|
147
|
+
disabled={isColumnStateEmpty(layout)}
|
|
148
|
+
onclick={() => onchange(undefined)}
|
|
149
|
+
>
|
|
150
|
+
{resetLabel}
|
|
151
|
+
</button>
|
|
152
|
+
{/snippet}
|
|
153
|
+
</Dropdown>
|
|
154
|
+
|
|
155
|
+
<style>
|
|
156
|
+
.plinth-columns-caret {
|
|
157
|
+
font-size: 0.7em;
|
|
158
|
+
opacity: 0.6;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
:global(.plinth-columns-panel) {
|
|
162
|
+
display: flex;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
gap: 0.25rem;
|
|
165
|
+
min-width: 14rem;
|
|
166
|
+
max-height: 22rem;
|
|
167
|
+
margin: 0.25rem 0 0;
|
|
168
|
+
overflow-y: auto;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.plinth-columns-list {
|
|
172
|
+
display: flex;
|
|
173
|
+
flex-direction: column;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.plinth-columns-row {
|
|
177
|
+
display: grid;
|
|
178
|
+
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
|
179
|
+
align-items: center;
|
|
180
|
+
gap: 0.375rem;
|
|
181
|
+
padding: 0.25rem 0.375rem;
|
|
182
|
+
border-radius: var(--radius-field, 0.5rem);
|
|
183
|
+
/* Borders on both edges from the start, so showing the drop line does not shift the rows. */
|
|
184
|
+
border-top: 2px solid transparent;
|
|
185
|
+
border-bottom: 2px solid transparent;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.plinth-columns-row:hover {
|
|
189
|
+
background: color-mix(in oklch, var(--color-base-content) 5%, transparent);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.plinth-dragging {
|
|
193
|
+
opacity: 0.4;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.plinth-drop-before {
|
|
197
|
+
border-top-color: var(--color-primary);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.plinth-drop-after {
|
|
201
|
+
border-bottom-color: var(--color-primary);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.plinth-columns-grip {
|
|
205
|
+
cursor: grab;
|
|
206
|
+
color: color-mix(in oklch, var(--color-base-content) 40%, transparent);
|
|
207
|
+
user-select: none;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.plinth-columns-label {
|
|
211
|
+
display: flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
gap: 0.5rem;
|
|
214
|
+
min-width: 0;
|
|
215
|
+
font-size: 0.875rem;
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
overflow-wrap: anywhere;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.plinth-columns-locked {
|
|
221
|
+
padding-left: calc(1rem + 0.5rem);
|
|
222
|
+
cursor: default;
|
|
223
|
+
color: color-mix(in oklch, var(--color-base-content) 65%, transparent);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.plinth-columns-move {
|
|
227
|
+
display: grid;
|
|
228
|
+
place-items: center;
|
|
229
|
+
width: 1.5rem;
|
|
230
|
+
height: 1.5rem;
|
|
231
|
+
border-radius: var(--radius-field, 0.5rem);
|
|
232
|
+
color: color-mix(in oklch, var(--color-base-content) 60%, transparent);
|
|
233
|
+
cursor: pointer;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.plinth-columns-move:hover:not(:disabled) {
|
|
237
|
+
background: color-mix(in oklch, var(--color-base-content) 10%, transparent);
|
|
238
|
+
color: var(--color-base-content);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.plinth-columns-move:disabled {
|
|
242
|
+
opacity: 0.3;
|
|
243
|
+
cursor: default;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.plinth-columns-reset {
|
|
247
|
+
align-self: flex-end;
|
|
248
|
+
}
|
|
249
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Column, ColumnState } from "./types.js";
|
|
2
|
+
declare function $$render<T>(): {
|
|
3
|
+
props: {
|
|
4
|
+
columns: readonly Column<T>[];
|
|
5
|
+
layout: ColumnState | undefined;
|
|
6
|
+
onchange: (layout: ColumnState | undefined) => void;
|
|
7
|
+
/** The trigger's text, and the menu's accessible name. */
|
|
8
|
+
label: string;
|
|
9
|
+
resetLabel: string;
|
|
10
|
+
moveLabel: (column: Column<T>, direction: "up" | "down") => string;
|
|
11
|
+
};
|
|
12
|
+
exports: {};
|
|
13
|
+
bindings: "";
|
|
14
|
+
slots: {};
|
|
15
|
+
events: {};
|
|
16
|
+
};
|
|
17
|
+
declare class __sveltets_Render<T> {
|
|
18
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
19
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
20
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
21
|
+
bindings(): "";
|
|
22
|
+
exports(): {};
|
|
23
|
+
}
|
|
24
|
+
interface $$IsomorphicComponent {
|
|
25
|
+
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
26
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
27
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
28
|
+
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
29
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Internal to `DataTable`: the menu that shows, hides and reorders its columns. It is a `Dropdown`,
|
|
33
|
+
* so Escape, a click outside and the trigger itself all close it without a listener of this file's
|
|
34
|
+
* own.
|
|
35
|
+
*
|
|
36
|
+
* Reordering is offered twice. Dragging a row is the quick way with a mouse, and it is native HTML
|
|
37
|
+
* drag and drop rather than pointer bookkeeping, so the browser draws the ghost and scrolls the list.
|
|
38
|
+
* The two arrow buttons on every row do the same one step at a time, and they are the only way to
|
|
39
|
+
* reorder from a keyboard or a screen reader — drag and drop has no keyboard story of its own.
|
|
40
|
+
*
|
|
41
|
+
* A column that is not hideable is listed without a checkbox, so the reader still sees it and can
|
|
42
|
+
* still move it; the last visible column's checkbox is disabled rather than hidden, for the same
|
|
43
|
+
* reason.
|
|
44
|
+
*/
|
|
45
|
+
declare const ColumnMenu: $$IsomorphicComponent;
|
|
46
|
+
type ColumnMenu<T> = InstanceType<typeof ColumnMenu<T>>;
|
|
47
|
+
export default ColumnMenu;
|