terminoir 0.4.0 → 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 +38 -28
- package/css/components.css +62 -0
- package/css/shell.css +32 -1
- package/dist/components/AccentSwatches.svelte +1 -1
- package/dist/components/AppShell.svelte +33 -5
- package/dist/components/AppShell.svelte.d.ts +10 -1
- package/dist/components/AppShellNavLink.svelte +2 -4
- package/dist/components/Button.svelte +32 -14
- package/dist/components/Button.svelte.d.ts +2 -0
- package/dist/components/CalendarGrid.svelte +245 -0
- package/dist/components/CalendarGrid.svelte.d.ts +12 -0
- package/dist/components/CanvasStage.svelte +81 -0
- package/dist/components/CanvasStage.svelte.d.ts +19 -0
- package/dist/components/Centered.svelte +36 -0
- package/dist/components/Centered.svelte.d.ts +10 -0
- package/dist/components/Checkbox.svelte +64 -12
- package/dist/components/Checkbox.svelte.d.ts +5 -0
- package/dist/components/Chip.svelte +90 -2
- package/dist/components/Chip.svelte.d.ts +8 -0
- package/dist/components/CommandPalette.svelte +178 -97
- package/dist/components/CommandPalette.svelte.d.ts +15 -8
- package/dist/components/ContextMenu.svelte +68 -0
- package/dist/components/ContextMenu.svelte.d.ts +12 -0
- package/dist/components/DataTable.svelte +139 -0
- package/dist/components/DataTable.svelte.d.ts +39 -0
- package/dist/components/DescriptionList.svelte +41 -0
- package/dist/components/DescriptionList.svelte.d.ts +8 -0
- package/dist/components/Dialog.svelte +2 -1
- package/dist/components/Disclosure.svelte +55 -0
- package/dist/components/Disclosure.svelte.d.ts +10 -0
- package/dist/components/DocumentViewer.svelte +105 -0
- package/dist/components/DocumentViewer.svelte.d.ts +23 -0
- package/dist/components/Drawer.svelte +119 -0
- package/dist/components/Drawer.svelte.d.ts +16 -0
- package/dist/components/DropdownMenu.svelte +46 -62
- package/dist/components/DropdownMenu.svelte.d.ts +7 -6
- package/dist/components/FieldMessage.svelte +9 -0
- package/dist/components/FieldMessage.svelte.d.ts +8 -0
- package/dist/components/FileButton.svelte +44 -0
- package/dist/components/FileButton.svelte.d.ts +16 -0
- package/dist/components/FormRow.svelte +62 -0
- package/dist/components/FormRow.svelte.d.ts +13 -0
- package/dist/components/IconField.svelte +80 -5
- package/dist/components/IconField.svelte.d.ts +2 -0
- package/dist/components/Inline.svelte +52 -0
- package/dist/components/Inline.svelte.d.ts +16 -0
- package/dist/components/Input.svelte +23 -3
- package/dist/components/Input.svelte.d.ts +9 -0
- package/dist/components/List.svelte +42 -0
- package/dist/components/List.svelte.d.ts +13 -0
- package/dist/components/ListItem.svelte +114 -0
- package/dist/components/ListItem.svelte.d.ts +17 -0
- package/dist/components/MenuItems.svelte +44 -0
- package/dist/components/MenuItems.svelte.d.ts +8 -0
- package/dist/components/NavSection.svelte +45 -0
- package/dist/components/NavSection.svelte.d.ts +12 -0
- package/dist/components/Page.svelte +53 -0
- package/dist/components/Page.svelte.d.ts +12 -0
- package/dist/components/PageHeader.svelte +91 -0
- package/dist/components/PageHeader.svelte.d.ts +15 -0
- package/dist/components/Prose.svelte +33 -0
- package/dist/components/Prose.svelte.d.ts +8 -0
- package/dist/components/RadioCards.svelte +68 -0
- package/dist/components/RadioCards.svelte.d.ts +11 -0
- package/dist/components/SearchPicker.svelte +171 -0
- package/dist/components/SearchPicker.svelte.d.ts +19 -0
- package/dist/components/Section.svelte +71 -0
- package/dist/components/Section.svelte.d.ts +12 -0
- package/dist/components/SegmentedControl.svelte +63 -0
- package/dist/components/SegmentedControl.svelte.d.ts +12 -0
- package/dist/components/Select.svelte +22 -4
- package/dist/components/Select.svelte.d.ts +9 -1
- package/dist/components/Spacer.svelte +6 -0
- package/dist/components/Spacer.svelte.d.ts +26 -0
- package/dist/components/Stack.svelte +37 -0
- package/dist/components/Stack.svelte.d.ts +12 -0
- package/dist/components/Switch.svelte +6 -2
- package/dist/components/Switch.svelte.d.ts +2 -0
- package/dist/components/Tabs.svelte +6 -4
- package/dist/components/Text.svelte +68 -0
- package/dist/components/Text.svelte.d.ts +15 -0
- package/dist/components/Textarea.svelte +19 -3
- package/dist/components/Textarea.svelte.d.ts +6 -0
- package/dist/components/TitleInput.svelte +62 -0
- package/dist/components/TitleInput.svelte.d.ts +10 -0
- package/dist/index.d.ts +35 -2
- package/dist/index.js +31 -0
- package/dist/utils/calendar.d.ts +35 -0
- package/dist/utils/calendar.js +61 -0
- package/dist/utils/commands.d.ts +22 -0
- package/dist/utils/commands.js +17 -0
- package/dist/utils/componentTypes.d.ts +41 -0
- package/dist/utils/componentTypes.js +1 -0
- package/dist/utils/listNav.d.ts +2 -0
- package/dist/utils/listNav.js +8 -0
- package/dist/utils/menu.d.ts +10 -0
- package/dist/utils/menu.js +1 -0
- package/package.json +5 -3
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DescriptionItem } from '../utils/componentTypes.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
items: DescriptionItem[];
|
|
4
|
+
class?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const DescriptionList: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type DescriptionList = ReturnType<typeof DescriptionList>;
|
|
8
|
+
export default DescriptionList;
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
onclose?: () => void;
|
|
19
19
|
} = $props();
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const fallbackId = $props.id();
|
|
22
|
+
const titleId = $derived(labelledBy ?? fallbackId);
|
|
22
23
|
|
|
23
24
|
function onBackdropClick(event: MouseEvent) {
|
|
24
25
|
if (event.target === event.currentTarget) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
summary,
|
|
7
|
+
open = $bindable(false),
|
|
8
|
+
class: className = '',
|
|
9
|
+
children,
|
|
10
|
+
}: {
|
|
11
|
+
summary: string;
|
|
12
|
+
open?: boolean;
|
|
13
|
+
class?: string;
|
|
14
|
+
children?: Snippet;
|
|
15
|
+
} = $props();
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<details class={cn('terminoir-disclosure', className)} bind:open>
|
|
19
|
+
<summary class="terminoir-disclosure__summary terminoir-focusable">{summary}</summary>
|
|
20
|
+
<div class="terminoir-disclosure__body">
|
|
21
|
+
{@render children?.()}
|
|
22
|
+
</div>
|
|
23
|
+
</details>
|
|
24
|
+
|
|
25
|
+
<style>
|
|
26
|
+
.terminoir-disclosure__summary {
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: var(--space-2);
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
list-style: none;
|
|
32
|
+
font-size: 0.8125rem;
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
color: var(--muted-foreground);
|
|
35
|
+
border-radius: var(--radius-control);
|
|
36
|
+
}
|
|
37
|
+
.terminoir-disclosure__summary::-webkit-details-marker { display: none; }
|
|
38
|
+
.terminoir-disclosure__summary::before {
|
|
39
|
+
content: '';
|
|
40
|
+
width: 0.4rem;
|
|
41
|
+
height: 0.4rem;
|
|
42
|
+
border-right: 1.5px solid currentColor;
|
|
43
|
+
border-bottom: 1.5px solid currentColor;
|
|
44
|
+
transform: rotate(-45deg);
|
|
45
|
+
transition: transform var(--duration-fast) ease;
|
|
46
|
+
}
|
|
47
|
+
.terminoir-disclosure[open] > .terminoir-disclosure__summary::before { transform: rotate(45deg); }
|
|
48
|
+
.terminoir-disclosure__summary:hover { color: var(--foreground); }
|
|
49
|
+
.terminoir-disclosure__body {
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
gap: var(--space-3);
|
|
53
|
+
padding-top: var(--space-3);
|
|
54
|
+
}
|
|
55
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
summary: string;
|
|
4
|
+
open?: boolean;
|
|
5
|
+
class?: string;
|
|
6
|
+
children?: Snippet;
|
|
7
|
+
};
|
|
8
|
+
declare const Disclosure: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
9
|
+
type Disclosure = ReturnType<typeof Disclosure>;
|
|
10
|
+
export default Disclosure;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
import type { ViewerMark } from '../utils/componentTypes.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
viewport = $bindable(),
|
|
9
|
+
page = $bindable(),
|
|
10
|
+
marks = [],
|
|
11
|
+
onmarkclick,
|
|
12
|
+
onpagepointerup,
|
|
13
|
+
error,
|
|
14
|
+
busy = false,
|
|
15
|
+
class: className = '',
|
|
16
|
+
toolbar,
|
|
17
|
+
children,
|
|
18
|
+
}: {
|
|
19
|
+
/** The scrolling area (measure it to fit pages to width). */
|
|
20
|
+
viewport?: HTMLDivElement;
|
|
21
|
+
/** The page layer marks are positioned against (use for selection maths). */
|
|
22
|
+
page?: HTMLDivElement;
|
|
23
|
+
/** Highlights drawn over the page. */
|
|
24
|
+
marks?: ViewerMark[];
|
|
25
|
+
onmarkclick?: (id: string) => void;
|
|
26
|
+
onpagepointerup?: (event: PointerEvent) => void;
|
|
27
|
+
/** Replaces the page with an error message. */
|
|
28
|
+
error?: string | null;
|
|
29
|
+
busy?: boolean;
|
|
30
|
+
class?: string;
|
|
31
|
+
/** Page navigation, zoom, download… */
|
|
32
|
+
toolbar?: Snippet;
|
|
33
|
+
/** The rendered page, e.g. a `<canvas>`. */
|
|
34
|
+
children?: Snippet;
|
|
35
|
+
} = $props();
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<div class={cn('terminoir-viewer', className)} aria-busy={busy}>
|
|
39
|
+
{#if toolbar}<div class="terminoir-viewer__toolbar">{@render toolbar()}</div>{/if}
|
|
40
|
+
{#if error}
|
|
41
|
+
<p class="terminoir-viewer__error" role="alert">{error}</p>
|
|
42
|
+
{:else}
|
|
43
|
+
<div class="terminoir-viewer__viewport" bind:this={viewport}>
|
|
44
|
+
<div class="terminoir-viewer__page" bind:this={page} role="presentation" onpointerup={onpagepointerup}>
|
|
45
|
+
{@render children?.()}
|
|
46
|
+
{#each marks as mark (mark.id)}
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
class="terminoir-viewer__mark"
|
|
50
|
+
style:left={`${mark.x * 100}%`}
|
|
51
|
+
style:top={`${mark.y * 100}%`}
|
|
52
|
+
style:width={`${mark.w * 100}%`}
|
|
53
|
+
style:height={`${mark.h * 100}%`}
|
|
54
|
+
style:background={mark.color ?? 'rgba(250, 204, 21, 0.35)'}
|
|
55
|
+
aria-label={mark.label ?? 'Highlight'}
|
|
56
|
+
onclick={() => onmarkclick?.(mark.id)}
|
|
57
|
+
></button>
|
|
58
|
+
{/each}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
{/if}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<style>
|
|
65
|
+
.terminoir-viewer {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
border: 1px solid var(--border);
|
|
69
|
+
border-radius: var(--radius);
|
|
70
|
+
background: color-mix(in srgb, var(--card) 35%, var(--background));
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
}
|
|
73
|
+
.terminoir-viewer__toolbar {
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-wrap: wrap;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: var(--space-1);
|
|
78
|
+
padding: var(--space-1) var(--space-2);
|
|
79
|
+
border-bottom: 1px solid var(--border);
|
|
80
|
+
}
|
|
81
|
+
.terminoir-viewer__viewport {
|
|
82
|
+
overflow: auto;
|
|
83
|
+
max-height: min(72vh, 900px);
|
|
84
|
+
padding: var(--space-2);
|
|
85
|
+
}
|
|
86
|
+
.terminoir-viewer__page {
|
|
87
|
+
position: relative;
|
|
88
|
+
display: block;
|
|
89
|
+
width: max-content;
|
|
90
|
+
margin: 0 auto;
|
|
91
|
+
}
|
|
92
|
+
.terminoir-viewer__page :global(canvas) {
|
|
93
|
+
display: block;
|
|
94
|
+
border-radius: calc(var(--radius) - 4px);
|
|
95
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--border) 60%, transparent);
|
|
96
|
+
}
|
|
97
|
+
.terminoir-viewer__mark {
|
|
98
|
+
position: absolute;
|
|
99
|
+
border: 0;
|
|
100
|
+
padding: 0;
|
|
101
|
+
border-radius: 2px;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
}
|
|
104
|
+
.terminoir-viewer__error { margin: var(--space-4); color: var(--destructive); font-size: 0.875rem; }
|
|
105
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { ViewerMark } from '../utils/componentTypes.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
/** The scrolling area (measure it to fit pages to width). */
|
|
5
|
+
viewport?: HTMLDivElement;
|
|
6
|
+
/** The page layer marks are positioned against (use for selection maths). */
|
|
7
|
+
page?: HTMLDivElement;
|
|
8
|
+
/** Highlights drawn over the page. */
|
|
9
|
+
marks?: ViewerMark[];
|
|
10
|
+
onmarkclick?: (id: string) => void;
|
|
11
|
+
onpagepointerup?: (event: PointerEvent) => void;
|
|
12
|
+
/** Replaces the page with an error message. */
|
|
13
|
+
error?: string | null;
|
|
14
|
+
busy?: boolean;
|
|
15
|
+
class?: string;
|
|
16
|
+
/** Page navigation, zoom, download… */
|
|
17
|
+
toolbar?: Snippet;
|
|
18
|
+
/** The rendered page, e.g. a `<canvas>`. */
|
|
19
|
+
children?: Snippet;
|
|
20
|
+
};
|
|
21
|
+
declare const DocumentViewer: import("svelte").Component<$$ComponentProps, {}, "viewport" | "page">;
|
|
22
|
+
type DocumentViewer = ReturnType<typeof DocumentViewer>;
|
|
23
|
+
export default DocumentViewer;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
import NavIcon from './NavIcon.svelte';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
open = $bindable(false),
|
|
8
|
+
title,
|
|
9
|
+
side = 'right',
|
|
10
|
+
width = '28rem',
|
|
11
|
+
onclose,
|
|
12
|
+
class: className = '',
|
|
13
|
+
children,
|
|
14
|
+
footer,
|
|
15
|
+
}: {
|
|
16
|
+
open?: boolean;
|
|
17
|
+
title: string;
|
|
18
|
+
side?: 'left' | 'right';
|
|
19
|
+
/** Panel width on wide screens; phones always get the full width. */
|
|
20
|
+
width?: string;
|
|
21
|
+
onclose?: () => void;
|
|
22
|
+
class?: string;
|
|
23
|
+
children?: Snippet;
|
|
24
|
+
/** Sticky action row at the bottom. */
|
|
25
|
+
footer?: Snippet;
|
|
26
|
+
} = $props();
|
|
27
|
+
|
|
28
|
+
const titleId = $props.id();
|
|
29
|
+
|
|
30
|
+
function close() {
|
|
31
|
+
open = false;
|
|
32
|
+
onclose?.();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$effect(() => {
|
|
36
|
+
if (!open) return;
|
|
37
|
+
const onKey = (event: KeyboardEvent) => event.key === 'Escape' && close();
|
|
38
|
+
document.addEventListener('keydown', onKey);
|
|
39
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
40
|
+
});
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
{#if open}
|
|
44
|
+
<div class="terminoir-drawer-backdrop" role="presentation" onclick={(e) => e.target === e.currentTarget && close()}>
|
|
45
|
+
<div
|
|
46
|
+
class={cn('terminoir-drawer glass-modal', `terminoir-drawer--${side}`, className)}
|
|
47
|
+
style:--terminoir-drawer-width={width}
|
|
48
|
+
role="dialog"
|
|
49
|
+
aria-modal="true"
|
|
50
|
+
aria-labelledby={titleId}
|
|
51
|
+
>
|
|
52
|
+
<header class="terminoir-drawer__head">
|
|
53
|
+
<h2 id={titleId} class="terminoir-drawer__title">{title}</h2>
|
|
54
|
+
<button type="button" class="terminoir-drawer__close terminoir-focusable" aria-label="Close" onclick={close}>
|
|
55
|
+
<NavIcon name="xmark" />
|
|
56
|
+
</button>
|
|
57
|
+
</header>
|
|
58
|
+
<div class="terminoir-drawer__body">{@render children?.()}</div>
|
|
59
|
+
{#if footer}<footer class="terminoir-drawer__footer">{@render footer()}</footer>{/if}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
{/if}
|
|
63
|
+
|
|
64
|
+
<style>
|
|
65
|
+
.terminoir-drawer-backdrop {
|
|
66
|
+
position: fixed;
|
|
67
|
+
inset: 0;
|
|
68
|
+
z-index: 60;
|
|
69
|
+
background: rgba(0, 0, 0, 0.3);
|
|
70
|
+
}
|
|
71
|
+
.terminoir-drawer {
|
|
72
|
+
position: absolute;
|
|
73
|
+
top: 0;
|
|
74
|
+
bottom: 0;
|
|
75
|
+
width: min(100%, var(--terminoir-drawer-width));
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-direction: column;
|
|
78
|
+
border-radius: 0;
|
|
79
|
+
}
|
|
80
|
+
.terminoir-drawer--right { right: 0; }
|
|
81
|
+
.terminoir-drawer--left { left: 0; }
|
|
82
|
+
.terminoir-drawer__head {
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: space-between;
|
|
86
|
+
gap: var(--space-3);
|
|
87
|
+
padding: var(--space-4) var(--space-5);
|
|
88
|
+
border-bottom: 1px solid var(--border);
|
|
89
|
+
}
|
|
90
|
+
.terminoir-drawer__title { margin: 0; font-size: 1rem; font-weight: 600; }
|
|
91
|
+
.terminoir-drawer__close {
|
|
92
|
+
display: inline-flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
width: 2rem;
|
|
96
|
+
height: 2rem;
|
|
97
|
+
border: 0;
|
|
98
|
+
border-radius: var(--radius-control);
|
|
99
|
+
background: transparent;
|
|
100
|
+
color: var(--muted-foreground);
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
}
|
|
103
|
+
.terminoir-drawer__close:hover { color: var(--foreground); background: color-mix(in srgb, var(--foreground) 8%, transparent); }
|
|
104
|
+
.terminoir-drawer__body {
|
|
105
|
+
flex: 1;
|
|
106
|
+
overflow-y: auto;
|
|
107
|
+
padding: var(--space-5);
|
|
108
|
+
display: flex;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
gap: var(--space-5);
|
|
111
|
+
}
|
|
112
|
+
.terminoir-drawer__footer {
|
|
113
|
+
display: flex;
|
|
114
|
+
justify-content: flex-end;
|
|
115
|
+
gap: var(--space-2);
|
|
116
|
+
padding: var(--space-3) var(--space-5);
|
|
117
|
+
border-top: 1px solid var(--border);
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
title: string;
|
|
5
|
+
side?: 'left' | 'right';
|
|
6
|
+
/** Panel width on wide screens; phones always get the full width. */
|
|
7
|
+
width?: string;
|
|
8
|
+
onclose?: () => void;
|
|
9
|
+
class?: string;
|
|
10
|
+
children?: Snippet;
|
|
11
|
+
/** Sticky action row at the bottom. */
|
|
12
|
+
footer?: Snippet;
|
|
13
|
+
};
|
|
14
|
+
declare const Drawer: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
15
|
+
type Drawer = ReturnType<typeof Drawer>;
|
|
16
|
+
export default Drawer;
|
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { cn } from '../utils/cn.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
label: string;
|
|
8
|
-
disabled?: boolean;
|
|
9
|
-
onSelect?: () => void;
|
|
10
|
-
};
|
|
4
|
+
import type { MenuItem } from '../utils/menu.js';
|
|
5
|
+
import MenuItems from './MenuItems.svelte';
|
|
6
|
+
import NavIcon from './NavIcon.svelte';
|
|
11
7
|
|
|
12
8
|
let {
|
|
13
9
|
label = 'Menu',
|
|
10
|
+
icon,
|
|
11
|
+
iconOnly = false,
|
|
12
|
+
align = 'start',
|
|
14
13
|
items = [],
|
|
15
14
|
class: className = '',
|
|
16
15
|
children,
|
|
17
16
|
}: {
|
|
17
|
+
/** Trigger text, or its accessible name when `iconOnly`. */
|
|
18
18
|
label?: string;
|
|
19
|
+
/** NavIcon name on the trigger, e.g. `ellipsis`. */
|
|
20
|
+
icon?: string;
|
|
21
|
+
iconOnly?: boolean;
|
|
22
|
+
/** Which trigger edge the menu lines up with. */
|
|
23
|
+
align?: 'start' | 'end';
|
|
19
24
|
items?: MenuItem[];
|
|
20
25
|
class?: string;
|
|
21
26
|
children?: Snippet;
|
|
22
27
|
} = $props();
|
|
23
28
|
|
|
24
29
|
let open = $state(false);
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
function toggle() {
|
|
28
|
-
open = !open;
|
|
29
|
-
}
|
|
30
|
+
let root: HTMLDivElement | undefined = $state();
|
|
30
31
|
|
|
31
32
|
function select(item: MenuItem) {
|
|
32
33
|
if (item.disabled) return;
|
|
@@ -34,76 +35,59 @@
|
|
|
34
35
|
open = false;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
38
|
+
$effect(() => {
|
|
39
|
+
if (!open) return;
|
|
40
|
+
const onPointer = (event: PointerEvent) => {
|
|
41
|
+
if (root && !root.contains(event.target as Node)) open = false;
|
|
42
|
+
};
|
|
43
|
+
const onKey = (event: KeyboardEvent) => {
|
|
44
|
+
if (event.key === 'Escape') open = false;
|
|
45
|
+
};
|
|
46
|
+
document.addEventListener('pointerdown', onPointer);
|
|
47
|
+
document.addEventListener('keydown', onKey);
|
|
48
|
+
return () => {
|
|
49
|
+
document.removeEventListener('pointerdown', onPointer);
|
|
50
|
+
document.removeEventListener('keydown', onKey);
|
|
51
|
+
};
|
|
52
|
+
});
|
|
40
53
|
</script>
|
|
41
54
|
|
|
42
|
-
<div class={cn('terminoir-menu', className)}
|
|
55
|
+
<div bind:this={root} class={cn('terminoir-menu', className)}>
|
|
43
56
|
<button
|
|
44
57
|
type="button"
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
class={cn(
|
|
59
|
+
'terminoir-btn terminoir-btn--ghost terminoir-btn--sm terminoir-focusable',
|
|
60
|
+
iconOnly && 'terminoir-menu__icon-trigger',
|
|
61
|
+
)}
|
|
47
62
|
aria-haspopup="menu"
|
|
48
63
|
aria-expanded={open}
|
|
49
|
-
|
|
64
|
+
aria-label={iconOnly ? label : undefined}
|
|
65
|
+
title={iconOnly ? label : undefined}
|
|
66
|
+
onclick={() => (open = !open)}
|
|
50
67
|
>
|
|
51
|
-
{
|
|
68
|
+
{#if icon}<NavIcon name={icon} />{/if}
|
|
69
|
+
{#if !iconOnly}{label}{/if}
|
|
52
70
|
</button>
|
|
53
71
|
{#if open}
|
|
54
|
-
<ul class="terminoir-menu__list glass-popover" role="menu">
|
|
55
|
-
{
|
|
56
|
-
<li role="none">
|
|
57
|
-
<button
|
|
58
|
-
type="button"
|
|
59
|
-
role="menuitem"
|
|
60
|
-
class="terminoir-menu__item terminoir-focusable"
|
|
61
|
-
disabled={item.disabled}
|
|
62
|
-
onclick={() => select(item)}
|
|
63
|
-
>
|
|
64
|
-
{item.label}
|
|
65
|
-
</button>
|
|
66
|
-
</li>
|
|
67
|
-
{/each}
|
|
72
|
+
<ul class="terminoir-menu__list glass-popover" class:terminoir-menu__list--end={align === 'end'} role="menu">
|
|
73
|
+
<MenuItems {items} onselect={select} />
|
|
68
74
|
{@render children?.()}
|
|
69
75
|
</ul>
|
|
70
76
|
{/if}
|
|
71
77
|
</div>
|
|
72
78
|
|
|
73
79
|
<style>
|
|
74
|
-
.terminoir-menu {
|
|
75
|
-
|
|
76
|
-
display: inline-block;
|
|
77
|
-
}
|
|
78
|
-
|
|
80
|
+
.terminoir-menu { position: relative; display: inline-block; }
|
|
81
|
+
.terminoir-menu__icon-trigger { padding-inline: 0.5rem; }
|
|
79
82
|
.terminoir-menu__list {
|
|
80
83
|
position: absolute;
|
|
81
|
-
top: calc(100% + var(--space-
|
|
84
|
+
top: calc(100% + var(--space-1));
|
|
82
85
|
left: 0;
|
|
83
|
-
min-width:
|
|
86
|
+
min-width: 11rem;
|
|
84
87
|
margin: 0;
|
|
85
|
-
padding: var(--space-
|
|
88
|
+
padding: var(--space-1);
|
|
86
89
|
list-style: none;
|
|
87
90
|
z-index: 35;
|
|
88
91
|
}
|
|
89
|
-
|
|
90
|
-
.terminoir-menu__item {
|
|
91
|
-
width: 100%;
|
|
92
|
-
text-align: left;
|
|
93
|
-
border: 0;
|
|
94
|
-
background: transparent;
|
|
95
|
-
border-radius: calc(var(--radius) - 4px);
|
|
96
|
-
padding: 0.45rem 0.55rem;
|
|
97
|
-
font: inherit;
|
|
98
|
-
cursor: pointer;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.terminoir-menu__item:hover:not(:disabled) {
|
|
102
|
-
background: color-mix(in srgb, var(--accent) 70%, transparent);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.terminoir-menu__item:disabled {
|
|
106
|
-
opacity: 0.5;
|
|
107
|
-
cursor: not-allowed;
|
|
108
|
-
}
|
|
92
|
+
.terminoir-menu__list--end { left: auto; right: 0; }
|
|
109
93
|
</style>
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
|
|
3
|
-
id: string;
|
|
4
|
-
label: string;
|
|
5
|
-
disabled?: boolean;
|
|
6
|
-
onSelect?: () => void;
|
|
7
|
-
};
|
|
2
|
+
import type { MenuItem } from '../utils/menu.js';
|
|
8
3
|
type $$ComponentProps = {
|
|
4
|
+
/** Trigger text, or its accessible name when `iconOnly`. */
|
|
9
5
|
label?: string;
|
|
6
|
+
/** NavIcon name on the trigger, e.g. `ellipsis`. */
|
|
7
|
+
icon?: string;
|
|
8
|
+
iconOnly?: boolean;
|
|
9
|
+
/** Which trigger edge the menu lines up with. */
|
|
10
|
+
align?: 'start' | 'end';
|
|
10
11
|
items?: MenuItem[];
|
|
11
12
|
class?: string;
|
|
12
13
|
children?: Snippet;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let { id, hint, error }: { id: string; hint?: string; error?: string } = $props();
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
{#if error}
|
|
6
|
+
<p {id} class="terminoir-field__hint terminoir-field__hint--error" role="alert">{error}</p>
|
|
7
|
+
{:else if hint}
|
|
8
|
+
<p {id} class="terminoir-field__hint">{hint}</p>
|
|
9
|
+
{/if}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
onselect,
|
|
7
|
+
accept,
|
|
8
|
+
multiple = false,
|
|
9
|
+
variant = 'secondary',
|
|
10
|
+
size = 'md',
|
|
11
|
+
disabled = false,
|
|
12
|
+
class: className = '',
|
|
13
|
+
children,
|
|
14
|
+
}: {
|
|
15
|
+
/** Called with the chosen files; the input resets so the same file can be picked again. */
|
|
16
|
+
onselect: (files: File[]) => void;
|
|
17
|
+
/** e.g. `application/pdf,image/*`. */
|
|
18
|
+
accept?: string;
|
|
19
|
+
multiple?: boolean;
|
|
20
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'outline';
|
|
21
|
+
size?: 'sm' | 'md' | 'lg';
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
class?: string;
|
|
24
|
+
children?: Snippet;
|
|
25
|
+
} = $props();
|
|
26
|
+
|
|
27
|
+
let input: HTMLInputElement | undefined = $state();
|
|
28
|
+
|
|
29
|
+
function onchange() {
|
|
30
|
+
const files = [...(input?.files ?? [])];
|
|
31
|
+
if (input) input.value = '';
|
|
32
|
+
if (files.length) onselect(files);
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
class={cn('terminoir-btn terminoir-focusable glass-interactive', `terminoir-btn--${variant}`, `terminoir-btn--${size}`, className)}
|
|
39
|
+
{disabled}
|
|
40
|
+
onclick={() => input?.click()}
|
|
41
|
+
>
|
|
42
|
+
{@render children?.()}
|
|
43
|
+
</button>
|
|
44
|
+
<input bind:this={input} class="terminoir-sr-only" type="file" tabindex="-1" aria-hidden="true" {accept} {multiple} {onchange} />
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** Called with the chosen files; the input resets so the same file can be picked again. */
|
|
4
|
+
onselect: (files: File[]) => void;
|
|
5
|
+
/** e.g. `application/pdf,image/*`. */
|
|
6
|
+
accept?: string;
|
|
7
|
+
multiple?: boolean;
|
|
8
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'outline';
|
|
9
|
+
size?: 'sm' | 'md' | 'lg';
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
class?: string;
|
|
12
|
+
children?: Snippet;
|
|
13
|
+
};
|
|
14
|
+
declare const FileButton: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
15
|
+
type FileButton = ReturnType<typeof FileButton>;
|
|
16
|
+
export default FileButton;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
label,
|
|
7
|
+
for: htmlFor,
|
|
8
|
+
hint,
|
|
9
|
+
class: className = '',
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
label: string;
|
|
13
|
+
/** Id of the control, so clicking the label focuses it. */
|
|
14
|
+
for?: string;
|
|
15
|
+
hint?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
/** One control, rendered with its own label hidden (`hideLabel`). */
|
|
18
|
+
children?: Snippet;
|
|
19
|
+
} = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class={cn('terminoir-form-row', className)}>
|
|
23
|
+
<label class="terminoir-form-row__label" for={htmlFor}>{label}</label>
|
|
24
|
+
<div class="terminoir-form-row__control">
|
|
25
|
+
{@render children?.()}
|
|
26
|
+
{#if hint}<p class="terminoir-form-row__hint">{hint}</p>{/if}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<style>
|
|
31
|
+
.terminoir-form-row {
|
|
32
|
+
display: grid;
|
|
33
|
+
grid-template-columns: minmax(7rem, 11rem) minmax(0, 1fr);
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: var(--space-2) var(--space-4);
|
|
36
|
+
min-height: 2.5rem;
|
|
37
|
+
}
|
|
38
|
+
.terminoir-form-row__label {
|
|
39
|
+
font-size: 0.8125rem;
|
|
40
|
+
color: var(--muted-foreground);
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
text-overflow: ellipsis;
|
|
43
|
+
white-space: nowrap;
|
|
44
|
+
}
|
|
45
|
+
.terminoir-form-row__control {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
gap: var(--space-1);
|
|
49
|
+
min-width: 0;
|
|
50
|
+
}
|
|
51
|
+
.terminoir-form-row__hint {
|
|
52
|
+
margin: 0;
|
|
53
|
+
font-size: 0.75rem;
|
|
54
|
+
color: var(--muted-foreground);
|
|
55
|
+
}
|
|
56
|
+
@media (max-width: 700px) {
|
|
57
|
+
.terminoir-form-row {
|
|
58
|
+
grid-template-columns: minmax(0, 1fr);
|
|
59
|
+
gap: var(--space-1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
label: string;
|
|
4
|
+
/** Id of the control, so clicking the label focuses it. */
|
|
5
|
+
for?: string;
|
|
6
|
+
hint?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
/** One control, rendered with its own label hidden (`hideLabel`). */
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
};
|
|
11
|
+
declare const FormRow: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type FormRow = ReturnType<typeof FormRow>;
|
|
13
|
+
export default FormRow;
|