@connve/ui 0.0.0-stage → 0.1.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 +373 -0
- package/README.md +92 -2
- package/dist/AppShell.svelte +258 -0
- package/dist/AppShell.svelte.d.ts +29 -0
- package/dist/Badge.svelte +33 -0
- package/dist/Badge.svelte.d.ts +9 -0
- package/dist/ContextBridge.svelte +18 -0
- package/dist/ContextBridge.svelte.d.ts +8 -0
- package/dist/CopyButton.svelte +57 -0
- package/dist/CopyButton.svelte.d.ts +11 -0
- package/dist/EditButton.svelte +37 -0
- package/dist/EditButton.svelte.d.ts +9 -0
- package/dist/FolderTree.svelte +116 -0
- package/dist/FolderTree.svelte.d.ts +42 -0
- package/dist/IconButton.svelte +33 -0
- package/dist/IconButton.svelte.d.ts +16 -0
- package/dist/Inspector.svelte +115 -0
- package/dist/Inspector.svelte.d.ts +20 -0
- package/dist/InspectorSection.svelte +18 -0
- package/dist/InspectorSection.svelte.d.ts +9 -0
- package/dist/ListHeader.svelte +73 -0
- package/dist/ListHeader.svelte.d.ts +18 -0
- package/dist/Loading.svelte +25 -0
- package/dist/Loading.svelte.d.ts +9 -0
- package/dist/Logo.svelte +93 -0
- package/dist/Logo.svelte.d.ts +9 -0
- package/dist/NavList.svelte +74 -0
- package/dist/NavList.svelte.d.ts +24 -0
- package/dist/PaneResizer.svelte +36 -0
- package/dist/PaneResizer.svelte.d.ts +7 -0
- package/dist/Segmented.svelte +38 -0
- package/dist/Segmented.svelte.d.ts +33 -0
- package/dist/SidePane.svelte +148 -0
- package/dist/SidePane.svelte.d.ts +17 -0
- package/dist/StateMessage.svelte +110 -0
- package/dist/StateMessage.svelte.d.ts +15 -0
- package/dist/StatusDot.svelte +32 -0
- package/dist/StatusDot.svelte.d.ts +10 -0
- package/dist/ThemeToggle.svelte +37 -0
- package/dist/ThemeToggle.svelte.d.ts +3 -0
- package/dist/focus.d.ts +2 -0
- package/dist/focus.js +39 -0
- package/dist/icons.d.ts +19 -0
- package/dist/icons.js +90 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +23 -0
- package/dist/labels.svelte.d.ts +28 -0
- package/dist/labels.svelte.js +27 -0
- package/dist/mobile.d.ts +5 -0
- package/dist/mobile.js +7 -0
- package/dist/paneSize.svelte.d.ts +26 -0
- package/dist/paneSize.svelte.js +112 -0
- package/dist/shell.svelte.d.ts +18 -0
- package/dist/shell.svelte.js +20 -0
- package/dist/theme.css +169 -0
- package/dist/theme.svelte.d.ts +13 -0
- package/dist/theme.svelte.js +46 -0
- package/dist/tooltip.d.ts +3 -0
- package/dist/tooltip.js +7 -0
- package/dist/tree.d.ts +17 -0
- package/dist/tree.js +44 -0
- package/package.json +82 -5
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type IconifyIcon } from '@iconify/svelte';
|
|
2
|
+
import { type TooltipSide } from './tooltip.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** An Iconify name, or icon data, which needs no request. */
|
|
5
|
+
icon: string | IconifyIcon;
|
|
6
|
+
/** Names the button, and shows as its tooltip. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** A link when set, otherwise a button. */
|
|
9
|
+
href?: string;
|
|
10
|
+
onclick?: (event: MouseEvent) => void;
|
|
11
|
+
tooltip?: TooltipSide;
|
|
12
|
+
ref?: HTMLAnchorElement | HTMLButtonElement | null;
|
|
13
|
+
}
|
|
14
|
+
declare const IconButton: import("svelte").Component<Props, {}, "ref">;
|
|
15
|
+
type IconButton = ReturnType<typeof IconButton>;
|
|
16
|
+
export default IconButton;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, untrack, type Snippet } from 'svelte';
|
|
3
|
+
import { cubicOut } from 'svelte/easing';
|
|
4
|
+
import { prefersReducedMotion } from 'svelte/motion';
|
|
5
|
+
import { MediaQuery } from 'svelte/reactivity';
|
|
6
|
+
import { slide } from 'svelte/transition';
|
|
7
|
+
import IconButton from './IconButton.svelte';
|
|
8
|
+
import * as icons from './icons.js';
|
|
9
|
+
import PaneResizer from './PaneResizer.svelte';
|
|
10
|
+
import { keepFocusWithin } from './focus.js';
|
|
11
|
+
import { labels } from './labels.svelte.js';
|
|
12
|
+
import { MOBILE_QUERY } from './mobile.js';
|
|
13
|
+
import { inspectorWidth as pane } from './paneSize.svelte.js';
|
|
14
|
+
import { getShell } from './shell.svelte.js';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
/** Names the inspected item, e.g. a tool or a log's task. */
|
|
18
|
+
title: string;
|
|
19
|
+
/** Names the panel for assistive tech. */
|
|
20
|
+
label: string;
|
|
21
|
+
onclose: () => void;
|
|
22
|
+
/** Steps back to the list this item was opened from. */
|
|
23
|
+
onback?: () => void;
|
|
24
|
+
/** Names that list on the back button. */
|
|
25
|
+
backLabel?: string;
|
|
26
|
+
/** A line under the title, e.g. the item's status or path. */
|
|
27
|
+
subtitle?: Snippet;
|
|
28
|
+
/** Buttons before the close button. */
|
|
29
|
+
actions?: Snippet;
|
|
30
|
+
children: Snippet;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let { title, label, onclose, onback, backLabel, subtitle, actions, children }: Props = $props();
|
|
34
|
+
|
|
35
|
+
const shell = getShell('Inspector');
|
|
36
|
+
|
|
37
|
+
let width = $derived(`min(${pane.size}px, 50vw)`);
|
|
38
|
+
|
|
39
|
+
/** On a phone the inspector covers the page, so it acts as a dialog; it follows the window as it resizes or turns. */
|
|
40
|
+
const phone = new MediaQuery(MOBILE_QUERY);
|
|
41
|
+
let covers = $derived(phone.current);
|
|
42
|
+
let panel = $state<HTMLElement | null>(null);
|
|
43
|
+
|
|
44
|
+
onMount(() => {
|
|
45
|
+
shell.inspectors += 1;
|
|
46
|
+
return () => (shell.inspectors -= 1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Covering the page, it takes focus as it opens: the panel rather than a button, so the Enter
|
|
50
|
+
// that opened it can't also press that button. Closing gives focus back to where it came from,
|
|
51
|
+
// unless it has moved on outside the panel.
|
|
52
|
+
$effect(() => {
|
|
53
|
+
if (!panel) return;
|
|
54
|
+
const el = panel;
|
|
55
|
+
const opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
56
|
+
if (untrack(() => covers)) el.focus();
|
|
57
|
+
return () => {
|
|
58
|
+
const active = document.activeElement;
|
|
59
|
+
if (!active || active === document.body || el.contains(active)) opener?.focus();
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function onkeydown(event: KeyboardEvent) {
|
|
64
|
+
if (!covers || !panel) return;
|
|
65
|
+
if (event.key === 'Escape' && !event.defaultPrevented) onclose();
|
|
66
|
+
else keepFocusWithin(event, panel);
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<aside
|
|
71
|
+
bind:this={panel}
|
|
72
|
+
tabindex="-1"
|
|
73
|
+
class="relative flex shrink-0 flex-col border-l border-base-200 bg-base-100 outline-none max-md:fixed max-md:inset-y-0 max-md:right-0 max-md:z-40 max-md:w-full! max-md:shadow-xl"
|
|
74
|
+
style:width
|
|
75
|
+
role={covers ? 'dialog' : undefined}
|
|
76
|
+
aria-modal={covers ? 'true' : undefined}
|
|
77
|
+
aria-label={label}
|
|
78
|
+
{onkeydown}
|
|
79
|
+
transition:slide={{
|
|
80
|
+
axis: 'x',
|
|
81
|
+
duration: prefersReducedMotion.current ? 0 : 200,
|
|
82
|
+
easing: cubicOut
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{#if !covers}
|
|
86
|
+
<PaneResizer {pane} />
|
|
87
|
+
{/if}
|
|
88
|
+
<div class="flex min-h-0 flex-1 flex-col max-md:w-full!" style:width>
|
|
89
|
+
<div class={['flex h-16 shrink-0 items-center gap-1 pr-3', onback ? 'pl-3' : 'pl-6']}>
|
|
90
|
+
{#if onback}
|
|
91
|
+
<IconButton
|
|
92
|
+
icon={icons.chevronLeft}
|
|
93
|
+
label={backLabel ?? labels.back}
|
|
94
|
+
tooltip="right"
|
|
95
|
+
onclick={onback}
|
|
96
|
+
/>
|
|
97
|
+
{/if}
|
|
98
|
+
<div class="min-w-0 flex-1">
|
|
99
|
+
<h2 class="truncate text-[0.9375rem] font-semibold">{title}</h2>
|
|
100
|
+
{#if subtitle}
|
|
101
|
+
<div
|
|
102
|
+
class="mt-0.5 flex min-w-0 items-center gap-1.5 truncate text-xs text-base-content/60"
|
|
103
|
+
>
|
|
104
|
+
{@render subtitle()}
|
|
105
|
+
</div>
|
|
106
|
+
{/if}
|
|
107
|
+
</div>
|
|
108
|
+
{@render actions?.()}
|
|
109
|
+
<IconButton icon={icons.x} label={labels.close} tooltip="left" onclick={onclose} />
|
|
110
|
+
</div>
|
|
111
|
+
<div class="min-h-0 flex-1 space-y-6 overflow-y-auto px-6 pb-6 text-sm">
|
|
112
|
+
{@render children()}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</aside>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Names the inspected item, e.g. a tool or a log's task. */
|
|
4
|
+
title: string;
|
|
5
|
+
/** Names the panel for assistive tech. */
|
|
6
|
+
label: string;
|
|
7
|
+
onclose: () => void;
|
|
8
|
+
/** Steps back to the list this item was opened from. */
|
|
9
|
+
onback?: () => void;
|
|
10
|
+
/** Names that list on the back button. */
|
|
11
|
+
backLabel?: string;
|
|
12
|
+
/** A line under the title, e.g. the item's status or path. */
|
|
13
|
+
subtitle?: Snippet;
|
|
14
|
+
/** Buttons before the close button. */
|
|
15
|
+
actions?: Snippet;
|
|
16
|
+
children: Snippet;
|
|
17
|
+
}
|
|
18
|
+
declare const Inspector: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type Inspector = ReturnType<typeof Inspector>;
|
|
20
|
+
export default Inspector;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
interface Props extends HTMLAttributes<HTMLElement> {
|
|
6
|
+
title?: string;
|
|
7
|
+
children: Snippet;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { title, class: className, children, ...rest }: Props = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<section {...rest} class={['space-y-2', className]}>
|
|
14
|
+
{#if title}
|
|
15
|
+
<h3 class="text-xs font-medium text-base-content/60">{title}</h3>
|
|
16
|
+
{/if}
|
|
17
|
+
{@render children()}
|
|
18
|
+
</section>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
interface Props extends HTMLAttributes<HTMLElement> {
|
|
4
|
+
title?: string;
|
|
5
|
+
children: Snippet;
|
|
6
|
+
}
|
|
7
|
+
declare const InspectorSection: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type InspectorSection = ReturnType<typeof InspectorSection>;
|
|
9
|
+
export default InspectorSection;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Icon from '@iconify/svelte';
|
|
3
|
+
import * as icons from './icons.js';
|
|
4
|
+
import { labels } from './labels.svelte.js';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Names the whole list, e.g. "All flows". */
|
|
8
|
+
allLabel: string;
|
|
9
|
+
/** The open folder, shown as a breadcrumb after `allLabel`. */
|
|
10
|
+
folder: string | null;
|
|
11
|
+
onselect: (folder: string | null) => void;
|
|
12
|
+
/** The number of items shown, e.g. "26 flows". */
|
|
13
|
+
count: string;
|
|
14
|
+
/** Pending changes, or null when authoring is off. */
|
|
15
|
+
pending: number | null;
|
|
16
|
+
/** Names the pending changes, count included, e.g. "3 pending changes". */
|
|
17
|
+
pendingLabel: string;
|
|
18
|
+
pendingHref: string;
|
|
19
|
+
newHref: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { allLabel, folder, onselect, count, pending, pendingLabel, pendingHref, newHref }: Props =
|
|
23
|
+
$props();
|
|
24
|
+
|
|
25
|
+
let segments = $derived(folder ? folder.split('/') : []);
|
|
26
|
+
|
|
27
|
+
const ANCESTOR = 'shrink-0 text-base-content/60 transition-colors hover:text-base-content';
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<!-- Lives in an `@container` column, so it folds to what fits beside an inspector. -->
|
|
31
|
+
<div class="flex min-h-8 items-center justify-between gap-3">
|
|
32
|
+
<div class="flex min-w-0 items-baseline gap-1.5 text-[0.9375rem] font-semibold whitespace-nowrap">
|
|
33
|
+
<nav aria-label={labels.breadcrumb} class="flex min-w-0 items-baseline gap-1.5">
|
|
34
|
+
{#if folder}
|
|
35
|
+
<button type="button" class={ANCESTOR} onclick={() => onselect(null)}>{allLabel}</button>
|
|
36
|
+
{#each segments as segment, i (i)}
|
|
37
|
+
<span class="shrink-0 opacity-40" aria-hidden="true">/</span>
|
|
38
|
+
{#if i < segments.length - 1}
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
class={ANCESTOR}
|
|
42
|
+
onclick={() => onselect(segments.slice(0, i + 1).join('/'))}
|
|
43
|
+
>
|
|
44
|
+
{segment}
|
|
45
|
+
</button>
|
|
46
|
+
{:else}
|
|
47
|
+
<span class="truncate" aria-current="location">{segment}</span>
|
|
48
|
+
{/if}
|
|
49
|
+
{/each}
|
|
50
|
+
{:else}
|
|
51
|
+
<span class="truncate" aria-current="location">{allLabel}</span>
|
|
52
|
+
{/if}
|
|
53
|
+
</nav>
|
|
54
|
+
<span class="shrink-0 text-xs font-normal opacity-60 @max-sm:hidden">· {count}</span>
|
|
55
|
+
</div>
|
|
56
|
+
{#if pending !== null}
|
|
57
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
58
|
+
<a
|
|
59
|
+
href={pendingHref}
|
|
60
|
+
class={['btn gap-1.5 btn-ghost btn-sm', pending > 0 && 'text-attention']}
|
|
61
|
+
aria-label={pendingLabel}
|
|
62
|
+
>
|
|
63
|
+
<Icon icon={icons.gitPullRequest} class="h-4 w-4" />
|
|
64
|
+
<span class="@max-2xl:hidden">{pendingLabel}</span>
|
|
65
|
+
<span class="tabular-nums @2xl:hidden">{pending}</span>
|
|
66
|
+
</a>
|
|
67
|
+
<a href={newHref} class="btn gap-1.5 btn-primary btn-sm">
|
|
68
|
+
<Icon icon={icons.plus} class="h-4 w-4" />
|
|
69
|
+
{labels.new}
|
|
70
|
+
</a>
|
|
71
|
+
</div>
|
|
72
|
+
{/if}
|
|
73
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Names the whole list, e.g. "All flows". */
|
|
3
|
+
allLabel: string;
|
|
4
|
+
/** The open folder, shown as a breadcrumb after `allLabel`. */
|
|
5
|
+
folder: string | null;
|
|
6
|
+
onselect: (folder: string | null) => void;
|
|
7
|
+
/** The number of items shown, e.g. "26 flows". */
|
|
8
|
+
count: string;
|
|
9
|
+
/** Pending changes, or null when authoring is off. */
|
|
10
|
+
pending: number | null;
|
|
11
|
+
/** Names the pending changes, count included, e.g. "3 pending changes". */
|
|
12
|
+
pendingLabel: string;
|
|
13
|
+
pendingHref: string;
|
|
14
|
+
newHref: string;
|
|
15
|
+
}
|
|
16
|
+
declare const ListHeader: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type ListHeader = ReturnType<typeof ListHeader>;
|
|
18
|
+
export default ListHeader;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Spinner centred in the panel it fills, shown while a page or inspector loads.
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { labels } from './labels.svelte.js';
|
|
5
|
+
|
|
6
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
size?: 'md' | 'lg';
|
|
8
|
+
/** Says what is loading to screen readers; `labels.loading` when unset. */
|
|
9
|
+
label?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { size = 'lg', label, class: className, ...rest }: Props = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
{...rest}
|
|
17
|
+
role="status"
|
|
18
|
+
class={['flex min-h-full flex-1 items-center justify-center py-12', className]}
|
|
19
|
+
>
|
|
20
|
+
<span
|
|
21
|
+
class={['loading loading-spinner text-primary', size === 'lg' ? 'loading-lg' : 'loading-md']}
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
></span>
|
|
24
|
+
<span class="sr-only">{label ?? labels.loading}</span>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
size?: 'md' | 'lg';
|
|
4
|
+
/** Says what is loading to screen readers; `labels.loading` when unset. */
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
declare const Loading: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type Loading = ReturnType<typeof Loading>;
|
|
9
|
+
export default Loading;
|
package/dist/Logo.svelte
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import type { ClassValue } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
class?: ClassValue;
|
|
7
|
+
/** Changes when events flow; each change plays one wave, at most one at a time. */
|
|
8
|
+
beat?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { class: className, beat = 0 }: Props = $props();
|
|
12
|
+
|
|
13
|
+
const MIN_GAP_MS = 1600;
|
|
14
|
+
let wave = $state(0);
|
|
15
|
+
let lastWave = 0;
|
|
16
|
+
|
|
17
|
+
$effect(() => {
|
|
18
|
+
if (!beat) return;
|
|
19
|
+
untrack(() => {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
if (now - lastWave < MIN_GAP_MS) return;
|
|
22
|
+
lastWave = now;
|
|
23
|
+
wave += 1;
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const DOTS = [
|
|
28
|
+
{ cx: 44.61, cy: 6.03, dx: -8, delay: 0 },
|
|
29
|
+
{ cx: 61.65, cy: 15.42, dx: 8, delay: 0.1 },
|
|
30
|
+
{ cx: 20.63, cy: 19.99, dx: 8, delay: 0.2 },
|
|
31
|
+
{ cx: 35.24, cy: 33.96, dx: -8, delay: 0.3 },
|
|
32
|
+
{ cx: 20.63, cy: 48.24, dx: 8, delay: 0.4 }
|
|
33
|
+
];
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<svg
|
|
37
|
+
class={['logo', className]}
|
|
38
|
+
viewBox="0 0 86 55"
|
|
39
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
>
|
|
42
|
+
<g
|
|
43
|
+
fill="none"
|
|
44
|
+
stroke="currentColor"
|
|
45
|
+
stroke-width="1.114"
|
|
46
|
+
stroke-linecap="round"
|
|
47
|
+
stroke-linejoin="round"
|
|
48
|
+
>
|
|
49
|
+
<path d="M0.56 6.03H44.61" />
|
|
50
|
+
<path d="M61.65 15.42H85.16" />
|
|
51
|
+
<path d="M20.63 19.99H45.44L57.64 31.96H85.16" />
|
|
52
|
+
<path d="M0.56 33.96H35.24" />
|
|
53
|
+
<path d="M20.63 48.24H50.43L57.34 41.33H85.16" />
|
|
54
|
+
</g>
|
|
55
|
+
{#key wave}
|
|
56
|
+
{#each DOTS as dot (dot.cx + dot.cy)}
|
|
57
|
+
<circle
|
|
58
|
+
class="logo-dot"
|
|
59
|
+
cx={dot.cx}
|
|
60
|
+
cy={dot.cy}
|
|
61
|
+
r="6.03"
|
|
62
|
+
style="--dx: {dot.dx}px; animation-delay: {dot.delay}s"
|
|
63
|
+
/>
|
|
64
|
+
{/each}
|
|
65
|
+
{/key}
|
|
66
|
+
</svg>
|
|
67
|
+
|
|
68
|
+
<style>
|
|
69
|
+
.logo-dot {
|
|
70
|
+
fill: currentColor;
|
|
71
|
+
animation: logo-slide 1.2s ease-in-out 1;
|
|
72
|
+
}
|
|
73
|
+
:global(a:hover) > .logo .logo-dot,
|
|
74
|
+
.logo:hover .logo-dot {
|
|
75
|
+
animation-iteration-count: infinite;
|
|
76
|
+
}
|
|
77
|
+
@keyframes logo-slide {
|
|
78
|
+
0%,
|
|
79
|
+
100% {
|
|
80
|
+
transform: translateX(0);
|
|
81
|
+
fill: currentColor;
|
|
82
|
+
}
|
|
83
|
+
50% {
|
|
84
|
+
transform: translateX(var(--dx));
|
|
85
|
+
fill: var(--color-accent);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
@media (prefers-reduced-motion: reduce) {
|
|
89
|
+
.logo-dot {
|
|
90
|
+
animation: none;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ClassValue } from 'svelte/elements';
|
|
2
|
+
interface Props {
|
|
3
|
+
class?: ClassValue;
|
|
4
|
+
/** Changes when events flow; each change plays one wave, at most one at a time. */
|
|
5
|
+
beat?: number;
|
|
6
|
+
}
|
|
7
|
+
declare const Logo: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type Logo = ReturnType<typeof Logo>;
|
|
9
|
+
export default Logo;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export interface NavLink {
|
|
3
|
+
href: string;
|
|
4
|
+
label: string;
|
|
5
|
+
active: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface NavGroup {
|
|
9
|
+
title: string;
|
|
10
|
+
/** An image URL shown before the title, such as a product icon. */
|
|
11
|
+
icon?: string;
|
|
12
|
+
links: NavLink[];
|
|
13
|
+
/** Groups nested under this one, indented. */
|
|
14
|
+
groups?: NavGroup[];
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<script lang="ts">
|
|
19
|
+
import Self from './NavList.svelte';
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
/** Each group's `title` and each link's `href` must be unique within its list. */
|
|
23
|
+
groups: NavGroup[];
|
|
24
|
+
/** Called when a link is followed, e.g. to close a drawer on a phone. */
|
|
25
|
+
onnavigate?: () => void;
|
|
26
|
+
/** Set by the list itself for `groups` within a group. */
|
|
27
|
+
nested?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let { groups, onnavigate, nested = false }: Props = $props();
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class={nested ? 'mt-1 space-y-2 pl-3' : 'space-y-5'}>
|
|
34
|
+
{#each groups as group (group.title)}
|
|
35
|
+
<section>
|
|
36
|
+
<svelte:element
|
|
37
|
+
this={nested ? 'h3' : 'h2'}
|
|
38
|
+
class={[
|
|
39
|
+
'mb-1 flex items-center gap-1.5 px-2 tracking-wider uppercase',
|
|
40
|
+
nested
|
|
41
|
+
? 'text-[10px] font-medium text-base-content/60'
|
|
42
|
+
: 'text-xs font-semibold text-base-content/60'
|
|
43
|
+
]}
|
|
44
|
+
>
|
|
45
|
+
{#if group.icon}
|
|
46
|
+
<img src={group.icon} alt="" class="h-4 w-4" />
|
|
47
|
+
{/if}
|
|
48
|
+
{group.title}
|
|
49
|
+
</svelte:element>
|
|
50
|
+
<ul class="space-y-0.5">
|
|
51
|
+
{#each group.links as link (link.href)}
|
|
52
|
+
<li>
|
|
53
|
+
<a
|
|
54
|
+
href={link.href}
|
|
55
|
+
aria-current={link.active ? 'page' : undefined}
|
|
56
|
+
class={[
|
|
57
|
+
'block truncate rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-base-200',
|
|
58
|
+
link.active
|
|
59
|
+
? 'bg-base-200 font-medium text-base-content'
|
|
60
|
+
: 'text-base-content/70 hover:text-base-content'
|
|
61
|
+
]}
|
|
62
|
+
onclick={onnavigate}
|
|
63
|
+
>
|
|
64
|
+
{link.label}
|
|
65
|
+
</a>
|
|
66
|
+
</li>
|
|
67
|
+
{/each}
|
|
68
|
+
</ul>
|
|
69
|
+
{#if group.groups && group.groups.length > 0}
|
|
70
|
+
<Self groups={group.groups} {onnavigate} nested />
|
|
71
|
+
{/if}
|
|
72
|
+
</section>
|
|
73
|
+
{/each}
|
|
74
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface NavLink {
|
|
2
|
+
href: string;
|
|
3
|
+
label: string;
|
|
4
|
+
active: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface NavGroup {
|
|
7
|
+
title: string;
|
|
8
|
+
/** An image URL shown before the title, such as a product icon. */
|
|
9
|
+
icon?: string;
|
|
10
|
+
links: NavLink[];
|
|
11
|
+
/** Groups nested under this one, indented. */
|
|
12
|
+
groups?: NavGroup[];
|
|
13
|
+
}
|
|
14
|
+
interface Props {
|
|
15
|
+
/** Each group's `title` and each link's `href` must be unique within its list. */
|
|
16
|
+
groups: NavGroup[];
|
|
17
|
+
/** Called when a link is followed, e.g. to close a drawer on a phone. */
|
|
18
|
+
onnavigate?: () => void;
|
|
19
|
+
/** Set by the list itself for `groups` within a group. */
|
|
20
|
+
nested?: boolean;
|
|
21
|
+
}
|
|
22
|
+
declare const NavList: import("svelte").Component<Props, {}, "">;
|
|
23
|
+
type NavList = ReturnType<typeof NavList>;
|
|
24
|
+
export default NavList;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { labels } from './labels.svelte.js';
|
|
3
|
+
import type { PaneSize } from './paneSize.svelte.js';
|
|
4
|
+
|
|
5
|
+
let { pane }: { pane: PaneSize } = $props();
|
|
6
|
+
|
|
7
|
+
const EDGE = {
|
|
8
|
+
left: 'inset-y-0 -left-1.5 w-3 cursor-col-resize justify-center',
|
|
9
|
+
right: 'inset-y-0 -right-1.5 w-3 cursor-col-resize justify-center',
|
|
10
|
+
top: 'inset-x-0 -top-1.5 h-3 cursor-row-resize items-center'
|
|
11
|
+
};
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<!-- Drag the pane's inner edge, or use the arrow keys, to resize it; Home or a
|
|
15
|
+
double-click restores the default. The hit area is wider than the line.
|
|
16
|
+
A focusable separator, as in the WAI-ARIA window splitter pattern. -->
|
|
17
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions (a focusable separator is a widget in WAI-ARIA) -->
|
|
18
|
+
<div
|
|
19
|
+
role="separator"
|
|
20
|
+
tabindex="0"
|
|
21
|
+
aria-label={labels.resize}
|
|
22
|
+
aria-orientation={pane.vertical ? 'horizontal' : 'vertical'}
|
|
23
|
+
aria-valuenow={pane.size}
|
|
24
|
+
aria-valuemin={pane.min}
|
|
25
|
+
aria-valuemax={pane.max}
|
|
26
|
+
class="group absolute z-10 flex touch-none focus-visible:outline-none {EDGE[pane.edge]}"
|
|
27
|
+
onpointerdown={pane.start}
|
|
28
|
+
ondblclick={pane.reset}
|
|
29
|
+
onkeydown={pane.keydown}
|
|
30
|
+
>
|
|
31
|
+
<span
|
|
32
|
+
class="transition-colors group-hover:bg-primary/60 group-focus-visible:bg-primary {pane.vertical
|
|
33
|
+
? 'h-px w-full'
|
|
34
|
+
: 'h-full w-px'} {pane.dragging ? 'bg-primary' : ''}"
|
|
35
|
+
></span>
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PaneSize } from './paneSize.svelte.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
pane: PaneSize;
|
|
4
|
+
};
|
|
5
|
+
declare const PaneResizer: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type PaneResizer = ReturnType<typeof PaneResizer>;
|
|
7
|
+
export default PaneResizer;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends string">
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Names the group for assistive tech. */
|
|
4
|
+
label: string;
|
|
5
|
+
options: { id: T; label: string }[];
|
|
6
|
+
value: T;
|
|
7
|
+
onchange?: (value: T) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { label, options, value = $bindable(), onchange }: Props = $props();
|
|
11
|
+
|
|
12
|
+
function select(id: T) {
|
|
13
|
+
value = id;
|
|
14
|
+
onchange?.(id);
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<div
|
|
19
|
+
class="flex items-center gap-0.5 rounded-full border border-base-300 bg-base-200/50 p-0.5 text-xs"
|
|
20
|
+
role="group"
|
|
21
|
+
aria-label={label}
|
|
22
|
+
>
|
|
23
|
+
{#each options as option (option.id)}
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
class={[
|
|
27
|
+
'h-6 rounded-full px-3 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
|
28
|
+
value === option.id
|
|
29
|
+
? 'bg-base-100 font-medium shadow-sm'
|
|
30
|
+
: 'text-base-content/60 hover:text-base-content'
|
|
31
|
+
]}
|
|
32
|
+
aria-pressed={value === option.id}
|
|
33
|
+
onclick={() => select(option.id)}
|
|
34
|
+
>
|
|
35
|
+
{option.label}
|
|
36
|
+
</button>
|
|
37
|
+
{/each}
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
declare function $$render<T extends string>(): {
|
|
2
|
+
props: {
|
|
3
|
+
/** Names the group for assistive tech. */
|
|
4
|
+
label: string;
|
|
5
|
+
options: {
|
|
6
|
+
id: T;
|
|
7
|
+
label: string;
|
|
8
|
+
}[];
|
|
9
|
+
value: T;
|
|
10
|
+
onchange?: (value: T) => void;
|
|
11
|
+
};
|
|
12
|
+
exports: {};
|
|
13
|
+
bindings: "value";
|
|
14
|
+
slots: {};
|
|
15
|
+
events: {};
|
|
16
|
+
};
|
|
17
|
+
declare class __sveltets_Render<T extends string> {
|
|
18
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
19
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
20
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
21
|
+
bindings(): "value";
|
|
22
|
+
exports(): {};
|
|
23
|
+
}
|
|
24
|
+
interface $$IsomorphicComponent {
|
|
25
|
+
new <T extends string>(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 extends string>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
29
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
30
|
+
}
|
|
31
|
+
declare const Segmented: $$IsomorphicComponent;
|
|
32
|
+
type Segmented<T extends string> = InstanceType<typeof Segmented<T>>;
|
|
33
|
+
export default Segmented;
|