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,91 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
title,
|
|
7
|
+
description,
|
|
8
|
+
class: className = '',
|
|
9
|
+
leading,
|
|
10
|
+
meta,
|
|
11
|
+
actions,
|
|
12
|
+
}: {
|
|
13
|
+
title: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
class?: string;
|
|
16
|
+
/** Icon or avatar before the title. */
|
|
17
|
+
leading?: Snippet;
|
|
18
|
+
/** Small line under the title (counts, badges, breadcrumbs). */
|
|
19
|
+
meta?: Snippet;
|
|
20
|
+
/** Buttons aligned to the right. */
|
|
21
|
+
actions?: Snippet;
|
|
22
|
+
} = $props();
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<header class={cn('terminoir-page-header', className)}>
|
|
26
|
+
{#if leading}
|
|
27
|
+
<div class="terminoir-page-header__leading">{@render leading()}</div>
|
|
28
|
+
{/if}
|
|
29
|
+
<div class="terminoir-page-header__text">
|
|
30
|
+
<h1 class="terminoir-page-header__title">{title}</h1>
|
|
31
|
+
{#if meta}<div class="terminoir-page-header__meta">{@render meta()}</div>{/if}
|
|
32
|
+
{#if description}<p class="terminoir-page-header__description">{description}</p>{/if}
|
|
33
|
+
</div>
|
|
34
|
+
{#if actions}
|
|
35
|
+
<div class="terminoir-page-header__actions">{@render actions()}</div>
|
|
36
|
+
{/if}
|
|
37
|
+
</header>
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
.terminoir-page-header {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: flex-start;
|
|
43
|
+
gap: var(--space-3);
|
|
44
|
+
min-width: 0;
|
|
45
|
+
}
|
|
46
|
+
.terminoir-page-header__leading {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
min-height: 2rem;
|
|
50
|
+
font-size: 1.25rem;
|
|
51
|
+
}
|
|
52
|
+
.terminoir-page-header__text {
|
|
53
|
+
flex: 1;
|
|
54
|
+
min-width: 0;
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
gap: var(--space-1);
|
|
58
|
+
}
|
|
59
|
+
.terminoir-page-header__title {
|
|
60
|
+
margin: 0;
|
|
61
|
+
font-size: 1.5rem;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
line-height: 1.3;
|
|
64
|
+
letter-spacing: -0.01em;
|
|
65
|
+
overflow-wrap: anywhere;
|
|
66
|
+
}
|
|
67
|
+
.terminoir-page-header__meta {
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-wrap: wrap;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: var(--space-2);
|
|
72
|
+
color: var(--muted-foreground);
|
|
73
|
+
font-size: 0.8125rem;
|
|
74
|
+
}
|
|
75
|
+
.terminoir-page-header__description {
|
|
76
|
+
margin: 0;
|
|
77
|
+
color: var(--muted-foreground);
|
|
78
|
+
font-size: 0.875rem;
|
|
79
|
+
max-width: 60ch;
|
|
80
|
+
}
|
|
81
|
+
.terminoir-page-header__actions {
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-wrap: wrap;
|
|
84
|
+
gap: var(--space-2);
|
|
85
|
+
flex-shrink: 0;
|
|
86
|
+
}
|
|
87
|
+
@media (max-width: 700px) {
|
|
88
|
+
.terminoir-page-header { flex-wrap: wrap; }
|
|
89
|
+
.terminoir-page-header__title { font-size: 1.25rem; }
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
class?: string;
|
|
6
|
+
/** Icon or avatar before the title. */
|
|
7
|
+
leading?: Snippet;
|
|
8
|
+
/** Small line under the title (counts, badges, breadcrumbs). */
|
|
9
|
+
meta?: Snippet;
|
|
10
|
+
/** Buttons aligned to the right. */
|
|
11
|
+
actions?: Snippet;
|
|
12
|
+
};
|
|
13
|
+
declare const PageHeader: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
14
|
+
type PageHeader = ReturnType<typeof PageHeader>;
|
|
15
|
+
export default PageHeader;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
let { class: className = '', children }: { class?: string; children?: Snippet } = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<!-- Readable long-form text (policies, help pages, rendered markdown). -->
|
|
9
|
+
<div class={cn('terminoir-prose', className)}>{@render children?.()}</div>
|
|
10
|
+
|
|
11
|
+
<style>
|
|
12
|
+
.terminoir-prose {
|
|
13
|
+
max-width: 44rem;
|
|
14
|
+
line-height: 1.65;
|
|
15
|
+
color: var(--foreground);
|
|
16
|
+
font-size: 0.9375rem;
|
|
17
|
+
}
|
|
18
|
+
.terminoir-prose :global(h1) { font-size: 1.5rem; font-weight: 600; margin: 0 0 var(--space-4); line-height: 1.3; }
|
|
19
|
+
.terminoir-prose :global(h2) { font-size: 1.125rem; font-weight: 600; margin: var(--space-8) 0 var(--space-3); }
|
|
20
|
+
.terminoir-prose :global(h3) { font-size: 1rem; font-weight: 600; margin: var(--space-6) 0 var(--space-2); }
|
|
21
|
+
.terminoir-prose :global(p),
|
|
22
|
+
.terminoir-prose :global(ul),
|
|
23
|
+
.terminoir-prose :global(ol) { margin: 0 0 var(--space-4); }
|
|
24
|
+
.terminoir-prose :global(li + li) { margin-top: var(--space-1); }
|
|
25
|
+
.terminoir-prose :global(a) { color: var(--primary); text-underline-offset: 2px; }
|
|
26
|
+
.terminoir-prose :global(code) {
|
|
27
|
+
font-family: var(--font-mono);
|
|
28
|
+
font-size: 0.875em;
|
|
29
|
+
padding: 0.1em 0.3em;
|
|
30
|
+
border-radius: 0.3em;
|
|
31
|
+
background: color-mix(in srgb, var(--foreground) 8%, transparent);
|
|
32
|
+
}
|
|
33
|
+
</style>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils/cn.js';
|
|
3
|
+
|
|
4
|
+
import type { RadioCardOption } from '../utils/componentTypes.js';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
label,
|
|
8
|
+
options,
|
|
9
|
+
value = $bindable(''),
|
|
10
|
+
class: className = '',
|
|
11
|
+
}: {
|
|
12
|
+
/** Group label (legend). */
|
|
13
|
+
label: string;
|
|
14
|
+
options: RadioCardOption[];
|
|
15
|
+
value?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
} = $props();
|
|
18
|
+
|
|
19
|
+
const name = $props.id();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<fieldset class={cn('terminoir-radio-cards', className)}>
|
|
23
|
+
<legend class="terminoir-field__label">{label}</legend>
|
|
24
|
+
<div class="terminoir-radio-cards__grid">
|
|
25
|
+
{#each options as option (option.value)}
|
|
26
|
+
<label class="terminoir-radio-cards__card" class:terminoir-radio-cards__card--selected={value === option.value}>
|
|
27
|
+
<input class="terminoir-sr-only" type="radio" {name} value={option.value} bind:group={value} />
|
|
28
|
+
<span class="terminoir-radio-cards__title">{option.label}</span>
|
|
29
|
+
{#if option.description}<span class="terminoir-radio-cards__description">{option.description}</span>{/if}
|
|
30
|
+
</label>
|
|
31
|
+
{/each}
|
|
32
|
+
</div>
|
|
33
|
+
</fieldset>
|
|
34
|
+
|
|
35
|
+
<style>
|
|
36
|
+
.terminoir-radio-cards {
|
|
37
|
+
margin: 0;
|
|
38
|
+
padding: 0;
|
|
39
|
+
border: 0;
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
gap: var(--space-2);
|
|
43
|
+
min-width: 0;
|
|
44
|
+
}
|
|
45
|
+
.terminoir-radio-cards__grid {
|
|
46
|
+
display: grid;
|
|
47
|
+
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
|
48
|
+
gap: var(--space-2);
|
|
49
|
+
}
|
|
50
|
+
.terminoir-radio-cards__card {
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
gap: var(--space-1);
|
|
54
|
+
padding: var(--space-3);
|
|
55
|
+
border: 1px solid var(--border);
|
|
56
|
+
border-radius: var(--radius-control);
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
transition: border-color var(--duration-fast) ease, background var(--duration-fast) ease;
|
|
59
|
+
}
|
|
60
|
+
.terminoir-radio-cards__card:hover { border-color: color-mix(in srgb, var(--foreground) 30%, var(--border)); }
|
|
61
|
+
.terminoir-radio-cards__card:has(input:focus-visible) { box-shadow: var(--focus-ring); }
|
|
62
|
+
.terminoir-radio-cards__card--selected {
|
|
63
|
+
border-color: var(--primary);
|
|
64
|
+
background: color-mix(in srgb, var(--primary) 10%, transparent);
|
|
65
|
+
}
|
|
66
|
+
.terminoir-radio-cards__title { font-size: 0.875rem; font-weight: 600; }
|
|
67
|
+
.terminoir-radio-cards__description { font-size: 0.75rem; color: var(--muted-foreground); line-height: 1.4; }
|
|
68
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RadioCardOption } from '../utils/componentTypes.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** Group label (legend). */
|
|
4
|
+
label: string;
|
|
5
|
+
options: RadioCardOption[];
|
|
6
|
+
value?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
};
|
|
9
|
+
declare const RadioCards: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
10
|
+
type RadioCards = ReturnType<typeof RadioCards>;
|
|
11
|
+
export default RadioCards;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils/cn.js';
|
|
3
|
+
import { moveIndex } from '../utils/listNav.js';
|
|
4
|
+
import NavIcon from './NavIcon.svelte';
|
|
5
|
+
|
|
6
|
+
import type { PickerOption } from '../utils/componentTypes.js';
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
label,
|
|
10
|
+
hideLabel = false,
|
|
11
|
+
quiet = false,
|
|
12
|
+
placeholder = 'Search…',
|
|
13
|
+
search,
|
|
14
|
+
onselect,
|
|
15
|
+
debounceMs = 150,
|
|
16
|
+
emptyText = 'No matches',
|
|
17
|
+
disabled = false,
|
|
18
|
+
class: className = '',
|
|
19
|
+
}: {
|
|
20
|
+
label: string;
|
|
21
|
+
/** Keep the label for screen readers only. */
|
|
22
|
+
hideLabel?: boolean;
|
|
23
|
+
/** Borderless until hovered or focused, for inline editing (property rows). */
|
|
24
|
+
quiet?: boolean;
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
/** Returns options for a non-empty query. Stale responses are ignored. */
|
|
27
|
+
search: (query: string) => Promise<PickerOption[]>;
|
|
28
|
+
onselect: (option: PickerOption) => void;
|
|
29
|
+
debounceMs?: number;
|
|
30
|
+
emptyText?: string;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
class?: string;
|
|
33
|
+
} = $props();
|
|
34
|
+
|
|
35
|
+
const uid = $props.id();
|
|
36
|
+
const listId = `${uid}-list`;
|
|
37
|
+
|
|
38
|
+
let query = $state('');
|
|
39
|
+
let options = $state<PickerOption[]>([]);
|
|
40
|
+
let open = $state(false);
|
|
41
|
+
let loading = $state(false);
|
|
42
|
+
let active = $state(-1);
|
|
43
|
+
let requestSeq = 0;
|
|
44
|
+
|
|
45
|
+
$effect(() => {
|
|
46
|
+
const q = query.trim();
|
|
47
|
+
if (!q) {
|
|
48
|
+
options = [];
|
|
49
|
+
open = false;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const seq = ++requestSeq;
|
|
53
|
+
const handle = setTimeout(async () => {
|
|
54
|
+
loading = true;
|
|
55
|
+
try {
|
|
56
|
+
const found = await search(q);
|
|
57
|
+
if (seq !== requestSeq) return;
|
|
58
|
+
options = found;
|
|
59
|
+
active = found.length ? 0 : -1;
|
|
60
|
+
open = true;
|
|
61
|
+
} catch {
|
|
62
|
+
if (seq === requestSeq) options = [];
|
|
63
|
+
} finally {
|
|
64
|
+
if (seq === requestSeq) loading = false;
|
|
65
|
+
}
|
|
66
|
+
}, debounceMs);
|
|
67
|
+
return () => clearTimeout(handle);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function choose(option: PickerOption | undefined) {
|
|
71
|
+
if (!option) return;
|
|
72
|
+
onselect(option);
|
|
73
|
+
query = '';
|
|
74
|
+
open = false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onKeydown(event: KeyboardEvent) {
|
|
78
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
open = true;
|
|
81
|
+
active = moveIndex(active, event.key === 'ArrowDown' ? 1 : -1, options.length);
|
|
82
|
+
} else if (event.key === 'Enter' && open) {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
choose(options[active]);
|
|
85
|
+
} else if (event.key === 'Escape') {
|
|
86
|
+
open = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<div class={cn('terminoir-field terminoir-picker', quiet && 'terminoir-field--quiet', className)}>
|
|
92
|
+
<label class={cn('terminoir-field__label', hideLabel && 'terminoir-sr-only')} for={`${uid}-input`}>{label}</label>
|
|
93
|
+
<div class="terminoir-picker__anchor">
|
|
94
|
+
<input
|
|
95
|
+
id={`${uid}-input`}
|
|
96
|
+
class="terminoir-input terminoir-focusable"
|
|
97
|
+
type="search"
|
|
98
|
+
role="combobox"
|
|
99
|
+
autocomplete="off"
|
|
100
|
+
aria-expanded={open}
|
|
101
|
+
aria-controls={listId}
|
|
102
|
+
aria-autocomplete="list"
|
|
103
|
+
aria-activedescendant={open && active >= 0 ? `${uid}-opt-${active}` : undefined}
|
|
104
|
+
{placeholder}
|
|
105
|
+
{disabled}
|
|
106
|
+
bind:value={query}
|
|
107
|
+
onkeydown={onKeydown}
|
|
108
|
+
onblur={() => setTimeout(() => (open = false), 120)}
|
|
109
|
+
onfocus={() => (open = options.length > 0)}
|
|
110
|
+
/>
|
|
111
|
+
{#if open}
|
|
112
|
+
<ul id={listId} class="terminoir-picker__list glass-popover" role="listbox" aria-label={label}>
|
|
113
|
+
{#each options as option, index (option.id)}
|
|
114
|
+
<!-- Combobox pattern: focus stays in the input, which handles arrow keys and Enter. -->
|
|
115
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
116
|
+
<li
|
|
117
|
+
id={`${uid}-opt-${index}`}
|
|
118
|
+
role="option"
|
|
119
|
+
aria-selected={index === active}
|
|
120
|
+
class="terminoir-picker__option"
|
|
121
|
+
class:terminoir-picker__option--active={index === active}
|
|
122
|
+
onpointerdown={(event) => event.preventDefault()}
|
|
123
|
+
onclick={() => choose(option)}
|
|
124
|
+
onpointerenter={() => (active = index)}
|
|
125
|
+
>
|
|
126
|
+
{#if option.icon}<NavIcon name={option.icon} color={option.iconColor} />{/if}
|
|
127
|
+
<span class="terminoir-picker__label">{option.label}</span>
|
|
128
|
+
{#if option.description}<span class="terminoir-picker__description">{option.description}</span>{/if}
|
|
129
|
+
</li>
|
|
130
|
+
{:else}
|
|
131
|
+
<li class="terminoir-picker__empty" role="presentation">{loading ? 'Searching…' : emptyText}</li>
|
|
132
|
+
{/each}
|
|
133
|
+
</ul>
|
|
134
|
+
{/if}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<style>
|
|
139
|
+
.terminoir-picker__anchor { position: relative; }
|
|
140
|
+
.terminoir-picker__list {
|
|
141
|
+
position: absolute;
|
|
142
|
+
top: calc(100% + var(--space-1));
|
|
143
|
+
left: 0;
|
|
144
|
+
right: 0;
|
|
145
|
+
z-index: 40;
|
|
146
|
+
max-height: 16rem;
|
|
147
|
+
overflow-y: auto;
|
|
148
|
+
margin: 0;
|
|
149
|
+
padding: var(--space-1);
|
|
150
|
+
list-style: none;
|
|
151
|
+
}
|
|
152
|
+
.terminoir-picker__option {
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
gap: var(--space-2);
|
|
156
|
+
padding: 0.45rem 0.6rem;
|
|
157
|
+
border-radius: calc(var(--radius-control) - 0.2rem);
|
|
158
|
+
font-size: 0.875rem;
|
|
159
|
+
cursor: pointer;
|
|
160
|
+
}
|
|
161
|
+
.terminoir-picker__option--active { background: color-mix(in srgb, var(--foreground) 8%, transparent); }
|
|
162
|
+
.terminoir-picker__label {
|
|
163
|
+
flex: 1;
|
|
164
|
+
min-width: 0;
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
text-overflow: ellipsis;
|
|
167
|
+
white-space: nowrap;
|
|
168
|
+
}
|
|
169
|
+
.terminoir-picker__description { font-size: 0.75rem; color: var(--muted-foreground); flex-shrink: 0; }
|
|
170
|
+
.terminoir-picker__empty { padding: 0.5rem 0.6rem; font-size: 0.8125rem; color: var(--muted-foreground); }
|
|
171
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PickerOption } from '../utils/componentTypes.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
label: string;
|
|
4
|
+
/** Keep the label for screen readers only. */
|
|
5
|
+
hideLabel?: boolean;
|
|
6
|
+
/** Borderless until hovered or focused, for inline editing (property rows). */
|
|
7
|
+
quiet?: boolean;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
/** Returns options for a non-empty query. Stale responses are ignored. */
|
|
10
|
+
search: (query: string) => Promise<PickerOption[]>;
|
|
11
|
+
onselect: (option: PickerOption) => void;
|
|
12
|
+
debounceMs?: number;
|
|
13
|
+
emptyText?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
class?: string;
|
|
16
|
+
};
|
|
17
|
+
declare const SearchPicker: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
18
|
+
type SearchPicker = ReturnType<typeof SearchPicker>;
|
|
19
|
+
export default SearchPicker;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
title,
|
|
7
|
+
description,
|
|
8
|
+
class: className = '',
|
|
9
|
+
actions,
|
|
10
|
+
children,
|
|
11
|
+
...rest
|
|
12
|
+
}: {
|
|
13
|
+
title?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
class?: string;
|
|
16
|
+
/** Small buttons beside the heading (e.g. "Add"). */
|
|
17
|
+
actions?: Snippet;
|
|
18
|
+
children?: Snippet;
|
|
19
|
+
} & Record<string, unknown> = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<section class={cn('terminoir-section', className)} {...rest}>
|
|
23
|
+
{#if title || actions}
|
|
24
|
+
<div class="terminoir-section__head">
|
|
25
|
+
<div class="terminoir-section__text">
|
|
26
|
+
{#if title}<h2 class="terminoir-section__title">{title}</h2>{/if}
|
|
27
|
+
{#if description}<p class="terminoir-section__description">{description}</p>{/if}
|
|
28
|
+
</div>
|
|
29
|
+
{#if actions}<div class="terminoir-section__actions">{@render actions()}</div>{/if}
|
|
30
|
+
</div>
|
|
31
|
+
{/if}
|
|
32
|
+
{@render children?.()}
|
|
33
|
+
</section>
|
|
34
|
+
|
|
35
|
+
<style>
|
|
36
|
+
.terminoir-section {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
gap: var(--space-3);
|
|
40
|
+
min-width: 0;
|
|
41
|
+
}
|
|
42
|
+
.terminoir-section__head {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: flex-end;
|
|
45
|
+
justify-content: space-between;
|
|
46
|
+
gap: var(--space-3);
|
|
47
|
+
}
|
|
48
|
+
.terminoir-section__text {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: var(--space-1);
|
|
52
|
+
min-width: 0;
|
|
53
|
+
}
|
|
54
|
+
.terminoir-section__title {
|
|
55
|
+
margin: 0;
|
|
56
|
+
font-size: 0.75rem;
|
|
57
|
+
font-weight: 600;
|
|
58
|
+
letter-spacing: 0.06em;
|
|
59
|
+
text-transform: uppercase;
|
|
60
|
+
color: var(--muted-foreground);
|
|
61
|
+
}
|
|
62
|
+
.terminoir-section__description {
|
|
63
|
+
margin: 0;
|
|
64
|
+
font-size: 0.8125rem;
|
|
65
|
+
color: var(--muted-foreground);
|
|
66
|
+
}
|
|
67
|
+
.terminoir-section__actions {
|
|
68
|
+
display: flex;
|
|
69
|
+
gap: var(--space-2);
|
|
70
|
+
}
|
|
71
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
title?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
class?: string;
|
|
6
|
+
/** Small buttons beside the heading (e.g. "Add"). */
|
|
7
|
+
actions?: Snippet;
|
|
8
|
+
children?: Snippet;
|
|
9
|
+
} & Record<string, unknown>;
|
|
10
|
+
declare const Section: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type Section = ReturnType<typeof Section>;
|
|
12
|
+
export default Section;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils/cn.js';
|
|
3
|
+
|
|
4
|
+
import type { SegmentOption } from '../utils/componentTypes.js';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
label,
|
|
8
|
+
options,
|
|
9
|
+
value = $bindable(''),
|
|
10
|
+
size = 'md',
|
|
11
|
+
class: className = '',
|
|
12
|
+
}: {
|
|
13
|
+
/** Accessible group name; not shown. */
|
|
14
|
+
label: string;
|
|
15
|
+
options: SegmentOption[];
|
|
16
|
+
value?: string;
|
|
17
|
+
size?: 'sm' | 'md';
|
|
18
|
+
class?: string;
|
|
19
|
+
} = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class={cn('terminoir-segmented', `terminoir-segmented--${size}`, className)} role="radiogroup" aria-label={label}>
|
|
23
|
+
{#each options as option (option.value)}
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
role="radio"
|
|
27
|
+
aria-checked={value === option.value}
|
|
28
|
+
class="terminoir-segmented__option terminoir-focusable"
|
|
29
|
+
class:terminoir-segmented__option--active={value === option.value}
|
|
30
|
+
onclick={() => (value = option.value)}
|
|
31
|
+
>
|
|
32
|
+
{option.label}
|
|
33
|
+
</button>
|
|
34
|
+
{/each}
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<style>
|
|
38
|
+
.terminoir-segmented {
|
|
39
|
+
display: inline-flex;
|
|
40
|
+
padding: 0.2rem;
|
|
41
|
+
gap: 0.15rem;
|
|
42
|
+
border: 1px solid var(--border);
|
|
43
|
+
border-radius: var(--radius-control);
|
|
44
|
+
background: color-mix(in srgb, var(--card) 40%, var(--background));
|
|
45
|
+
}
|
|
46
|
+
.terminoir-segmented__option {
|
|
47
|
+
border: 0;
|
|
48
|
+
background: transparent;
|
|
49
|
+
color: var(--muted-foreground);
|
|
50
|
+
font: inherit;
|
|
51
|
+
font-size: 0.8125rem;
|
|
52
|
+
font-weight: 500;
|
|
53
|
+
padding: 0.3rem 0.7rem;
|
|
54
|
+
border-radius: calc(var(--radius-control) - 0.2rem);
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
}
|
|
57
|
+
.terminoir-segmented--sm .terminoir-segmented__option { font-size: 0.75rem; padding: 0.2rem 0.55rem; }
|
|
58
|
+
.terminoir-segmented__option:hover { color: var(--foreground); }
|
|
59
|
+
.terminoir-segmented__option--active {
|
|
60
|
+
color: var(--foreground);
|
|
61
|
+
background: color-mix(in srgb, var(--foreground) 10%, transparent);
|
|
62
|
+
}
|
|
63
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SegmentOption } from '../utils/componentTypes.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
/** Accessible group name; not shown. */
|
|
4
|
+
label: string;
|
|
5
|
+
options: SegmentOption[];
|
|
6
|
+
value?: string;
|
|
7
|
+
size?: 'sm' | 'md';
|
|
8
|
+
class?: string;
|
|
9
|
+
};
|
|
10
|
+
declare const SegmentedControl: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
11
|
+
type SegmentedControl = ReturnType<typeof SegmentedControl>;
|
|
12
|
+
export default SegmentedControl;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
2
3
|
import { cn } from '../utils/cn.js';
|
|
4
|
+
import FieldMessage from './FieldMessage.svelte';
|
|
3
5
|
|
|
4
6
|
let {
|
|
5
7
|
label,
|
|
8
|
+
hideLabel = false,
|
|
9
|
+
quiet = false,
|
|
10
|
+
hint,
|
|
11
|
+
error,
|
|
6
12
|
id,
|
|
7
13
|
value = $bindable(''),
|
|
8
14
|
disabled = false,
|
|
@@ -11,25 +17,37 @@
|
|
|
11
17
|
...rest
|
|
12
18
|
}: {
|
|
13
19
|
label: string;
|
|
20
|
+
/** Keep the label for screen readers only (compact rows, toolbars). */
|
|
21
|
+
hideLabel?: boolean;
|
|
22
|
+
/** Borderless until hovered or focused, for inline editing (property rows). */
|
|
23
|
+
quiet?: boolean;
|
|
24
|
+
hint?: string;
|
|
25
|
+
error?: string;
|
|
14
26
|
id?: string;
|
|
15
27
|
value?: string;
|
|
16
28
|
disabled?: boolean;
|
|
17
29
|
class?: string;
|
|
18
|
-
|
|
30
|
+
/** `<option>` elements. */
|
|
31
|
+
children?: Snippet;
|
|
19
32
|
} & Record<string, unknown> = $props();
|
|
20
33
|
|
|
21
|
-
const
|
|
34
|
+
const fallbackId = $props.id();
|
|
35
|
+
const selectId = $derived(id ?? fallbackId);
|
|
36
|
+
const messageId = $derived(`${selectId}-message`);
|
|
22
37
|
</script>
|
|
23
38
|
|
|
24
|
-
<div class={cn('terminoir-field', className)}>
|
|
25
|
-
<label class=
|
|
39
|
+
<div class={cn('terminoir-field', quiet && 'terminoir-field--quiet', className)}>
|
|
40
|
+
<label class={cn('terminoir-field__label', hideLabel && 'terminoir-sr-only')} for={selectId}>{label}</label>
|
|
26
41
|
<select
|
|
27
42
|
id={selectId}
|
|
28
43
|
class="terminoir-select terminoir-focusable"
|
|
29
44
|
bind:value
|
|
30
45
|
{disabled}
|
|
46
|
+
aria-invalid={error ? true : undefined}
|
|
47
|
+
aria-describedby={error || hint ? messageId : undefined}
|
|
31
48
|
{...rest}
|
|
32
49
|
>
|
|
33
50
|
{@render children?.()}
|
|
34
51
|
</select>
|
|
52
|
+
<FieldMessage id={messageId} {hint} {error} />
|
|
35
53
|
</div>
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
type $$ComponentProps = {
|
|
2
3
|
label: string;
|
|
4
|
+
/** Keep the label for screen readers only (compact rows, toolbars). */
|
|
5
|
+
hideLabel?: boolean;
|
|
6
|
+
/** Borderless until hovered or focused, for inline editing (property rows). */
|
|
7
|
+
quiet?: boolean;
|
|
8
|
+
hint?: string;
|
|
9
|
+
error?: string;
|
|
3
10
|
id?: string;
|
|
4
11
|
value?: string;
|
|
5
12
|
disabled?: boolean;
|
|
6
13
|
class?: string;
|
|
7
|
-
|
|
14
|
+
/** `<option>` elements. */
|
|
15
|
+
children?: Snippet;
|
|
8
16
|
} & Record<string, unknown>;
|
|
9
17
|
declare const Select: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
10
18
|
type Select = ReturnType<typeof Select>;
|