dr-widget 0.1.3__py3-none-any.whl
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.
- dr_widget/__init__.py +5 -0
- dr_widget/py.typed +0 -0
- dr_widget/widgets/__init__.py +5 -0
- dr_widget/widgets/config_file_manager/.gitignore +24 -0
- dr_widget/widgets/config_file_manager/.vscode/extensions.json +3 -0
- dr_widget/widgets/config_file_manager/README.md +89 -0
- dr_widget/widgets/config_file_manager/__init__.py +283 -0
- dr_widget/widgets/config_file_manager/components.json +16 -0
- dr_widget/widgets/config_file_manager/index.html +12 -0
- dr_widget/widgets/config_file_manager/jsrepo.json +18 -0
- dr_widget/widgets/config_file_manager/package.json +49 -0
- dr_widget/widgets/config_file_manager/postcss.config.js +6 -0
- dr_widget/widgets/config_file_manager/public/fonts/Inter-roman.var.woff2 +0 -0
- dr_widget/widgets/config_file_manager/public/vite.svg +1 -0
- dr_widget/widgets/config_file_manager/src/App.svelte +62 -0
- dr_widget/widgets/config_file_manager/src/ConfigFileManager.svelte +605 -0
- dr_widget/widgets/config_file_manager/src/app.css +134 -0
- dr_widget/widgets/config_file_manager/src/index.js +5 -0
- dr_widget/widgets/config_file_manager/src/lib/@test_state.json +20 -0
- dr_widget/widgets/config_file_manager/src/lib/Counter.svelte +10 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/BrowseConfigsPanel.svelte +137 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/ComplexJsonViewer.svelte +94 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/ConfigViewerPanel.svelte +282 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/LoadedConfigPreview.svelte +74 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/SaveConfigPanel.svelte +449 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/SelectedFileRow.svelte +38 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/SelectedFilesList.svelte +30 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/SimpleJsonViewer.svelte +405 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/badge/badge.svelte +50 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/badge/index.ts +2 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/button/button.svelte +128 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/button/index.ts +27 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/card-action.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/card-content.svelte +15 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/card-description.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/card-footer.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/card-header.svelte +23 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/card-title.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/card.svelte +23 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/index.ts +25 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-close.svelte +11 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-content.svelte +47 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-description.svelte +21 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-footer.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-header.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-overlay.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-title.svelte +21 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/index.ts +41 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-close.svelte +11 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-content.svelte +41 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-description.svelte +21 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-footer.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-header.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-nested.svelte +16 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-overlay.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-title.svelte +21 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-trigger.svelte +11 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer.svelte +16 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/index.ts +45 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/empty/empty-content.svelte +23 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/empty/empty-description.svelte +23 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/empty/empty-header.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/empty/empty-media.svelte +41 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/empty/empty-title.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/empty/empty.svelte +23 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/empty/index.ts +22 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-content.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-description.svelte +25 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-error.svelte +58 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-group.svelte +23 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-label.svelte +26 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-legend.svelte +29 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-separator.svelte +38 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-set.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-title.svelte +23 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field.svelte +53 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/index.ts +33 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/file-drop-zone/file-drop-zone.svelte +178 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/file-drop-zone/index.ts +29 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/file-drop-zone/types.ts +51 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/index.ts +34 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-actions.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-content.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-description.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-footer.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-group.svelte +21 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-header.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-media.svelte +42 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-separator.svelte +19 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-title.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item.svelte +60 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/label/index.ts +7 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/label/label.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/index.ts +13 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal-content.svelte +29 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal-description.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal-footer.svelte +29 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal-header.svelte +29 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal-title.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal-trigger.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal.svelte.ts +32 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/separator/index.ts +7 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/separator/separator.svelte +21 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/tabs/index.ts +16 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/tabs/tabs-content.svelte +17 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/tabs/tabs-list.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/tabs/tabs-trigger.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/tabs/tabs.svelte +19 -0
- dr_widget/widgets/config_file_manager/src/lib/hooks/use-file-bindings.ts +189 -0
- dr_widget/widgets/config_file_manager/src/lib/react/JsonTreeCanvas.tsx +207 -0
- dr_widget/widgets/config_file_manager/src/lib/utils/config-format.ts +113 -0
- dr_widget/widgets/config_file_manager/src/lib/utils/utils.ts +21 -0
- dr_widget/widgets/config_file_manager/src/lib/utils.ts +17 -0
- dr_widget/widgets/config_file_manager/src/main.js +7 -0
- dr_widget/widgets/config_file_manager/static/fonts/Inter-roman.var.woff2 +0 -0
- dr_widget/widgets/config_file_manager/static/index.js +9719 -0
- dr_widget/widgets/config_file_manager/static/style.css +1 -0
- dr_widget/widgets/config_file_manager/static/vite.svg +1 -0
- dr_widget/widgets/config_file_manager/svelte.config.js +8 -0
- dr_widget/widgets/config_file_manager/tailwind.config.js +12 -0
- dr_widget/widgets/config_file_manager/tsconfig.json +28 -0
- dr_widget/widgets/config_file_manager/vite.config.js +36 -0
- dr_widget-0.1.3.dist-info/METADATA +62 -0
- dr_widget-0.1.3.dist-info/RECORD +127 -0
- dr_widget-0.1.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from "$lib/components/ui/button/index.js";
|
|
3
|
+
import { Badge } from "$lib/components/ui/badge/index.js";
|
|
4
|
+
import ConfigViewerPanel from "$lib/components/file-drop/ConfigViewerPanel.svelte";
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
fileName,
|
|
8
|
+
savedAtLabel,
|
|
9
|
+
versionLabel,
|
|
10
|
+
rawContents,
|
|
11
|
+
parsedContents,
|
|
12
|
+
baselineContents,
|
|
13
|
+
dirty,
|
|
14
|
+
onClose,
|
|
15
|
+
onManage,
|
|
16
|
+
wrappedContents,
|
|
17
|
+
wrappedParsed,
|
|
18
|
+
} = $props<{
|
|
19
|
+
fileName?: string;
|
|
20
|
+
savedAtLabel?: string;
|
|
21
|
+
versionLabel?: string;
|
|
22
|
+
rawContents?: string;
|
|
23
|
+
parsedContents?: unknown;
|
|
24
|
+
baselineContents?: unknown;
|
|
25
|
+
dirty?: boolean;
|
|
26
|
+
onClose: () => void;
|
|
27
|
+
onManage?: () => void;
|
|
28
|
+
wrappedContents?: string;
|
|
29
|
+
wrappedParsed?: unknown;
|
|
30
|
+
}>();
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class="space-y-3 rounded-lg border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
|
34
|
+
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
35
|
+
<div>
|
|
36
|
+
<p class="text-base font-semibold text-zinc-900 dark:text-zinc-100">
|
|
37
|
+
{fileName ?? "Loaded configuration"}
|
|
38
|
+
</p>
|
|
39
|
+
{#if savedAtLabel || versionLabel}
|
|
40
|
+
<div class="mt-1 flex flex-wrap items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
|
|
41
|
+
{#if savedAtLabel}
|
|
42
|
+
<span>Saved {savedAtLabel}</span>
|
|
43
|
+
{/if}
|
|
44
|
+
{#if versionLabel}
|
|
45
|
+
<Badge variant="secondary" class="px-2 py-0.5 text-[0.65rem]">
|
|
46
|
+
{versionLabel}
|
|
47
|
+
</Badge>
|
|
48
|
+
{/if}
|
|
49
|
+
{#if dirty}
|
|
50
|
+
<Badge variant="secondary" class="bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-200">
|
|
51
|
+
Unsaved changes
|
|
52
|
+
</Badge>
|
|
53
|
+
{/if}
|
|
54
|
+
</div>
|
|
55
|
+
{/if}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="flex gap-2">
|
|
59
|
+
{#if onManage}
|
|
60
|
+
<Button variant="outline" onclick={onManage}>Manage Configs</Button>
|
|
61
|
+
{/if}
|
|
62
|
+
<Button variant="outline" onclick={onClose}>Close</Button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<ConfigViewerPanel
|
|
67
|
+
data={parsedContents}
|
|
68
|
+
rawJson={rawContents}
|
|
69
|
+
baselineData={baselineContents}
|
|
70
|
+
{dirty}
|
|
71
|
+
wrappedJson={wrappedContents}
|
|
72
|
+
wrappedData={wrappedParsed}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Card from "$lib/components/ui/card/index.js";
|
|
3
|
+
import { Button } from "$lib/components/ui/button/index.js";
|
|
4
|
+
import { Badge } from "$lib/components/ui/badge/index.js";
|
|
5
|
+
import ConfigViewerPanel from "$lib/components/file-drop/ConfigViewerPanel.svelte";
|
|
6
|
+
import { buildWrappedPayload } from "$lib/utils/config-format";
|
|
7
|
+
import {
|
|
8
|
+
writeBindingBaselineState,
|
|
9
|
+
writeBindingConfigFile,
|
|
10
|
+
writeBindingConfigFileDisplay,
|
|
11
|
+
writeBindingError,
|
|
12
|
+
writeBindingSavedAt,
|
|
13
|
+
writeBindingVersion,
|
|
14
|
+
type FileBinding,
|
|
15
|
+
} from "$lib/hooks/use-file-bindings";
|
|
16
|
+
|
|
17
|
+
type SaveFilePickerOptions = {
|
|
18
|
+
suggestedName?: string;
|
|
19
|
+
startIn?: BrowserFileHandle;
|
|
20
|
+
types?: Array<{ description?: string; accept: Record<string, string[]> }>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type BrowserFileHandle = {
|
|
24
|
+
readonly kind?: "file" | "directory";
|
|
25
|
+
name: string;
|
|
26
|
+
createWritable: () => Promise<{
|
|
27
|
+
write: (data: Blob | BufferSource | string) => Promise<void>;
|
|
28
|
+
close: () => Promise<void>;
|
|
29
|
+
abort?: () => Promise<void>;
|
|
30
|
+
}>;
|
|
31
|
+
getFile?: () => Promise<File>;
|
|
32
|
+
requestPermission?: (options?: { mode?: "read" | "readwrite" }) => Promise<PermissionState>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type FileSystemAccessWindow = Window &
|
|
36
|
+
typeof globalThis & {
|
|
37
|
+
showSaveFilePicker?: (options?: SaveFilePickerOptions) => Promise<BrowserFileHandle>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
bindings,
|
|
42
|
+
rawConfig,
|
|
43
|
+
baselineConfig,
|
|
44
|
+
defaultFileName = "config.json",
|
|
45
|
+
saveTargetLabel,
|
|
46
|
+
dirty = false,
|
|
47
|
+
currentVersion = "",
|
|
48
|
+
canEditVersion = false,
|
|
49
|
+
} = $props<{
|
|
50
|
+
bindings: FileBinding;
|
|
51
|
+
rawConfig?: string;
|
|
52
|
+
baselineConfig?: unknown;
|
|
53
|
+
defaultFileName?: string;
|
|
54
|
+
saveTargetLabel?: string;
|
|
55
|
+
dirty?: boolean;
|
|
56
|
+
currentVersion?: string;
|
|
57
|
+
canEditVersion?: boolean;
|
|
58
|
+
}>();
|
|
59
|
+
|
|
60
|
+
const isAbsolutePath = (value?: string): boolean => {
|
|
61
|
+
if (!value) return false;
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
if (!trimmed) return false;
|
|
64
|
+
return trimmed.startsWith("/") || trimmed.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(trimmed);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const extractFileName = (value?: string): string | undefined => {
|
|
68
|
+
if (!value) return undefined;
|
|
69
|
+
const trimmed = value.trim();
|
|
70
|
+
if (!trimmed) return undefined;
|
|
71
|
+
const parts = trimmed.split(/[\\/]+/).filter(Boolean);
|
|
72
|
+
return parts.length > 0 ? parts[parts.length - 1] : trimmed;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const dirname = (value?: string): string => {
|
|
76
|
+
if (!value) return "";
|
|
77
|
+
const trimmed = value.trim();
|
|
78
|
+
if (!trimmed) return "";
|
|
79
|
+
const windowsDrive = trimmed.match(/^[A-Za-z]:[\\/]/)?.[0];
|
|
80
|
+
const sep = trimmed.includes("\\") && !trimmed.includes("/") ? "\\" : "/";
|
|
81
|
+
const normalized = trimmed.replace(/[\\/]+/g, sep);
|
|
82
|
+
const parts = normalized.split(sep);
|
|
83
|
+
if (parts.length <= 1) {
|
|
84
|
+
return windowsDrive ?? (normalized.startsWith(sep) ? sep : "");
|
|
85
|
+
}
|
|
86
|
+
parts.pop();
|
|
87
|
+
let dir = parts.join(sep);
|
|
88
|
+
if (windowsDrive && !dir.startsWith(windowsDrive)) {
|
|
89
|
+
dir = `${windowsDrive}${dir}`;
|
|
90
|
+
} else if (normalized.startsWith(sep) && !dir.startsWith(sep)) {
|
|
91
|
+
dir = `${sep}${dir}`;
|
|
92
|
+
}
|
|
93
|
+
return dir;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const joinPath = (dir: string, leaf: string): string => {
|
|
97
|
+
if (!dir) return leaf;
|
|
98
|
+
const sep = dir.includes("\\") && !dir.includes("/") ? "\\" : "/";
|
|
99
|
+
const normalizedDir = dir === sep || dir.endsWith(sep) ? dir : `${dir}${sep}`;
|
|
100
|
+
return `${normalizedDir}${leaf}`;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const resolveAbsoluteTarget = (candidate?: string, fallback?: string | null): string => {
|
|
104
|
+
const trimmedCandidate = candidate?.trim();
|
|
105
|
+
if (!trimmedCandidate) {
|
|
106
|
+
return fallback?.trim() ?? "";
|
|
107
|
+
}
|
|
108
|
+
if (isAbsolutePath(trimmedCandidate)) {
|
|
109
|
+
return trimmedCandidate;
|
|
110
|
+
}
|
|
111
|
+
const fallbackValue = fallback?.trim() ?? "";
|
|
112
|
+
if (fallbackValue && isAbsolutePath(fallbackValue)) {
|
|
113
|
+
const parent = dirname(fallbackValue);
|
|
114
|
+
if (parent) {
|
|
115
|
+
return joinPath(parent, trimmedCandidate);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return trimmedCandidate;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
let chosenFileName = $state(defaultFileName);
|
|
122
|
+
let lastSavedMessage = $state("");
|
|
123
|
+
let saveError = $state("");
|
|
124
|
+
let saving = $state(false);
|
|
125
|
+
let versionInput = $state(currentVersion);
|
|
126
|
+
const parsedConfig = $derived.by(() => {
|
|
127
|
+
if (!rawConfig) return undefined;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(rawConfig);
|
|
131
|
+
} catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const derivedSavePath = (candidate?: string | null) => {
|
|
137
|
+
const value = candidate?.trim();
|
|
138
|
+
if (value && isAbsolutePath(value)) {
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const buildSaveMetadataEntry = (absolutePath?: string | null, label?: string | null) => {
|
|
145
|
+
const resolvedPath = derivedSavePath(absolutePath);
|
|
146
|
+
if (resolvedPath) {
|
|
147
|
+
return { save_path: resolvedPath };
|
|
148
|
+
}
|
|
149
|
+
const trimmedLabel = label?.trim();
|
|
150
|
+
if (trimmedLabel) {
|
|
151
|
+
return { save_path: trimmedLabel };
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const wrappedPreview = $derived.by(() => {
|
|
157
|
+
if (!parsedConfig || typeof parsedConfig !== "object" || Array.isArray(parsedConfig)) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const versionCandidate = versionInput?.trim() || currentVersion?.trim() || undefined;
|
|
162
|
+
const metadataPreviewLabel =
|
|
163
|
+
bindings.config_file_display || bindings.config_file || defaultFileName || chosenFileName;
|
|
164
|
+
const metadataEntry = buildSaveMetadataEntry(bindings.config_file, metadataPreviewLabel);
|
|
165
|
+
const payload = buildWrappedPayload({
|
|
166
|
+
data: parsedConfig as Record<string, unknown>,
|
|
167
|
+
version: versionCandidate,
|
|
168
|
+
savedAt: undefined,
|
|
169
|
+
metadata: metadataEntry,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
json: JSON.stringify(payload, null, 2),
|
|
174
|
+
data: payload,
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const fsWindow: FileSystemAccessWindow | undefined =
|
|
179
|
+
typeof window !== "undefined"
|
|
180
|
+
? (window as FileSystemAccessWindow)
|
|
181
|
+
: undefined;
|
|
182
|
+
|
|
183
|
+
const supportsFileSystemAccess = Boolean(fsWindow?.showSaveFilePicker);
|
|
184
|
+
const versionInputId = `config-version-${Math.random().toString(36).slice(2)}`;
|
|
185
|
+
const defaultSaveLabel = $derived.by(() => {
|
|
186
|
+
const explicitLabel = saveTargetLabel?.trim();
|
|
187
|
+
if (explicitLabel) return explicitLabel;
|
|
188
|
+
const fileName = defaultFileName?.trim();
|
|
189
|
+
if (fileName) return fileName;
|
|
190
|
+
return "config.json";
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
$effect(() => {
|
|
194
|
+
if (defaultFileName && !chosenFileName) {
|
|
195
|
+
chosenFileName = defaultFileName;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
let lastDefaultFileName = $state(defaultFileName);
|
|
200
|
+
$effect(() => {
|
|
201
|
+
if (defaultFileName !== lastDefaultFileName) {
|
|
202
|
+
chosenFileName = defaultFileName;
|
|
203
|
+
lastDefaultFileName = defaultFileName;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
$effect(() => {
|
|
208
|
+
versionInput = currentVersion ?? "";
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const buildPickerOptions = (): SaveFilePickerOptions => {
|
|
212
|
+
const options: SaveFilePickerOptions = {
|
|
213
|
+
suggestedName: chosenFileName || defaultFileName,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return options;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const pickHandle = async () => {
|
|
220
|
+
if (!supportsFileSystemAccess || !fsWindow?.showSaveFilePicker) return null;
|
|
221
|
+
try {
|
|
222
|
+
const handle = await fsWindow.showSaveFilePicker(buildPickerOptions());
|
|
223
|
+
if (handle.name) {
|
|
224
|
+
chosenFileName = handle.name;
|
|
225
|
+
}
|
|
226
|
+
saveError = "";
|
|
227
|
+
return handle;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if ((error as DOMException).name === "AbortError") {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const message = (error as Error)?.message ?? "Unable to choose file location.";
|
|
233
|
+
saveError = message;
|
|
234
|
+
writeBindingError(bindings, message);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const downloadFallback = (contents: string, fileName: string) => {
|
|
240
|
+
const blob = new Blob([contents], {
|
|
241
|
+
type: "application/json",
|
|
242
|
+
});
|
|
243
|
+
const url = URL.createObjectURL(blob);
|
|
244
|
+
const anchor = document.createElement("a");
|
|
245
|
+
anchor.href = url;
|
|
246
|
+
anchor.download = fileName;
|
|
247
|
+
anchor.click();
|
|
248
|
+
URL.revokeObjectURL(url);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const handleSave = async () => {
|
|
252
|
+
const latestRawConfig = bindings.current_state ?? rawConfig;
|
|
253
|
+
if (!latestRawConfig) {
|
|
254
|
+
saveError = "No config data available to save.";
|
|
255
|
+
writeBindingError(bindings, saveError);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const dataObject =
|
|
260
|
+
parsedConfig && typeof parsedConfig === "object" && !Array.isArray(parsedConfig)
|
|
261
|
+
? (parsedConfig as Record<string, unknown>)
|
|
262
|
+
: undefined;
|
|
263
|
+
|
|
264
|
+
if (!dataObject) {
|
|
265
|
+
saveError = "Config JSON must be an object.";
|
|
266
|
+
writeBindingError(bindings, saveError);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
saveError = "";
|
|
271
|
+
lastSavedMessage = "";
|
|
272
|
+
|
|
273
|
+
const timestamp = new Date().toISOString();
|
|
274
|
+
const trimmedInput = versionInput?.trim();
|
|
275
|
+
const fallbackVersion = currentVersion?.trim();
|
|
276
|
+
const normalizedVersion = trimmedInput || fallbackVersion || "default_v0";
|
|
277
|
+
if ((bindings.version ?? "") !== normalizedVersion) {
|
|
278
|
+
writeBindingVersion(bindings, normalizedVersion);
|
|
279
|
+
versionInput = normalizedVersion;
|
|
280
|
+
}
|
|
281
|
+
const targetFileName = chosenFileName || defaultFileName;
|
|
282
|
+
const absoluteTargetPath = resolveAbsoluteTarget(targetFileName, bindings.config_file ?? undefined);
|
|
283
|
+
const fallbackName = absoluteTargetPath || "config.json";
|
|
284
|
+
const preferredLabel = extractFileName(absoluteTargetPath) ?? targetFileName ?? fallbackName;
|
|
285
|
+
|
|
286
|
+
const buildSerializedConfig = (labelChoice?: string | null) => {
|
|
287
|
+
const metadataEntry = buildSaveMetadataEntry(absoluteTargetPath, labelChoice ?? preferredLabel);
|
|
288
|
+
const payload = buildWrappedPayload({
|
|
289
|
+
data: dataObject,
|
|
290
|
+
version: normalizedVersion,
|
|
291
|
+
savedAt: timestamp,
|
|
292
|
+
metadata: metadataEntry,
|
|
293
|
+
});
|
|
294
|
+
return {
|
|
295
|
+
metadataEntry,
|
|
296
|
+
label: labelChoice ?? preferredLabel,
|
|
297
|
+
serialized: `${JSON.stringify(payload, null, 2)}\n`,
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
let { serialized: serializedConfig, label: currentLabel } = buildSerializedConfig(preferredLabel);
|
|
302
|
+
const downloadName = currentLabel || fallbackName;
|
|
303
|
+
|
|
304
|
+
const persistSuccessMetadata = (options?: { absolutePath?: string; label?: string }) => {
|
|
305
|
+
const baselineSource = bindings.current_state ?? latestRawConfig ?? "";
|
|
306
|
+
writeBindingBaselineState(bindings, baselineSource);
|
|
307
|
+
const nextAbsolutePath = options?.absolutePath ?? absoluteTargetPath;
|
|
308
|
+
const savedLabel =
|
|
309
|
+
options?.label ?? (nextAbsolutePath ? extractFileName(nextAbsolutePath) : undefined) ?? downloadName;
|
|
310
|
+
if (nextAbsolutePath && isAbsolutePath(nextAbsolutePath)) {
|
|
311
|
+
writeBindingConfigFile(bindings, nextAbsolutePath);
|
|
312
|
+
writeBindingConfigFileDisplay(bindings, savedLabel);
|
|
313
|
+
} else if (savedLabel) {
|
|
314
|
+
writeBindingConfigFileDisplay(bindings, savedLabel);
|
|
315
|
+
}
|
|
316
|
+
writeBindingSavedAt(bindings, timestamp);
|
|
317
|
+
writeBindingError(bindings, "");
|
|
318
|
+
return savedLabel;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
if (!supportsFileSystemAccess) {
|
|
322
|
+
downloadFallback(serializedConfig, downloadName);
|
|
323
|
+
const persistedLabel = persistSuccessMetadata({ label: currentLabel });
|
|
324
|
+
lastSavedMessage = `Downloaded ${persistedLabel}`;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
saving = true;
|
|
330
|
+
const handle = await pickHandle();
|
|
331
|
+
if (!handle) {
|
|
332
|
+
saving = false;
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
await handle.requestPermission?.({ mode: "readwrite" });
|
|
337
|
+
|
|
338
|
+
currentLabel = handle.name ?? currentLabel;
|
|
339
|
+
({ serialized: serializedConfig } = buildSerializedConfig(currentLabel));
|
|
340
|
+
|
|
341
|
+
const writable = await handle.createWritable();
|
|
342
|
+
try {
|
|
343
|
+
const fileBlob = new Blob([serializedConfig], { type: "application/json" });
|
|
344
|
+
await writable.write(fileBlob);
|
|
345
|
+
await writable.close();
|
|
346
|
+
} catch (writeError) {
|
|
347
|
+
if (typeof writable.abort === "function") {
|
|
348
|
+
await writable.abort();
|
|
349
|
+
}
|
|
350
|
+
throw writeError;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const persistedLabel = persistSuccessMetadata({ label: currentLabel });
|
|
354
|
+
const savedLabel = persistedLabel ?? currentLabel ?? downloadName;
|
|
355
|
+
lastSavedMessage = `Saved ${savedLabel} at ${new Date(timestamp).toLocaleString()}`;
|
|
356
|
+
if (handle.name) {
|
|
357
|
+
chosenFileName = handle.name;
|
|
358
|
+
}
|
|
359
|
+
saveError = "";
|
|
360
|
+
} catch (error) {
|
|
361
|
+
const message = (error as Error)?.message ?? "Failed to save config.";
|
|
362
|
+
saveError = message;
|
|
363
|
+
writeBindingError(bindings, message);
|
|
364
|
+
} finally {
|
|
365
|
+
saving = false;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
</script>
|
|
369
|
+
|
|
370
|
+
<Card.Root>
|
|
371
|
+
<Card.Header>
|
|
372
|
+
<Card.Title>Save Config</Card.Title>
|
|
373
|
+
<Card.Description>
|
|
374
|
+
{#if dirty}
|
|
375
|
+
Choose where to write the modified configuration.
|
|
376
|
+
{:else}
|
|
377
|
+
Config matches the last saved version.
|
|
378
|
+
{/if}
|
|
379
|
+
</Card.Description>
|
|
380
|
+
</Card.Header>
|
|
381
|
+
<Card.Content class="space-y-4">
|
|
382
|
+
<div class="space-y-2">
|
|
383
|
+
<label class="text-sm font-medium text-zinc-600 dark:text-zinc-300" for={versionInputId}>
|
|
384
|
+
Version
|
|
385
|
+
</label>
|
|
386
|
+
<input
|
|
387
|
+
class="w-full rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-70 dark:border-zinc-700 dark:bg-zinc-900"
|
|
388
|
+
id={versionInputId}
|
|
389
|
+
value={versionInput}
|
|
390
|
+
placeholder="e.g. 1.0.0"
|
|
391
|
+
disabled={!canEditVersion || !rawConfig}
|
|
392
|
+
oninput={(event) => {
|
|
393
|
+
const nextValue = (event.target as HTMLInputElement).value;
|
|
394
|
+
versionInput = nextValue;
|
|
395
|
+
const trimmed = nextValue.trim();
|
|
396
|
+
writeBindingVersion(bindings, trimmed || "");
|
|
397
|
+
}}
|
|
398
|
+
/>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
<div class="rounded-md border border-zinc-100 bg-zinc-50 px-3 py-2 text-xs text-zinc-600 dark:border-zinc-800 dark:bg-zinc-900/60 dark:text-zinc-300">
|
|
402
|
+
{#if supportsFileSystemAccess}
|
|
403
|
+
<p>
|
|
404
|
+
Default file name:
|
|
405
|
+
<span class="font-medium text-zinc-900 dark:text-zinc-100">{defaultSaveLabel}</span>. You'll choose the folder after clicking
|
|
406
|
+
<span class="font-medium">Save</span>.
|
|
407
|
+
</p>
|
|
408
|
+
{:else}
|
|
409
|
+
<p>
|
|
410
|
+
Download name:
|
|
411
|
+
<span class="font-medium text-zinc-900 dark:text-zinc-100">{defaultSaveLabel}</span>. Your browser will download the file directly.
|
|
412
|
+
</p>
|
|
413
|
+
{/if}
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
417
|
+
{#if dirty}
|
|
418
|
+
<Badge variant="secondary" class="bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-200">
|
|
419
|
+
Unsaved changes
|
|
420
|
+
</Badge>
|
|
421
|
+
{:else}
|
|
422
|
+
<Badge variant="secondary">Up to date</Badge>
|
|
423
|
+
{/if}
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<div class="flex flex-wrap gap-2">
|
|
427
|
+
<Button onclick={handleSave} disabled={!rawConfig || saving}>
|
|
428
|
+
{saving ? "Saving…" : supportsFileSystemAccess ? "Save" : "Download"}
|
|
429
|
+
</Button>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
{#if lastSavedMessage}
|
|
433
|
+
<p class="text-sm text-emerald-600 dark:text-emerald-400">{lastSavedMessage}</p>
|
|
434
|
+
{/if}
|
|
435
|
+
|
|
436
|
+
{#if saveError}
|
|
437
|
+
<p class="text-sm text-red-500 dark:text-red-400">{saveError}</p>
|
|
438
|
+
{/if}
|
|
439
|
+
|
|
440
|
+
<ConfigViewerPanel
|
|
441
|
+
data={parsedConfig}
|
|
442
|
+
rawJson={rawConfig}
|
|
443
|
+
baselineData={baselineConfig}
|
|
444
|
+
{dirty}
|
|
445
|
+
wrappedJson={wrappedPreview?.json}
|
|
446
|
+
wrappedData={wrappedPreview?.data}
|
|
447
|
+
/>
|
|
448
|
+
</Card.Content>
|
|
449
|
+
</Card.Root>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from "$lib/components/ui/button/index.js";
|
|
3
|
+
import { displaySize } from "$lib/components/ui/file-drop-zone";
|
|
4
|
+
import type { BoundFile } from "$lib/hooks/use-file-bindings";
|
|
5
|
+
import { X } from "@lucide/svelte";
|
|
6
|
+
|
|
7
|
+
const { file, onLoad, onRemove } = $props<{
|
|
8
|
+
file: BoundFile;
|
|
9
|
+
onLoad: () => void;
|
|
10
|
+
onRemove: () => void;
|
|
11
|
+
}>();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div
|
|
15
|
+
class="flex items-center justify-between rounded-lg border border-zinc-200 bg-white p-3 text-sm shadow-sm dark:border-zinc-700 dark:bg-zinc-900"
|
|
16
|
+
>
|
|
17
|
+
<div>
|
|
18
|
+
<p class="font-medium text-zinc-800 dark:text-zinc-100">{file.name}</p>
|
|
19
|
+
<p class="text-xs text-zinc-500 dark:text-zinc-400">
|
|
20
|
+
{displaySize(file.size)} · {file.type || "unknown type"}
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="flex gap-2">
|
|
25
|
+
<Button variant="ghost" class="w-30 bg-slate-50 shadow-sm" onclick={onLoad}>
|
|
26
|
+
Load
|
|
27
|
+
</Button>
|
|
28
|
+
<Button
|
|
29
|
+
variant="ghost"
|
|
30
|
+
size="icon"
|
|
31
|
+
class="bg-red-100 shadow-sm"
|
|
32
|
+
onclick={onRemove}
|
|
33
|
+
>
|
|
34
|
+
<span class="sr-only">Remove</span>
|
|
35
|
+
<X class="size-4" />
|
|
36
|
+
</Button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { BoundFile } from "$lib/hooks/use-file-bindings";
|
|
3
|
+
import SelectedFileRow from "./SelectedFileRow.svelte";
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
files = [],
|
|
7
|
+
onLoad,
|
|
8
|
+
onRemove,
|
|
9
|
+
emptyMessage = "No files selected yet. Use the drop zone above to add some.",
|
|
10
|
+
} = $props<{
|
|
11
|
+
files?: BoundFile[];
|
|
12
|
+
onLoad: (index: number) => void;
|
|
13
|
+
onRemove: (index: number) => void;
|
|
14
|
+
emptyMessage?: string;
|
|
15
|
+
}>();
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
{#if files.length > 0}
|
|
19
|
+
<div class="space-y-3">
|
|
20
|
+
{#each files as file, index (file.name + file.size + file.type)}
|
|
21
|
+
<SelectedFileRow
|
|
22
|
+
{file}
|
|
23
|
+
onLoad={() => onLoad(index)}
|
|
24
|
+
onRemove={() => onRemove(index)}
|
|
25
|
+
/>
|
|
26
|
+
{/each}
|
|
27
|
+
</div>
|
|
28
|
+
{:else}
|
|
29
|
+
<p class="text-sm text-zinc-500 dark:text-zinc-400">{emptyMessage}</p>
|
|
30
|
+
{/if}
|