@spaethtech/svelte-ui 0.1.10 → 0.3.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 +166 -42
- package/dist/components/Alert.svelte +119 -0
- package/dist/components/Alert.svelte.d.ts +29 -0
- package/dist/components/Badge/Badge.svelte +142 -69
- package/dist/components/Badge/Badge.svelte.d.ts +26 -6
- package/dist/components/Badge/index.d.ts +1 -1
- package/dist/components/Badge/index.js +1 -1
- package/dist/components/Banner.svelte +133 -0
- package/dist/components/Banner.svelte.d.ts +31 -0
- package/dist/components/Button.svelte +240 -0
- package/dist/components/Button.svelte.d.ts +33 -0
- package/dist/components/ButtonDropdown.svelte +145 -0
- package/dist/components/ButtonDropdown.svelte.d.ts +31 -0
- package/dist/components/Calendar.svelte +259 -0
- package/dist/components/Calendar.svelte.d.ts +27 -0
- package/dist/components/Card.svelte +78 -0
- package/dist/components/Card.svelte.d.ts +17 -0
- package/dist/components/CardBody.svelte +41 -0
- package/dist/components/CardBody.svelte.d.ts +17 -0
- package/dist/components/CardFooter.svelte +49 -0
- package/dist/components/CardFooter.svelte.d.ts +16 -0
- package/dist/components/CardHeader.svelte +49 -0
- package/dist/components/CardHeader.svelte.d.ts +16 -0
- package/dist/components/Checkbox.svelte +131 -0
- package/dist/components/Checkbox.svelte.d.ts +16 -0
- package/dist/components/DataTable.svelte +518 -0
- package/dist/components/DataTable.svelte.d.ts +74 -0
- package/dist/components/DatePicker.svelte +312 -0
- package/dist/components/DatePicker.svelte.d.ts +32 -0
- package/dist/components/DateTimeInput.svelte +93 -0
- package/dist/components/DateTimeInput.svelte.d.ts +20 -0
- package/dist/components/Dialog.svelte +132 -0
- package/dist/components/Dialog.svelte.d.ts +24 -0
- package/dist/components/{EmailInput/EmailInput.svelte → EmailInput.svelte} +17 -8
- package/dist/components/EmailInput.svelte.d.ts +21 -0
- package/dist/components/Input.svelte +369 -0
- package/dist/components/Input.svelte.d.ts +35 -0
- package/dist/components/{List/List.svelte → List.svelte} +194 -133
- package/dist/components/{List/List.svelte.d.ts → List.svelte.d.ts} +9 -1
- package/dist/components/Menu.svelte +117 -0
- package/dist/components/Menu.svelte.d.ts +20 -0
- package/dist/components/NotesEditor.svelte +127 -0
- package/dist/components/NotesEditor.svelte.d.ts +17 -0
- package/dist/components/{NumberInput/NumberInput.svelte → NumberInput.svelte} +146 -105
- package/dist/components/{NumberInput/NumberInput.svelte.d.ts → NumberInput.svelte.d.ts} +11 -4
- package/dist/components/PasswordInput.svelte +52 -0
- package/dist/components/PasswordInput.svelte.d.ts +20 -0
- package/dist/components/Popup.svelte +140 -0
- package/dist/components/Popup.svelte.d.ts +31 -0
- package/dist/components/Query.svelte +284 -0
- package/dist/components/Query.svelte.d.ts +15 -0
- package/dist/components/{Rating/Rating.svelte → Rating.svelte} +19 -10
- package/dist/components/Rating.svelte.d.ts +13 -0
- package/dist/components/{SearchInput/SearchInput.svelte → SearchInput.svelte} +19 -8
- package/dist/components/{SearchInput/SearchInput.svelte.d.ts → SearchInput.svelte.d.ts} +9 -2
- package/dist/components/{Select/Select.svelte → Select.svelte} +48 -76
- package/dist/components/{Select/Select.svelte.d.ts → Select.svelte.d.ts} +11 -1
- package/dist/components/SideBarMenu/SideBarMenu.svelte +724 -0
- package/dist/components/SideBarMenu/SideBarMenu.svelte.d.ts +109 -0
- package/dist/components/SideBarMenu/index.d.ts +2 -0
- package/dist/components/SideBarMenu/index.js +1 -0
- package/dist/components/TabStrip/TabStrip.svelte +384 -0
- package/dist/components/TabStrip/TabStrip.svelte.d.ts +50 -0
- package/dist/components/TabStrip/index.d.ts +3 -0
- package/dist/components/TabStrip/index.js +2 -0
- package/dist/components/{TextArea/TextArea.svelte → TextArea.svelte} +143 -112
- package/dist/components/TextArea.svelte.d.ts +38 -0
- package/dist/components/ThemeSelector.svelte +81 -0
- package/dist/components/ThemeSelector.svelte.d.ts +10 -0
- package/dist/components/TimePicker.svelte +148 -0
- package/dist/components/TimePicker.svelte.d.ts +28 -0
- package/dist/components/TimeRangeInput.svelte +203 -0
- package/dist/components/TimeRangeInput.svelte.d.ts +29 -0
- package/dist/components/TimeSpinner.svelte +202 -0
- package/dist/components/TimeSpinner.svelte.d.ts +26 -0
- package/dist/components/Toast/Toaster.svelte +51 -0
- package/dist/components/Toast/Toaster.svelte.d.ts +12 -0
- package/dist/components/Toast/toast.svelte.d.ts +29 -0
- package/dist/components/Toast/toast.svelte.js +27 -0
- package/dist/components/Toggle.svelte +93 -0
- package/dist/components/Toggle.svelte.d.ts +15 -0
- package/dist/data/dataset.d.ts +42 -0
- package/dist/data/dataset.js +3 -0
- package/dist/data/index.d.ts +3 -0
- package/dist/data/index.js +3 -0
- package/dist/data/query/ast.d.ts +62 -0
- package/dist/data/query/ast.js +12 -0
- package/dist/data/query/index.d.ts +3 -0
- package/dist/data/query/index.js +3 -0
- package/dist/data/query/lexer.d.ts +33 -0
- package/dist/data/query/lexer.js +225 -0
- package/dist/data/query/parser.d.ts +11 -0
- package/dist/data/query/parser.js +252 -0
- package/dist/data/table/grid.svelte.d.ts +57 -0
- package/dist/data/table/grid.svelte.js +139 -0
- package/dist/data/table/index.d.ts +2 -0
- package/dist/data/table/index.js +2 -0
- package/dist/data/table/types.d.ts +79 -0
- package/dist/index.d.ts +48 -22
- package/dist/index.js +45 -21
- package/dist/positioning/anchored.d.ts +61 -0
- package/dist/positioning/anchored.js +122 -0
- package/dist/positioning/tooltip.d.ts +41 -0
- package/dist/positioning/tooltip.js +147 -0
- package/dist/theme.css +205 -0
- package/dist/types/breakpoints.d.ts +24 -0
- package/dist/types/breakpoints.js +13 -0
- package/dist/types/responsive.d.ts +32 -0
- package/dist/types/responsive.js +54 -0
- package/dist/types/sizes.d.ts +10 -3
- package/dist/types/time.d.ts +19 -0
- package/dist/types/time.js +10 -0
- package/dist/types/variants.d.ts +8 -1
- package/dist/types/variants.js +16 -1
- package/package.json +93 -63
- package/dist/components/Button/Button.svelte +0 -172
- package/dist/components/Button/Button.svelte.d.ts +0 -32
- package/dist/components/Button/index.d.ts +0 -1
- package/dist/components/Button/index.js +0 -1
- package/dist/components/Dialog/Dialog.svelte +0 -101
- package/dist/components/Dialog/Dialog.svelte.d.ts +0 -18
- package/dist/components/Dialog/index.d.ts +0 -1
- package/dist/components/Dialog/index.js +0 -1
- package/dist/components/ElementManager/ElementManager.svelte +0 -397
- package/dist/components/ElementManager/ElementManager.svelte.d.ts +0 -43
- package/dist/components/ElementManager/index.d.ts +0 -1
- package/dist/components/ElementManager/index.js +0 -1
- package/dist/components/EmailInput/EmailInput.svelte.d.ts +0 -14
- package/dist/components/EmailInput/index.d.ts +0 -1
- package/dist/components/EmailInput/index.js +0 -1
- package/dist/components/Icon/Icon.svelte +0 -74
- package/dist/components/Icon/Icon.svelte.d.ts +0 -13
- package/dist/components/Icon/index.d.ts +0 -1
- package/dist/components/Icon/index.js +0 -1
- package/dist/components/Input/Input.svelte +0 -268
- package/dist/components/Input/Input.svelte.d.ts +0 -18
- package/dist/components/Input/index.d.ts +0 -1
- package/dist/components/Input/index.js +0 -1
- package/dist/components/List/index.d.ts +0 -1
- package/dist/components/List/index.js +0 -1
- package/dist/components/ListItem/ListItem.svelte +0 -175
- package/dist/components/ListItem/ListItem.svelte.d.ts +0 -24
- package/dist/components/ListItem/index.d.ts +0 -2
- package/dist/components/ListItem/index.js +0 -2
- package/dist/components/ListView/ListView.svelte +0 -463
- package/dist/components/ListView/ListView.svelte.d.ts +0 -37
- package/dist/components/ListView/index.d.ts +0 -2
- package/dist/components/ListView/index.js +0 -2
- package/dist/components/NodeGraph/BaseNode.d.ts +0 -57
- package/dist/components/NodeGraph/BaseNode.js +0 -30
- package/dist/components/NodeGraph/Edge.svelte +0 -60
- package/dist/components/NodeGraph/Edge.svelte.d.ts +0 -16
- package/dist/components/NodeGraph/ExpressionEvaluator.d.ts +0 -82
- package/dist/components/NodeGraph/ExpressionEvaluator.js +0 -152
- package/dist/components/NodeGraph/Node.svelte +0 -100
- package/dist/components/NodeGraph/Node.svelte.d.ts +0 -10
- package/dist/components/NodeGraph/NodeGraph.svelte +0 -52
- package/dist/components/NodeGraph/NodeGraph.svelte.d.ts +0 -14
- package/dist/components/NodeGraph/NodeInstance.svelte.d.ts +0 -80
- package/dist/components/NodeGraph/NodeInstance.svelte.js +0 -241
- package/dist/components/NodeGraph/NodePort.svelte +0 -75
- package/dist/components/NodeGraph/NodePort.svelte.d.ts +0 -11
- package/dist/components/NodeGraph/decorators.d.ts +0 -81
- package/dist/components/NodeGraph/decorators.js +0 -148
- package/dist/components/NodeGraph/index.d.ts +0 -9
- package/dist/components/NodeGraph/index.js +0 -9
- package/dist/components/NodeGraph/types.d.ts +0 -94
- package/dist/components/NodeGraph/types.js +0 -33
- package/dist/components/NotesEditor/NotesEditor.svelte +0 -203
- package/dist/components/NotesEditor/NotesEditor.svelte.d.ts +0 -9
- package/dist/components/NotesEditor/index.d.ts +0 -1
- package/dist/components/NotesEditor/index.js +0 -1
- package/dist/components/NumberInput/index.d.ts +0 -1
- package/dist/components/NumberInput/index.js +0 -1
- package/dist/components/PasswordInput/PasswordInput.svelte +0 -41
- package/dist/components/PasswordInput/PasswordInput.svelte.d.ts +0 -13
- package/dist/components/PasswordInput/index.d.ts +0 -1
- package/dist/components/PasswordInput/index.js +0 -1
- package/dist/components/Popup/index.d.ts +0 -1
- package/dist/components/Popup/index.js +0 -1
- package/dist/components/Rating/Rating.svelte.d.ts +0 -7
- package/dist/components/Rating/index.d.ts +0 -1
- package/dist/components/Rating/index.js +0 -1
- package/dist/components/ScrollView/ScrollView.svelte +0 -285
- package/dist/components/ScrollView/ScrollView.svelte.d.ts +0 -19
- package/dist/components/ScrollView/index.d.ts +0 -1
- package/dist/components/ScrollView/index.js +0 -1
- package/dist/components/SearchInput/index.d.ts +0 -1
- package/dist/components/SearchInput/index.js +0 -1
- package/dist/components/Select/index.d.ts +0 -1
- package/dist/components/Select/index.js +0 -1
- package/dist/components/TextArea/TextArea.svelte.d.ts +0 -19
- package/dist/components/TextArea/index.d.ts +0 -1
- package/dist/components/TextArea/index.js +0 -1
- package/dist/components/ThemeSelector/ThemeSelector.svelte +0 -64
- package/dist/components/ThemeSelector/ThemeSelector.svelte.d.ts +0 -3
- package/dist/components/ThemeSelector/index.d.ts +0 -1
- package/dist/components/ThemeSelector/index.js +0 -1
- package/dist/components/TooltipHandler/TooltipHandler.svelte +0 -593
- package/dist/components/TooltipHandler/TooltipHandler.svelte.d.ts +0 -10
- package/dist/components/TooltipHandler/index.d.ts +0 -1
- package/dist/components/TooltipHandler/index.js +0 -1
- package/dist/styles/sizes.d.ts +0 -78
- package/dist/styles/sizes.js +0 -120
- package/dist/styles/variants.d.ts +0 -106
- package/dist/styles/variants.js +0 -194
- package/dist/types/ManagerTypes.d.ts +0 -42
- /package/dist/{types/ManagerTypes.js → data/table/types.js} +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
/**
|
|
3
|
+
* Menu — a keyboard-navigable action menu rendered inside a Popup (top layer, flips vs boundary).
|
|
4
|
+
*
|
|
5
|
+
* Convention (from the data-table): render ALL items always and `disabled` the invalid ones.
|
|
6
|
+
* If ANY item has an icon, icon-less items reserve the icon column so labels align. Selecting an
|
|
7
|
+
* enabled item runs its onclick / follows its href and closes; disabled items don't close.
|
|
8
|
+
*/
|
|
9
|
+
-->
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
import Popup from "./Popup.svelte";
|
|
12
|
+
import type { Side, Align, Boundary } from "../positioning/anchored.js";
|
|
13
|
+
import type { MenuItem } from "../data/table/types.js";
|
|
14
|
+
import type { Size } from "../types/sizes.js";
|
|
15
|
+
import { responsiveClasses, type Responsive } from "../types/responsive.js";
|
|
16
|
+
import { variantToken, type Variant } from "../types/variants.js";
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
anchor,
|
|
20
|
+
open = $bindable(false),
|
|
21
|
+
items,
|
|
22
|
+
side = "bottom",
|
|
23
|
+
align = "end",
|
|
24
|
+
boundary = "viewport",
|
|
25
|
+
size = "md",
|
|
26
|
+
variant,
|
|
27
|
+
}: {
|
|
28
|
+
anchor: HTMLElement | undefined;
|
|
29
|
+
open?: boolean;
|
|
30
|
+
items: MenuItem[];
|
|
31
|
+
side?: Side;
|
|
32
|
+
align?: Align;
|
|
33
|
+
boundary?: Boundary;
|
|
34
|
+
size?: Responsive<Size>;
|
|
35
|
+
/** Tints the highlighted item with a semantic colour (via `.ui-accent` / `--ui-accent`) instead
|
|
36
|
+
* of neutral grey. Omit for the default. */
|
|
37
|
+
variant?: Variant;
|
|
38
|
+
} = $props();
|
|
39
|
+
|
|
40
|
+
let active = $state(-1);
|
|
41
|
+
const hasIcons = $derived(items.some((i) => i.icon));
|
|
42
|
+
const enabledIdx = $derived(items.map((i, idx) => (i.disabled ? -1 : idx)).filter((i) => i >= 0));
|
|
43
|
+
|
|
44
|
+
// Size axis → item padding / text / icon box. Variant → --ui-accent (re-mixes the item's hover tint).
|
|
45
|
+
const padMap: Record<Size, string> = { sm: "px-1.5 py-1", md: "px-2 py-1.5", lg: "px-2.5 py-2" };
|
|
46
|
+
const textMap: Record<Size, string> = { sm: "text-xs", md: "text-sm", lg: "text-base" };
|
|
47
|
+
const iconMap: Record<Size, string> = { sm: "w-3.5 h-3.5", md: "w-4 h-4", lg: "w-5 h-5" };
|
|
48
|
+
const padClass = $derived(responsiveClasses(size, padMap));
|
|
49
|
+
const textClass = $derived(responsiveClasses(size, textMap));
|
|
50
|
+
const iconClass = $derived(responsiveClasses(size, iconMap));
|
|
51
|
+
const accentVar = $derived(variant ? `--ui-accent: var(${variantToken[variant]});` : "");
|
|
52
|
+
|
|
53
|
+
function choose(item: MenuItem) {
|
|
54
|
+
if (item.disabled) return;
|
|
55
|
+
item.onclick?.();
|
|
56
|
+
if (item.href) window.location.href = item.href;
|
|
57
|
+
open = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function onkeydown(e: KeyboardEvent) {
|
|
61
|
+
if (!enabledIdx.length) return;
|
|
62
|
+
const pos = enabledIdx.indexOf(active);
|
|
63
|
+
if (e.key === "ArrowDown") {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
active = enabledIdx[(pos + 1 + enabledIdx.length) % enabledIdx.length] ?? enabledIdx[0];
|
|
66
|
+
} else if (e.key === "ArrowUp") {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
active =
|
|
69
|
+
enabledIdx[(pos - 1 + enabledIdx.length) % enabledIdx.length] ??
|
|
70
|
+
enabledIdx[enabledIdx.length - 1];
|
|
71
|
+
} else if (e.key === "Enter" && active >= 0) {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
choose(items[active]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
$effect(() => {
|
|
78
|
+
if (!open) active = -1;
|
|
79
|
+
});
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<Popup {anchor} bind:open {side} {align} {boundary}>
|
|
83
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
84
|
+
<ul
|
|
85
|
+
role="menu"
|
|
86
|
+
tabindex="0"
|
|
87
|
+
{onkeydown}
|
|
88
|
+
class="ui-accent min-w-48 rounded-md border p-1 shadow-lg outline-none [background-color:var(--ui-color-background)] [border-color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border),transparent)]"
|
|
89
|
+
style={accentVar}
|
|
90
|
+
>
|
|
91
|
+
{#each items as item, i (item.label)}
|
|
92
|
+
{@const danger = item.danger}
|
|
93
|
+
<li role="none">
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
role="menuitem"
|
|
97
|
+
disabled={item.disabled}
|
|
98
|
+
onmouseenter={() => (active = item.disabled ? active : i)}
|
|
99
|
+
onclick={() => choose(item)}
|
|
100
|
+
class="flex w-full items-center gap-2 rounded {padClass} {textClass} text-left transition-colors
|
|
101
|
+
{item.disabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer'}
|
|
102
|
+
{i === active && !item.disabled ? '[background-color:var(--ui-color-surface)]' : ''}
|
|
103
|
+
{danger ? '[color:var(--ui-color-error)]' : '[color:var(--ui-color-text)]'}"
|
|
104
|
+
>
|
|
105
|
+
{#if hasIcons}
|
|
106
|
+
<span
|
|
107
|
+
class="inline-flex {iconClass} shrink-0 items-center justify-center [&_svg]:w-full [&_svg]:h-full"
|
|
108
|
+
>
|
|
109
|
+
{#if item.icon}{@const Icon = item.icon}<Icon />{/if}
|
|
110
|
+
</span>
|
|
111
|
+
{/if}
|
|
112
|
+
<span class="truncate">{item.label}</span>
|
|
113
|
+
</button>
|
|
114
|
+
</li>
|
|
115
|
+
{/each}
|
|
116
|
+
</ul>
|
|
117
|
+
</Popup>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Side, Align, Boundary } from "../positioning/anchored.js";
|
|
2
|
+
import type { MenuItem } from "../data/table/types.js";
|
|
3
|
+
import type { Size } from "../types/sizes.js";
|
|
4
|
+
import { type Responsive } from "../types/responsive.js";
|
|
5
|
+
import { type Variant } from "../types/variants.js";
|
|
6
|
+
type $$ComponentProps = {
|
|
7
|
+
anchor: HTMLElement | undefined;
|
|
8
|
+
open?: boolean;
|
|
9
|
+
items: MenuItem[];
|
|
10
|
+
side?: Side;
|
|
11
|
+
align?: Align;
|
|
12
|
+
boundary?: Boundary;
|
|
13
|
+
size?: Responsive<Size>;
|
|
14
|
+
/** Tints the highlighted item with a semantic colour (via `.ui-accent` / `--ui-accent`) instead
|
|
15
|
+
* of neutral grey. Omit for the default. */
|
|
16
|
+
variant?: Variant;
|
|
17
|
+
};
|
|
18
|
+
declare const Menu: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
19
|
+
type Menu = ReturnType<typeof Menu>;
|
|
20
|
+
export default Menu;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
/**
|
|
3
|
+
* NotesEditor — an auto-saving notes field. Built on the core <TextArea> (so it inherits the shared
|
|
4
|
+
* frame, autosize, and the `size` + `variant` axes) plus a debounced auto-save + a status indicator.
|
|
5
|
+
* Saves 1s after typing stops, or immediately on Ctrl/Cmd+S while focused.
|
|
6
|
+
*/
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import TextArea from "./TextArea.svelte";
|
|
10
|
+
import type { Size } from "../types/sizes.js";
|
|
11
|
+
import type { Responsive } from "../types/responsive.js";
|
|
12
|
+
import type { Variant } from "../types/variants.js";
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
notes = $bindable(""),
|
|
16
|
+
placeholder = "Add your notes...",
|
|
17
|
+
onSave,
|
|
18
|
+
size = "md",
|
|
19
|
+
variant = "secondary",
|
|
20
|
+
copy,
|
|
21
|
+
className = "",
|
|
22
|
+
}: {
|
|
23
|
+
notes?: string;
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
onSave?: (notes: string) => Promise<void>;
|
|
26
|
+
/** Shared axes — forwarded to the underlying TextArea. */
|
|
27
|
+
size?: Responsive<Size>;
|
|
28
|
+
variant?: Variant;
|
|
29
|
+
/** Copy affordance — forwarded to the underlying TextArea (defaults to its `'selection'`). */
|
|
30
|
+
copy?: "none" | "selection" | "button";
|
|
31
|
+
className?: string;
|
|
32
|
+
} = $props();
|
|
33
|
+
|
|
34
|
+
let saveState = $state<"idle" | "typing" | "saving" | "saved" | "error">("idle");
|
|
35
|
+
let saveTimeout: ReturnType<typeof setTimeout>;
|
|
36
|
+
let lastSavedNotes = $state(notes || "");
|
|
37
|
+
let textareaElement = $state<HTMLTextAreaElement>();
|
|
38
|
+
|
|
39
|
+
// Auto-save: 1s debounce after typing stops.
|
|
40
|
+
$effect(() => {
|
|
41
|
+
const currentNotes = notes || "";
|
|
42
|
+
if (currentNotes !== lastSavedNotes && saveState !== "saving") {
|
|
43
|
+
saveState = "typing";
|
|
44
|
+
clearTimeout(saveTimeout);
|
|
45
|
+
saveTimeout = setTimeout(() => {
|
|
46
|
+
handleSave();
|
|
47
|
+
}, 1000);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Ctrl/Cmd+S saves immediately while the editor is focused.
|
|
52
|
+
$effect(() => {
|
|
53
|
+
async function handleKeyDown(event: KeyboardEvent) {
|
|
54
|
+
if (
|
|
55
|
+
(event.ctrlKey || event.metaKey) &&
|
|
56
|
+
event.key === "s" &&
|
|
57
|
+
textareaElement &&
|
|
58
|
+
document.activeElement === textareaElement
|
|
59
|
+
) {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
event.stopPropagation();
|
|
62
|
+
clearTimeout(saveTimeout);
|
|
63
|
+
await handleSave();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
67
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
async function handleSave() {
|
|
71
|
+
const currentNotes = notes || "";
|
|
72
|
+
|
|
73
|
+
if (currentNotes === lastSavedNotes) {
|
|
74
|
+
// Brief "Saved" feedback for a manual save with no changes.
|
|
75
|
+
saveState = "saved";
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
if (saveState === "saved") saveState = "idle";
|
|
78
|
+
}, 1000);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
saveState = "saving";
|
|
83
|
+
try {
|
|
84
|
+
await onSave?.(currentNotes);
|
|
85
|
+
lastSavedNotes = currentNotes;
|
|
86
|
+
saveState = "saved";
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
if (saveState === "saved") saveState = "idle";
|
|
89
|
+
}, 2000);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error("Failed to save notes:", error);
|
|
92
|
+
saveState = "error";
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
if (saveState === "error") saveState = "idle";
|
|
95
|
+
}, 3000);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Status text + colour (theme tokens, not hard-coded Tailwind colours).
|
|
100
|
+
const STATUS = {
|
|
101
|
+
idle: { text: "", color: "" },
|
|
102
|
+
typing: { text: "Typing…", color: "color-mix(in srgb, var(--ui-color-text) 55%, transparent)" },
|
|
103
|
+
saving: { text: "Saving…", color: "var(--ui-color-info)" },
|
|
104
|
+
saved: { text: "Saved", color: "var(--ui-color-success)" },
|
|
105
|
+
error: { text: "Save failed", color: "var(--ui-color-error)" },
|
|
106
|
+
} as const;
|
|
107
|
+
const status = $derived(STATUS[saveState]);
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<div class={className}>
|
|
111
|
+
<!-- Header: label + save status -->
|
|
112
|
+
<div class="flex items-center justify-between mb-2">
|
|
113
|
+
<span class="block text-sm font-medium [color:var(--ui-color-text)]">Notes</span>
|
|
114
|
+
<span class="text-xs min-h-[16px]" style="color: {status.color}">{status.text}</span>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<TextArea
|
|
118
|
+
bind:value={notes}
|
|
119
|
+
bind:element={textareaElement}
|
|
120
|
+
{placeholder}
|
|
121
|
+
{size}
|
|
122
|
+
{variant}
|
|
123
|
+
{copy}
|
|
124
|
+
minRows={3}
|
|
125
|
+
maxRows={15}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Size } from "../types/sizes.js";
|
|
2
|
+
import type { Responsive } from "../types/responsive.js";
|
|
3
|
+
import type { Variant } from "../types/variants.js";
|
|
4
|
+
type $$ComponentProps = {
|
|
5
|
+
notes?: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
onSave?: (notes: string) => Promise<void>;
|
|
8
|
+
/** Shared axes — forwarded to the underlying TextArea. */
|
|
9
|
+
size?: Responsive<Size>;
|
|
10
|
+
variant?: Variant;
|
|
11
|
+
/** Copy affordance — forwarded to the underlying TextArea (defaults to its `'selection'`). */
|
|
12
|
+
copy?: "none" | "selection" | "button";
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
declare const NotesEditor: import("svelte").Component<$$ComponentProps, {}, "notes">;
|
|
16
|
+
type NotesEditor = ReturnType<typeof NotesEditor>;
|
|
17
|
+
export default NotesEditor;
|