@spaethtech/svelte-ui 0.1.10
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 +81 -0
- package/dist/components/Badge/Badge.svelte +80 -0
- package/dist/components/Badge/Badge.svelte.d.ts +14 -0
- package/dist/components/Badge/index.d.ts +1 -0
- package/dist/components/Badge/index.js +1 -0
- package/dist/components/Button/Button.svelte +172 -0
- package/dist/components/Button/Button.svelte.d.ts +32 -0
- package/dist/components/Button/index.d.ts +1 -0
- package/dist/components/Button/index.js +1 -0
- package/dist/components/Dialog/Dialog.svelte +101 -0
- package/dist/components/Dialog/Dialog.svelte.d.ts +18 -0
- package/dist/components/Dialog/index.d.ts +1 -0
- package/dist/components/Dialog/index.js +1 -0
- package/dist/components/ElementManager/ElementManager.svelte +397 -0
- package/dist/components/ElementManager/ElementManager.svelte.d.ts +43 -0
- package/dist/components/ElementManager/index.d.ts +1 -0
- package/dist/components/ElementManager/index.js +1 -0
- package/dist/components/EmailInput/EmailInput.svelte +47 -0
- package/dist/components/EmailInput/EmailInput.svelte.d.ts +14 -0
- package/dist/components/EmailInput/index.d.ts +1 -0
- package/dist/components/EmailInput/index.js +1 -0
- package/dist/components/Icon/Icon.svelte +74 -0
- package/dist/components/Icon/Icon.svelte.d.ts +13 -0
- package/dist/components/Icon/index.d.ts +1 -0
- package/dist/components/Icon/index.js +1 -0
- package/dist/components/Input/Input.svelte +268 -0
- package/dist/components/Input/Input.svelte.d.ts +18 -0
- package/dist/components/Input/index.d.ts +1 -0
- package/dist/components/Input/index.js +1 -0
- package/dist/components/List/List.svelte +580 -0
- package/dist/components/List/List.svelte.d.ts +38 -0
- package/dist/components/List/index.d.ts +1 -0
- package/dist/components/List/index.js +1 -0
- package/dist/components/ListItem/ListItem.svelte +175 -0
- package/dist/components/ListItem/ListItem.svelte.d.ts +24 -0
- package/dist/components/ListItem/index.d.ts +2 -0
- package/dist/components/ListItem/index.js +2 -0
- package/dist/components/ListView/ListView.svelte +463 -0
- package/dist/components/ListView/ListView.svelte.d.ts +37 -0
- package/dist/components/ListView/index.d.ts +2 -0
- package/dist/components/ListView/index.js +2 -0
- package/dist/components/NodeGraph/BaseNode.d.ts +57 -0
- package/dist/components/NodeGraph/BaseNode.js +30 -0
- package/dist/components/NodeGraph/Edge.svelte +60 -0
- package/dist/components/NodeGraph/Edge.svelte.d.ts +16 -0
- package/dist/components/NodeGraph/ExpressionEvaluator.d.ts +82 -0
- package/dist/components/NodeGraph/ExpressionEvaluator.js +152 -0
- package/dist/components/NodeGraph/Node.svelte +100 -0
- package/dist/components/NodeGraph/Node.svelte.d.ts +10 -0
- package/dist/components/NodeGraph/NodeGraph.svelte +52 -0
- package/dist/components/NodeGraph/NodeGraph.svelte.d.ts +14 -0
- package/dist/components/NodeGraph/NodeInstance.svelte.d.ts +80 -0
- package/dist/components/NodeGraph/NodeInstance.svelte.js +241 -0
- package/dist/components/NodeGraph/NodePort.svelte +75 -0
- package/dist/components/NodeGraph/NodePort.svelte.d.ts +11 -0
- package/dist/components/NodeGraph/decorators.d.ts +81 -0
- package/dist/components/NodeGraph/decorators.js +148 -0
- package/dist/components/NodeGraph/index.d.ts +9 -0
- package/dist/components/NodeGraph/index.js +9 -0
- package/dist/components/NodeGraph/types.d.ts +94 -0
- package/dist/components/NodeGraph/types.js +33 -0
- package/dist/components/NotesEditor/NotesEditor.svelte +203 -0
- package/dist/components/NotesEditor/NotesEditor.svelte.d.ts +9 -0
- package/dist/components/NotesEditor/index.d.ts +1 -0
- package/dist/components/NotesEditor/index.js +1 -0
- package/dist/components/NumberInput/NumberInput.svelte +353 -0
- package/dist/components/NumberInput/NumberInput.svelte.d.ts +26 -0
- package/dist/components/NumberInput/index.d.ts +1 -0
- package/dist/components/NumberInput/index.js +1 -0
- package/dist/components/PasswordInput/PasswordInput.svelte +41 -0
- package/dist/components/PasswordInput/PasswordInput.svelte.d.ts +13 -0
- package/dist/components/PasswordInput/index.d.ts +1 -0
- package/dist/components/PasswordInput/index.js +1 -0
- package/dist/components/Popup/index.d.ts +1 -0
- package/dist/components/Popup/index.js +1 -0
- package/dist/components/Rating/Rating.svelte +77 -0
- package/dist/components/Rating/Rating.svelte.d.ts +7 -0
- package/dist/components/Rating/index.d.ts +1 -0
- package/dist/components/Rating/index.js +1 -0
- package/dist/components/ScrollView/ScrollView.svelte +285 -0
- package/dist/components/ScrollView/ScrollView.svelte.d.ts +19 -0
- package/dist/components/ScrollView/index.d.ts +1 -0
- package/dist/components/ScrollView/index.js +1 -0
- package/dist/components/SearchInput/SearchInput.svelte +50 -0
- package/dist/components/SearchInput/SearchInput.svelte.d.ts +15 -0
- package/dist/components/SearchInput/index.d.ts +1 -0
- package/dist/components/SearchInput/index.js +1 -0
- package/dist/components/Select/Select.svelte +278 -0
- package/dist/components/Select/Select.svelte.d.ts +35 -0
- package/dist/components/Select/index.d.ts +1 -0
- package/dist/components/Select/index.js +1 -0
- package/dist/components/TextArea/TextArea.svelte +734 -0
- package/dist/components/TextArea/TextArea.svelte.d.ts +19 -0
- package/dist/components/TextArea/index.d.ts +1 -0
- package/dist/components/TextArea/index.js +1 -0
- package/dist/components/ThemeSelector/ThemeSelector.svelte +64 -0
- package/dist/components/ThemeSelector/ThemeSelector.svelte.d.ts +3 -0
- package/dist/components/ThemeSelector/index.d.ts +1 -0
- package/dist/components/ThemeSelector/index.js +1 -0
- package/dist/components/TooltipHandler/TooltipHandler.svelte +593 -0
- package/dist/components/TooltipHandler/TooltipHandler.svelte.d.ts +10 -0
- package/dist/components/TooltipHandler/index.d.ts +1 -0
- package/dist/components/TooltipHandler/index.js +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +26 -0
- package/dist/styles/sizes.d.ts +78 -0
- package/dist/styles/sizes.js +120 -0
- package/dist/styles/variants.d.ts +106 -0
- package/dist/styles/variants.js +194 -0
- package/dist/types/ManagerTypes.d.ts +42 -0
- package/dist/types/ManagerTypes.js +1 -0
- package/dist/types/sizes.d.ts +5 -0
- package/dist/types/sizes.js +1 -0
- package/dist/types/variants.d.ts +1 -0
- package/dist/types/variants.js +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port mode determines the type of connection
|
|
3
|
+
*/
|
|
4
|
+
export declare enum PortMode {
|
|
5
|
+
Data = "data",// Pass values between nodes
|
|
6
|
+
Flow = "flow"
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Control type for input rendering
|
|
10
|
+
*/
|
|
11
|
+
export declare enum ControlType {
|
|
12
|
+
Port = "port",// Port only (default for connected inputs)
|
|
13
|
+
Text = "text",// Text input
|
|
14
|
+
Number = "number",// Number input
|
|
15
|
+
Expression = "expression",// Expression editor with {{ }} support
|
|
16
|
+
Select = "select",// Dropdown
|
|
17
|
+
Checkbox = "checkbox",// Boolean toggle
|
|
18
|
+
Textarea = "textarea",// Multi-line text
|
|
19
|
+
Slider = "slider",// Numeric slider
|
|
20
|
+
Color = "color",// Color picker
|
|
21
|
+
None = "none"
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Variable scope hierarchy
|
|
25
|
+
*/
|
|
26
|
+
export declare enum VariableScope {
|
|
27
|
+
Global = "global",// Available across all projects
|
|
28
|
+
Project = "project",// Available within a project
|
|
29
|
+
Workflow = "workflow"
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Options for @Node decorator
|
|
33
|
+
*/
|
|
34
|
+
export interface NodeOptions {
|
|
35
|
+
title: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
color?: string;
|
|
38
|
+
category?: string;
|
|
39
|
+
executeInEditor?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Options for @Input decorator
|
|
43
|
+
*/
|
|
44
|
+
export interface InputOptions {
|
|
45
|
+
label?: string;
|
|
46
|
+
dataType?: string;
|
|
47
|
+
mode?: PortMode;
|
|
48
|
+
control?: ControlType;
|
|
49
|
+
showPort?: boolean;
|
|
50
|
+
alwaysShowControl?: boolean;
|
|
51
|
+
defaultValue?: any;
|
|
52
|
+
options?: string[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Options for @Output decorator
|
|
56
|
+
*/
|
|
57
|
+
export interface OutputOptions {
|
|
58
|
+
label?: string;
|
|
59
|
+
dataType?: string;
|
|
60
|
+
mode?: PortMode;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Port metadata stored by decorators
|
|
64
|
+
*/
|
|
65
|
+
export interface PortMetadata {
|
|
66
|
+
key: string;
|
|
67
|
+
label: string;
|
|
68
|
+
dataType?: string;
|
|
69
|
+
mode: PortMode;
|
|
70
|
+
control?: ControlType;
|
|
71
|
+
showPort?: boolean;
|
|
72
|
+
alwaysShowControl?: boolean;
|
|
73
|
+
defaultValue?: any;
|
|
74
|
+
options?: string[];
|
|
75
|
+
value?: any;
|
|
76
|
+
connected?: boolean;
|
|
77
|
+
sourceNode?: any;
|
|
78
|
+
sourcePort?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Complete node metadata from all decorators
|
|
82
|
+
*/
|
|
83
|
+
export interface NodeMetadata {
|
|
84
|
+
node: NodeOptions;
|
|
85
|
+
inputs: PortMetadata[];
|
|
86
|
+
outputs: PortMetadata[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Position in 2D space
|
|
90
|
+
*/
|
|
91
|
+
export interface Position {
|
|
92
|
+
x: number;
|
|
93
|
+
y: number;
|
|
94
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port mode determines the type of connection
|
|
3
|
+
*/
|
|
4
|
+
export var PortMode;
|
|
5
|
+
(function (PortMode) {
|
|
6
|
+
PortMode["Data"] = "data";
|
|
7
|
+
PortMode["Flow"] = "flow"; // Control execution order
|
|
8
|
+
})(PortMode || (PortMode = {}));
|
|
9
|
+
/**
|
|
10
|
+
* Control type for input rendering
|
|
11
|
+
*/
|
|
12
|
+
export var ControlType;
|
|
13
|
+
(function (ControlType) {
|
|
14
|
+
ControlType["Port"] = "port";
|
|
15
|
+
ControlType["Text"] = "text";
|
|
16
|
+
ControlType["Number"] = "number";
|
|
17
|
+
ControlType["Expression"] = "expression";
|
|
18
|
+
ControlType["Select"] = "select";
|
|
19
|
+
ControlType["Checkbox"] = "checkbox";
|
|
20
|
+
ControlType["Textarea"] = "textarea";
|
|
21
|
+
ControlType["Slider"] = "slider";
|
|
22
|
+
ControlType["Color"] = "color";
|
|
23
|
+
ControlType["None"] = "none"; // No UI control (port only)
|
|
24
|
+
})(ControlType || (ControlType = {}));
|
|
25
|
+
/**
|
|
26
|
+
* Variable scope hierarchy
|
|
27
|
+
*/
|
|
28
|
+
export var VariableScope;
|
|
29
|
+
(function (VariableScope) {
|
|
30
|
+
VariableScope["Global"] = "global";
|
|
31
|
+
VariableScope["Project"] = "project";
|
|
32
|
+
VariableScope["Workflow"] = "workflow"; // Available within a specific workflow/graph
|
|
33
|
+
})(VariableScope || (VariableScope = {}));
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
notes = $bindable(""),
|
|
4
|
+
placeholder = "Add your notes...",
|
|
5
|
+
onSave,
|
|
6
|
+
className = ""
|
|
7
|
+
}: {
|
|
8
|
+
notes: string | undefined;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
onSave: (notes: string) => Promise<void>;
|
|
11
|
+
className?: string;
|
|
12
|
+
} = $props();
|
|
13
|
+
|
|
14
|
+
// State management
|
|
15
|
+
let saveState = $state<'idle' | 'typing' | 'saving' | 'saved' | 'error'>('idle');
|
|
16
|
+
let saveTimeout: ReturnType<typeof setTimeout>;
|
|
17
|
+
let lastSavedNotes = $state(notes || '');
|
|
18
|
+
|
|
19
|
+
// Auto-save effect - runs when notes change
|
|
20
|
+
$effect(() => {
|
|
21
|
+
const currentNotes = notes || '';
|
|
22
|
+
|
|
23
|
+
if (currentNotes !== lastSavedNotes && saveState !== 'saving') {
|
|
24
|
+
saveState = 'typing';
|
|
25
|
+
|
|
26
|
+
// Clear existing timeout
|
|
27
|
+
clearTimeout(saveTimeout);
|
|
28
|
+
|
|
29
|
+
// Set new timeout for auto-save
|
|
30
|
+
saveTimeout = setTimeout(async () => {
|
|
31
|
+
await handleSave();
|
|
32
|
+
}, 1000); // Save after 1 second of no typing
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Textarea reference for initial sizing
|
|
37
|
+
let textareaElement: HTMLTextAreaElement;
|
|
38
|
+
|
|
39
|
+
// Initial sizing effect - runs after component mounts
|
|
40
|
+
$effect(() => {
|
|
41
|
+
if (textareaElement && notes) {
|
|
42
|
+
// Use setTimeout to ensure DOM is fully rendered
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
resizeTextarea(textareaElement);
|
|
45
|
+
}, 0);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Ctrl+S listener for manual save
|
|
50
|
+
$effect(() => {
|
|
51
|
+
async function handleKeyDown(event: KeyboardEvent) {
|
|
52
|
+
// Check if Ctrl+S (or Cmd+S on Mac) is pressed and textarea is focused
|
|
53
|
+
if ((event.ctrlKey || event.metaKey) && event.key === 's' &&
|
|
54
|
+
textareaElement && document.activeElement === textareaElement) {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
event.stopPropagation();
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
// Clear any pending auto-save timeout and save immediately
|
|
60
|
+
clearTimeout(saveTimeout);
|
|
61
|
+
await handleSave();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Add listener to document
|
|
66
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
67
|
+
|
|
68
|
+
// Cleanup function
|
|
69
|
+
return () => {
|
|
70
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
async function handleSave() {
|
|
75
|
+
const currentNotes = notes || '';
|
|
76
|
+
|
|
77
|
+
if (currentNotes === lastSavedNotes) {
|
|
78
|
+
// Show brief "No changes" feedback for manual save attempts
|
|
79
|
+
saveState = 'saved';
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
if (saveState === 'saved') {
|
|
82
|
+
saveState = 'idle';
|
|
83
|
+
}
|
|
84
|
+
}, 1000);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
saveState = 'saving';
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await onSave(currentNotes);
|
|
92
|
+
lastSavedNotes = currentNotes;
|
|
93
|
+
saveState = 'saved';
|
|
94
|
+
|
|
95
|
+
// Clear "saved" status after 2 seconds
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
if (saveState === 'saved') {
|
|
98
|
+
saveState = 'idle';
|
|
99
|
+
}
|
|
100
|
+
}, 2000);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error('Failed to save notes:', error);
|
|
103
|
+
saveState = 'error';
|
|
104
|
+
|
|
105
|
+
// Clear error status after 3 seconds
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
if (saveState === 'error') {
|
|
108
|
+
saveState = 'idle';
|
|
109
|
+
}
|
|
110
|
+
}, 3000);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Shared function to resize textarea
|
|
115
|
+
function resizeTextarea(target: HTMLTextAreaElement) {
|
|
116
|
+
// Get computed styles to calculate exact line height
|
|
117
|
+
const computedStyle = window.getComputedStyle(target);
|
|
118
|
+
const lineHeight = parseInt(computedStyle.lineHeight);
|
|
119
|
+
const paddingTop = parseInt(computedStyle.paddingTop);
|
|
120
|
+
const paddingBottom = parseInt(computedStyle.paddingBottom);
|
|
121
|
+
const borderTop = parseInt(computedStyle.borderTopWidth);
|
|
122
|
+
const borderBottom = parseInt(computedStyle.borderBottomWidth);
|
|
123
|
+
|
|
124
|
+
// Calculate heights based on actual lines
|
|
125
|
+
const minLines = 3;
|
|
126
|
+
const maxLines = 15;
|
|
127
|
+
const minHeight = (lineHeight * minLines) + paddingTop + paddingBottom + borderTop + borderBottom;
|
|
128
|
+
const maxHeight = (lineHeight * maxLines) + paddingTop + paddingBottom + borderTop + borderBottom;
|
|
129
|
+
|
|
130
|
+
// Reset height to measure content
|
|
131
|
+
target.style.height = 'auto';
|
|
132
|
+
|
|
133
|
+
// Set height based on content, capped at max
|
|
134
|
+
const contentHeight = target.scrollHeight;
|
|
135
|
+
const newHeight = Math.min(Math.max(contentHeight, minHeight), maxHeight);
|
|
136
|
+
target.style.height = newHeight + 'px';
|
|
137
|
+
|
|
138
|
+
// Only show scrollbar when content actually exceeds max height
|
|
139
|
+
if (contentHeight > maxHeight) {
|
|
140
|
+
target.style.overflowY = 'auto';
|
|
141
|
+
} else {
|
|
142
|
+
target.style.overflowY = 'hidden';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function handleInput(event: Event) {
|
|
147
|
+
const target = event.target as HTMLTextAreaElement;
|
|
148
|
+
resizeTextarea(target);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
// Get status text and styling - simplified approach
|
|
153
|
+
let statusText = $state('');
|
|
154
|
+
let statusClass = $state('');
|
|
155
|
+
|
|
156
|
+
$effect(() => {
|
|
157
|
+
switch (saveState) {
|
|
158
|
+
case 'typing':
|
|
159
|
+
statusText = 'Typing...';
|
|
160
|
+
statusClass = 'text-gray-500';
|
|
161
|
+
break;
|
|
162
|
+
case 'saving':
|
|
163
|
+
statusText = 'Saving...';
|
|
164
|
+
statusClass = 'text-blue-500';
|
|
165
|
+
break;
|
|
166
|
+
case 'saved':
|
|
167
|
+
statusText = 'Saved';
|
|
168
|
+
statusClass = 'text-green-600';
|
|
169
|
+
break;
|
|
170
|
+
case 'error':
|
|
171
|
+
statusText = 'Save failed';
|
|
172
|
+
statusClass = 'text-red-500';
|
|
173
|
+
break;
|
|
174
|
+
default:
|
|
175
|
+
statusText = '';
|
|
176
|
+
statusClass = '';
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
</script>
|
|
180
|
+
|
|
181
|
+
<div class={className}>
|
|
182
|
+
<!-- Header with Notes label and status -->
|
|
183
|
+
<div class="flex items-center justify-between mb-2">
|
|
184
|
+
<label for="notes-textarea" class="block text-sm font-medium [color:var(--color-text)]">
|
|
185
|
+
Notes
|
|
186
|
+
</label>
|
|
187
|
+
<span class="text-xs {statusClass} min-h-[16px]">
|
|
188
|
+
{statusText}
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- Textarea -->
|
|
193
|
+
<textarea
|
|
194
|
+
id="notes-textarea"
|
|
195
|
+
bind:this={textareaElement}
|
|
196
|
+
bind:value={notes}
|
|
197
|
+
{placeholder}
|
|
198
|
+
class="w-full px-3 py-2 bg-transparent [color:var(--color-text)] [border-color:var(--color-secondary)] border-2 rounded-md focus:outline-none hover:[border-color:color-mix(in_srgb,var(--color-secondary)_80%,black)] focus:[border-color:color-mix(in_srgb,var(--color-secondary)_90%,black)] focus:[outline:2px_solid_color-mix(in_srgb,currentColor_70%,transparent)] focus:[outline-offset:0px] resize-none min-h-[80px] overflow-hidden"
|
|
199
|
+
rows="3"
|
|
200
|
+
oninput={handleInput}
|
|
201
|
+
></textarea>
|
|
202
|
+
|
|
203
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
notes: string | undefined;
|
|
3
|
+
placeholder?: string;
|
|
4
|
+
onSave: (notes: string) => Promise<void>;
|
|
5
|
+
className?: string;
|
|
6
|
+
};
|
|
7
|
+
declare const NotesEditor: import("svelte").Component<$$ComponentProps, {}, "notes">;
|
|
8
|
+
type NotesEditor = ReturnType<typeof NotesEditor>;
|
|
9
|
+
export default NotesEditor;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './NotesEditor.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './NotesEditor.svelte';
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Input from '../Input/Input.svelte';
|
|
3
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
interface Props extends Omit<HTMLInputAttributes, 'type' | 'value'> {
|
|
6
|
+
value: number | null;
|
|
7
|
+
class?: string;
|
|
8
|
+
inputClass?: string;
|
|
9
|
+
validate?: (value: number | null) => boolean | string;
|
|
10
|
+
valid?: boolean;
|
|
11
|
+
touched?: boolean;
|
|
12
|
+
element?: HTMLInputElement;
|
|
13
|
+
min?: number;
|
|
14
|
+
max?: number;
|
|
15
|
+
step?: number;
|
|
16
|
+
allowDecimals?: boolean;
|
|
17
|
+
decimalPlaces?: number;
|
|
18
|
+
thousandsSeparator?: boolean;
|
|
19
|
+
prefix?: string;
|
|
20
|
+
suffix?: string;
|
|
21
|
+
currency?: string; // e.g., '$', '€', '£', '¥', etc.
|
|
22
|
+
decimalSeparator?: string; // Decimal separator (default: '.')
|
|
23
|
+
thousandsChar?: string; // Thousands separator character (default: ',')
|
|
24
|
+
roundingMode?: 'round' | 'floor' | 'ceil' | 'trunc'; // Rounding behavior for integers
|
|
25
|
+
negativeMode?: 'minus' | 'parentheses' | 'both'; // How to display/accept negative numbers
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
value = $bindable(),
|
|
30
|
+
valid = $bindable(true),
|
|
31
|
+
touched = $bindable(false),
|
|
32
|
+
element = $bindable(),
|
|
33
|
+
validate,
|
|
34
|
+
min,
|
|
35
|
+
max,
|
|
36
|
+
step = 1,
|
|
37
|
+
allowDecimals = true,
|
|
38
|
+
decimalPlaces = 2,
|
|
39
|
+
thousandsSeparator: enableThousandsSeparator = false,
|
|
40
|
+
prefix = '',
|
|
41
|
+
suffix = '',
|
|
42
|
+
currency,
|
|
43
|
+
decimalSeparator = '.',
|
|
44
|
+
thousandsChar = ',',
|
|
45
|
+
roundingMode = 'round',
|
|
46
|
+
negativeMode = 'minus',
|
|
47
|
+
placeholder = 'Enter a number...',
|
|
48
|
+
...restProps
|
|
49
|
+
}: Props = $props();
|
|
50
|
+
|
|
51
|
+
let displayValue = $state('');
|
|
52
|
+
let focused = $state(false);
|
|
53
|
+
|
|
54
|
+
// Convert number to formatted display string
|
|
55
|
+
function formatNumber(num: number | null): string {
|
|
56
|
+
if (num === null || num === undefined || isNaN(num)) return '';
|
|
57
|
+
|
|
58
|
+
const isNegative = num < 0;
|
|
59
|
+
const absoluteNum = Math.abs(num);
|
|
60
|
+
let formatted = absoluteNum.toString();
|
|
61
|
+
|
|
62
|
+
// Handle decimal places and rounding
|
|
63
|
+
if (allowDecimals && decimalPlaces > 0) {
|
|
64
|
+
formatted = absoluteNum.toFixed(decimalPlaces);
|
|
65
|
+
} else if (!allowDecimals) {
|
|
66
|
+
// Apply rounding mode for integers
|
|
67
|
+
let rounded: number;
|
|
68
|
+
switch (roundingMode) {
|
|
69
|
+
case 'floor':
|
|
70
|
+
rounded = Math.floor(absoluteNum);
|
|
71
|
+
break;
|
|
72
|
+
case 'ceil':
|
|
73
|
+
rounded = Math.ceil(absoluteNum);
|
|
74
|
+
break;
|
|
75
|
+
case 'trunc':
|
|
76
|
+
rounded = Math.trunc(absoluteNum);
|
|
77
|
+
break;
|
|
78
|
+
default: // 'round'
|
|
79
|
+
rounded = Math.round(absoluteNum);
|
|
80
|
+
}
|
|
81
|
+
formatted = rounded.toString();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Add thousands separator and convert to custom format
|
|
85
|
+
if (enableThousandsSeparator) {
|
|
86
|
+
const parts = formatted.split('.');
|
|
87
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandsChar);
|
|
88
|
+
formatted = parts.join(decimalSeparator);
|
|
89
|
+
} else if (decimalSeparator !== '.') {
|
|
90
|
+
// Even without thousands separator, we need to convert decimal separator
|
|
91
|
+
formatted = formatted.replace('.', decimalSeparator);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const finalPrefix = currency || prefix;
|
|
95
|
+
|
|
96
|
+
// Handle negative number display
|
|
97
|
+
if (isNegative) {
|
|
98
|
+
if (negativeMode === 'parentheses') {
|
|
99
|
+
// For parentheses, put everything (prefix, number, suffix) inside
|
|
100
|
+
return `(${finalPrefix}${formatted}${suffix})`;
|
|
101
|
+
} else {
|
|
102
|
+
// 'minus' or 'both' - put minus in front
|
|
103
|
+
return `-${finalPrefix}${formatted}${suffix}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return `${finalPrefix}${formatted}${suffix}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Parse display string back to number
|
|
111
|
+
function parseNumber(str: string): number | null {
|
|
112
|
+
if (!str || str.trim() === '') return null;
|
|
113
|
+
|
|
114
|
+
// Check for negative indicators
|
|
115
|
+
const hasParentheses = str.includes('(') && str.includes(')');
|
|
116
|
+
const hasMinus = str.includes('-');
|
|
117
|
+
const isNegative = hasParentheses || hasMinus;
|
|
118
|
+
|
|
119
|
+
// Remove prefix, suffix, currency
|
|
120
|
+
const finalPrefix = currency || prefix;
|
|
121
|
+
let cleaned = str;
|
|
122
|
+
|
|
123
|
+
if (finalPrefix) {
|
|
124
|
+
const escapedPrefix = finalPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
125
|
+
// Remove prefix from beginning, or after negative indicators
|
|
126
|
+
cleaned = cleaned.replace(new RegExp(`^${escapedPrefix}`), ''); // $123
|
|
127
|
+
cleaned = cleaned.replace(new RegExp(`^-${escapedPrefix}`), '-'); // -$123 -> -123
|
|
128
|
+
cleaned = cleaned.replace(new RegExp(`^\\(${escapedPrefix}`), '('); // ($123 -> (123
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (suffix) {
|
|
132
|
+
const escapedSuffix = suffix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
133
|
+
cleaned = cleaned.replace(new RegExp(`${escapedSuffix}$`), '');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Remove negative indicators
|
|
137
|
+
cleaned = cleaned.replace(/[()]/g, ''); // Remove parentheses
|
|
138
|
+
cleaned = cleaned.replace(/-/g, ''); // Remove minus signs
|
|
139
|
+
|
|
140
|
+
// Remove thousands separators (both , and . could be thousands separators)
|
|
141
|
+
cleaned = cleaned.replace(new RegExp(`\\${thousandsChar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'g'), '');
|
|
142
|
+
|
|
143
|
+
// Convert decimal separator to standard decimal point
|
|
144
|
+
if (decimalSeparator !== '.') {
|
|
145
|
+
// Replace the decimal separator with standard decimal point
|
|
146
|
+
// But only the last occurrence to handle cases like "1.234,56"
|
|
147
|
+
const lastSeparatorIndex = cleaned.lastIndexOf(decimalSeparator);
|
|
148
|
+
if (lastSeparatorIndex !== -1) {
|
|
149
|
+
cleaned = cleaned.substring(0, lastSeparatorIndex) + '.' + cleaned.substring(lastSeparatorIndex + 1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const parsed = parseFloat(cleaned);
|
|
154
|
+
if (isNaN(parsed)) return null;
|
|
155
|
+
|
|
156
|
+
return isNegative ? -parsed : parsed;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Update display value when value changes
|
|
160
|
+
$effect(() => {
|
|
161
|
+
if (!focused) {
|
|
162
|
+
displayValue = formatNumber(value);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Default validation
|
|
167
|
+
const defaultValidator = (val: number | null) => {
|
|
168
|
+
if (val === null) return true; // Allow null/empty values
|
|
169
|
+
|
|
170
|
+
if (min !== undefined && val < min) return `Value must be at least ${min}`;
|
|
171
|
+
if (max !== undefined && val > max) return `Value must be at most ${max}`;
|
|
172
|
+
|
|
173
|
+
if (!allowDecimals && val % 1 !== 0) return 'Decimal values are not allowed';
|
|
174
|
+
|
|
175
|
+
return true;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Use custom validator if provided, otherwise use default
|
|
179
|
+
const numberValidator = validate || defaultValidator;
|
|
180
|
+
|
|
181
|
+
function handleInput(inputValue: string) {
|
|
182
|
+
displayValue = inputValue;
|
|
183
|
+
|
|
184
|
+
// Only skip parsing for truly incomplete inputs that have no numeric content
|
|
185
|
+
const finalPrefix = currency || prefix;
|
|
186
|
+
|
|
187
|
+
// Check if input is incomplete (has negative indicators but no actual numbers)
|
|
188
|
+
const incompletePatterns = [
|
|
189
|
+
'-', // Just minus
|
|
190
|
+
'(', // Just opening paren
|
|
191
|
+
'()', // Empty parens
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
// Add prefix-related incomplete patterns
|
|
195
|
+
if (finalPrefix) {
|
|
196
|
+
incompletePatterns.push(
|
|
197
|
+
`-${finalPrefix}`, // -$, -€, etc.
|
|
198
|
+
`(${finalPrefix}`, // ($, (€, etc.
|
|
199
|
+
`(${finalPrefix})` // ($), (€), etc.
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Add suffix-related incomplete patterns
|
|
204
|
+
if (suffix) {
|
|
205
|
+
incompletePatterns.push(
|
|
206
|
+
`-${suffix}`, // -%, etc.
|
|
207
|
+
`(${suffix}`, // (%, etc.
|
|
208
|
+
`(${suffix})` // (%), etc.
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Combined prefix and suffix patterns
|
|
212
|
+
if (finalPrefix) {
|
|
213
|
+
incompletePatterns.push(
|
|
214
|
+
`-${finalPrefix}${suffix}`, // -$%, etc.
|
|
215
|
+
`(${finalPrefix}${suffix}`, // ($%, etc.
|
|
216
|
+
`(${finalPrefix}${suffix})` // ($%), etc.
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (incompletePatterns.includes(inputValue)) {
|
|
222
|
+
// Keep the current value but update display for incomplete inputs
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const parsed = parseNumber(inputValue);
|
|
227
|
+
value = parsed;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function handleFocus() {
|
|
231
|
+
focused = true;
|
|
232
|
+
// Show the full formatted value including prefix/suffix for consistency
|
|
233
|
+
if (value !== null && !isNaN(value)) {
|
|
234
|
+
displayValue = formatNumber(value);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function handleBlur() {
|
|
239
|
+
focused = false;
|
|
240
|
+
// Format the number when focus is lost
|
|
241
|
+
displayValue = formatNumber(value);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
245
|
+
// Allow control keys (backspace, delete, arrow keys, tab, etc.)
|
|
246
|
+
if (
|
|
247
|
+
event.key === 'Backspace' ||
|
|
248
|
+
event.key === 'Delete' ||
|
|
249
|
+
event.key === 'Tab' ||
|
|
250
|
+
event.key === 'Escape' ||
|
|
251
|
+
event.key === 'Enter' ||
|
|
252
|
+
event.key.startsWith('Arrow') ||
|
|
253
|
+
event.key === 'Home' ||
|
|
254
|
+
event.key === 'End' ||
|
|
255
|
+
(event.ctrlKey || event.metaKey) // Allow Ctrl/Cmd shortcuts
|
|
256
|
+
) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const currentValue = displayValue;
|
|
261
|
+
const finalPrefix = currency || prefix;
|
|
262
|
+
const input = event.target as HTMLInputElement;
|
|
263
|
+
const cursorPosition = input.selectionStart || 0;
|
|
264
|
+
|
|
265
|
+
// Basic allowed characters
|
|
266
|
+
const allowedChars = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
|
|
267
|
+
|
|
268
|
+
// Always allow both comma and period for international input flexibility
|
|
269
|
+
allowedChars.add(',');
|
|
270
|
+
allowedChars.add('.');
|
|
271
|
+
|
|
272
|
+
// Add currency characters (but validate position)
|
|
273
|
+
if (finalPrefix && event.key === finalPrefix) {
|
|
274
|
+
// Only allow currency at the beginning (after minus if present)
|
|
275
|
+
const hasMinusAtStart = currentValue.startsWith('-');
|
|
276
|
+
const expectedPosition = hasMinusAtStart ? 1 : 0;
|
|
277
|
+
if (cursorPosition === expectedPosition && !currentValue.includes(finalPrefix)) {
|
|
278
|
+
allowedChars.add(finalPrefix);
|
|
279
|
+
} else {
|
|
280
|
+
event.preventDefault();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Add suffix characters (but validate position and prevent duplicates)
|
|
286
|
+
if (suffix && event.key === suffix) {
|
|
287
|
+
// Only allow suffix at the end and if not already present
|
|
288
|
+
if (cursorPosition === currentValue.length && !currentValue.includes(suffix)) {
|
|
289
|
+
allowedChars.add(suffix);
|
|
290
|
+
} else {
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Always allow both minus and parentheses for input (negativeMode only affects display formatting)
|
|
297
|
+
if (event.key === '-') {
|
|
298
|
+
// Only allow minus at the very beginning and if not already present
|
|
299
|
+
if (cursorPosition === 0 && !currentValue.includes('-')) {
|
|
300
|
+
allowedChars.add('-');
|
|
301
|
+
} else {
|
|
302
|
+
event.preventDefault();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (event.key === '(') {
|
|
308
|
+
// Only allow opening parenthesis at the beginning and if not already present
|
|
309
|
+
if (cursorPosition === 0 && !currentValue.includes('(')) {
|
|
310
|
+
allowedChars.add('(');
|
|
311
|
+
} else {
|
|
312
|
+
event.preventDefault();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (event.key === ')') {
|
|
317
|
+
// Only allow closing parenthesis at the end, if opening exists, and no closing yet
|
|
318
|
+
if (cursorPosition === currentValue.length && currentValue.includes('(') && !currentValue.includes(')')) {
|
|
319
|
+
allowedChars.add(')');
|
|
320
|
+
} else {
|
|
321
|
+
event.preventDefault();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Prevent typing if character is not allowed
|
|
327
|
+
if (!allowedChars.has(event.key)) {
|
|
328
|
+
event.preventDefault();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Convert our number value to string for the Input component
|
|
333
|
+
let stringValue = $derived(displayValue);
|
|
334
|
+
</script>
|
|
335
|
+
|
|
336
|
+
<Input
|
|
337
|
+
bind:value={stringValue}
|
|
338
|
+
bind:valid
|
|
339
|
+
bind:touched
|
|
340
|
+
bind:element
|
|
341
|
+
type="text"
|
|
342
|
+
inputmode="numeric"
|
|
343
|
+
validate={(val) => {
|
|
344
|
+
const parsed = parseNumber(val);
|
|
345
|
+
return numberValidator(parsed);
|
|
346
|
+
}}
|
|
347
|
+
oninput={(e) => handleInput((e.target as HTMLInputElement).value)}
|
|
348
|
+
onfocus={handleFocus}
|
|
349
|
+
onblur={handleBlur}
|
|
350
|
+
onkeydown={handleKeyDown}
|
|
351
|
+
{placeholder}
|
|
352
|
+
{...restProps}
|
|
353
|
+
/>
|