@jxsuite/studio 2.4.2 → 3.0.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/dist/chunks/{cf-settings-g5fbf2zp.js → cf-settings-zj2cj1ar.js} +6 -3
- package/dist/chunks/{content-source-k9724vcv.js → content-source-7fy9qrm0.js} +10 -9
- package/dist/chunks/{convert-to-repeater-k10dvcc7.js → convert-to-repeater-6vvx96n2.js} +9 -8
- package/dist/chunks/{extension-sections-0ada0xcw.js → extension-sections-kwzvpemr.js} +13 -12
- package/dist/chunks/{files-yew27sn9.js → files-xcybanxe.js} +14 -13
- package/dist/chunks/{format-host-wewba4nq.js → format-host-8fr3545e.js} +2 -2
- package/dist/chunks/{git-panel-2dh1aw50.js → git-panel-g01d93vc.js} +9 -8
- package/dist/chunks/{layers-panel-qnsghwwd.js → layers-panel-27essgkc.js} +12 -11
- package/dist/chunks/{publish-panel-a0pdft5w.js → publish-panel-09sy2bab.js} +8 -7
- package/dist/chunks/{quick-search-9avyc4pc.js → quick-search-sefb61v8.js} +15 -14
- package/dist/chunks/{right-panel-kcnhtc5j.js → right-panel-9r32jd5j.js} +11 -10
- package/dist/chunks/{studio-aweyr4v1.js → studio-2x9zbxmx.js} +2 -2
- package/dist/chunks/{studio-ef8t53vc.js → studio-3dkcd4c5.js} +3 -3
- package/dist/chunks/{studio-hb7c46jy.js → studio-5efrtn1m.js} +9 -7
- package/dist/chunks/{studio-v1va0vhd.js → studio-6z5eyjwn.js} +24 -25
- package/dist/chunks/{studio-ke244pam.js → studio-7p35254k.js} +33 -11
- package/dist/chunks/{studio-f5yyvqxv.js → studio-8ezdegcx.js} +13 -11
- package/dist/chunks/{studio-cz2j1c0g.js → studio-a0yp7syn.js} +55 -18
- package/dist/chunks/{studio-26sv44br.js → studio-dc8s8heb.js} +34 -23
- package/dist/chunks/{studio-jnakhg87.js → studio-g5gdqnyd.js} +4 -4
- package/dist/chunks/{studio-rqemfhz6.js → studio-hcxzyk3x.js} +2 -2
- package/dist/chunks/{studio-ws6ef6js.js → studio-hj2se583.js} +12 -10
- package/dist/chunks/{studio-pyrw8h0w.js → studio-j595e748.js} +407 -167
- package/dist/chunks/{studio-dgj7gq44.js → studio-k4jvjcfx.js} +4 -4
- package/dist/chunks/studio-kfpek536.js +28 -0
- package/dist/chunks/{studio-sccge2wf.js → studio-p5dja96s.js} +2 -2
- package/dist/chunks/{studio-0b6fs05q.js → studio-pkp61mh8.js} +5 -5
- package/dist/chunks/studio-qpfq7xhm.js +1802 -0
- package/dist/chunks/{studio-ra7ar50f.js → studio-r5xbtctf.js} +51 -1565
- package/dist/chunks/{studio-v2c5pmjj.js → studio-rh1cnamv.js} +17 -15
- package/dist/chunks/{studio-28xc8ws4.js → studio-ry1vz5sv.js} +10 -8
- package/dist/chunks/{studio-hyn3vhwr.js → studio-xntfdy1x.js} +5 -5
- package/dist/iframe-entry.js +344 -26
- package/dist/studio.js +374 -248
- package/package.json +9 -9
- package/src/browse/library-layouts.ts +3 -3
- package/src/browse/library-pane.ts +2 -2
- package/src/browse/library-preview.ts +19 -3
- package/src/canvas/asset-refs.ts +119 -0
- package/src/canvas/asset-resolve.ts +256 -0
- package/src/canvas/canvas-live-render.ts +23 -15
- package/src/canvas/iframe-entry.ts +1 -0
- package/src/canvas/iframe-host.ts +4 -0
- package/src/canvas/iframe-protocol.ts +9 -0
- package/src/canvas/iframe-render.ts +28 -10
- package/src/canvas/site-style-css.ts +2 -6
- package/src/editor/file-drop-action.ts +2 -1
- package/src/files/files.ts +2 -2
- package/src/files/media-paths.ts +37 -14
- package/src/files/media-upload.ts +47 -6
- package/src/grid/grid-idle.ts +45 -0
- package/src/grid/grid-view.ts +21 -0
- package/src/grid/schema-columns.ts +2 -1
- package/src/grid/sources/content-source.ts +24 -3
- package/src/new-project/import-tab.ts +3 -2
- package/src/panels/ai-chat/composer.ts +30 -13
- package/src/panels/frontmatter-fields.ts +6 -2
- package/src/panels/properties-panel.ts +8 -2
- package/src/panels/seo-modal.ts +5 -6
- package/src/panels/signals-panel.ts +2 -1
- package/src/platforms/cloud.ts +75 -5
- package/src/platforms/devserver.ts +5 -1
- package/src/services/ai-models.ts +101 -11
- package/src/services/ai-settings.ts +69 -71
- package/src/services/cf-settings.ts +19 -37
- package/src/services/document-assistant.ts +4 -3
- package/src/services/idle.ts +6 -1
- package/src/services/settings/definitions.ts +164 -0
- package/src/services/settings/kernel.ts +335 -0
- package/src/services/settings/write-queue.ts +116 -0
- package/src/settings/preferences-accounts.ts +6 -19
- package/src/settings/preferences-keymap.ts +8 -18
- package/src/settings/schema-field-ui.ts +2 -2
- package/src/shell.ts +6 -14
- package/src/studio.ts +32 -1
- package/src/types.ts +61 -3
- package/src/ui/ai-credentials-form.ts +52 -27
- package/src/ui/media-picker.ts +6 -11
- package/src/utils/studio-utils.ts +45 -3
- package/dist/chunks/studio-a7pzveq1.js +0 -110
- package/src/canvas/content-assets.ts +0 -263
- package/src/services/settings-store.ts +0 -99
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Content-relative asset references, resolved for the canvas.
|
|
3
|
-
*
|
|
4
|
-
* A content entry references media relative to ITSELF — `` — so the file
|
|
5
|
-
* reads correctly in a plain markdown editor and so collections sourced outside the project root
|
|
6
|
-
* still reach their media (specs/site-architecture.md §9.3). On the built site the collection
|
|
7
|
-
* loader rewrites those refs onto the content type's asset mount,
|
|
8
|
-
* `/content/<type>/images/hero.png`.
|
|
9
|
-
*
|
|
10
|
-
* Studio opens an entry as a STANDALONE document, so that loader never runs and the canvas would
|
|
11
|
-
* render the authored path verbatim — which the browser resolves against `canvas.html`, not the
|
|
12
|
-
* entry. This module applies the same mount mapping to the render document so the canvas previews
|
|
13
|
-
* the URL production actually serves.
|
|
14
|
-
*
|
|
15
|
-
* The URL math is `assetUrlFor` from `@jxsuite/schema/asset-paths` — the same function the loader
|
|
16
|
-
* and the compiler use — so the three cannot drift. It is pure string containment, which works
|
|
17
|
-
* unchanged on the project-relative paths Studio deals in.
|
|
18
|
-
*
|
|
19
|
-
* The SOURCE document is never touched: the authored relative ref is what gets serialized back to
|
|
20
|
-
* disk, and the properties panel shows the author what they wrote.
|
|
21
|
-
*
|
|
22
|
-
* One deliberate divergence from the loader: it declines to rewrite a ref whose target does not
|
|
23
|
-
* exist (`existsSync`), leaving an unresolvable path alone rather than changing its meaning. The
|
|
24
|
-
* browser has no filesystem, so this rewrites optimistically. A missing file is broken either way;
|
|
25
|
-
* pointing at the mount URL is the more informative failure, and it is the URL the built site would
|
|
26
|
-
* have attempted.
|
|
27
|
-
*
|
|
28
|
-
* @docs studio/projects/media
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import { assetUrlFor } from "@jxsuite/schema/asset-paths";
|
|
32
|
-
import { projectState } from "../store";
|
|
33
|
-
import { activeTab } from "../workspace/workspace";
|
|
34
|
-
import type { AssetMount } from "@jxsuite/schema/asset-paths";
|
|
35
|
-
import type { ContentSectionEntry } from "../types";
|
|
36
|
-
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
37
|
-
|
|
38
|
-
/** Node keys whose value is a media reference. Mirrors `ASSET_KEYS` in the content loader. */
|
|
39
|
-
const ASSET_KEYS = ["src", "poster"] as const;
|
|
40
|
-
|
|
41
|
-
/** Content type names safe in a URL path segment. Mirrors `SAFE_TYPE_NAME` in the content loader. */
|
|
42
|
-
const SAFE_TYPE_NAME = /^[\w.~-]+$/;
|
|
43
|
-
|
|
44
|
-
/** The project.json section key content types live under; also their URL namespace. */
|
|
45
|
-
const SECTION_KEY = "content";
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* A value that is already a URL, a template, or a fragment — never a content-relative file. Mirrors
|
|
49
|
-
* `isNonRelativeRef` in the content loader.
|
|
50
|
-
*/
|
|
51
|
-
function isNonRelativeRef(value: string): boolean {
|
|
52
|
-
return (
|
|
53
|
-
value === "" ||
|
|
54
|
-
value.startsWith("/") ||
|
|
55
|
-
value.startsWith("#") ||
|
|
56
|
-
value.includes("${") ||
|
|
57
|
-
/^[a-z][\w+.-]*:/i.test(value)
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Strip `./` and any trailing slash from a configured `source`, the way collectionDirs does. */
|
|
62
|
-
function normalizeSource(source: string): string {
|
|
63
|
-
return source.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Resolve `ref` against `dir` in a pure, POSIX-ish way — no `node:path`, and no leading-slash
|
|
68
|
-
* assumption, because Studio's paths are project-relative. Returns null when the result would climb
|
|
69
|
-
* above the project root (`../` past the top), which can never name a mounted file.
|
|
70
|
-
*/
|
|
71
|
-
export function resolveRelativePath(dir: string, ref: string): string | null {
|
|
72
|
-
const segments = dir === "" || dir === "." ? [] : dir.replaceAll("\\", "/").split("/");
|
|
73
|
-
const out = segments.filter((s) => s !== "" && s !== ".");
|
|
74
|
-
for (const segment of ref.replaceAll("\\", "/").split("/")) {
|
|
75
|
-
if (segment === "" || segment === ".") {
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
if (segment === "..") {
|
|
79
|
-
if (out.length === 0) {
|
|
80
|
-
return null; // Escapes the project root.
|
|
81
|
-
}
|
|
82
|
-
out.pop();
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
out.push(segment);
|
|
86
|
-
}
|
|
87
|
-
return out.join("/");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** The directory portion of a project-relative file path (`""` for a root-level file). */
|
|
91
|
-
export function dirOf(path: string): string {
|
|
92
|
-
const normalized = path.replaceAll("\\", "/");
|
|
93
|
-
const slash = normalized.lastIndexOf("/");
|
|
94
|
-
return slash === -1 ? "" : normalized.slice(0, slash);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* The asset mount for the content type that owns `documentPath`, or null when the document is not a
|
|
99
|
-
* directory-backed content entry.
|
|
100
|
-
*
|
|
101
|
-
* Only in-project sources can match: an entry Studio can address project-relatively is by
|
|
102
|
-
* definition inside the project, and a source pointing elsewhere (`../../docs`) names files the
|
|
103
|
-
* file tree cannot reach in the first place.
|
|
104
|
-
*/
|
|
105
|
-
export function contentMountFor(
|
|
106
|
-
documentPath: string | null | undefined,
|
|
107
|
-
content: Record<string, ContentSectionEntry> | null | undefined,
|
|
108
|
-
): AssetMount | null {
|
|
109
|
-
if (!documentPath || !content) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
const path = documentPath.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
113
|
-
let best: AssetMount | null = null;
|
|
114
|
-
for (const [name, def] of Object.entries(content)) {
|
|
115
|
-
if (!def?.source || !SAFE_TYPE_NAME.test(name)) {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
const dir = normalizeSource(def.source);
|
|
119
|
-
// A single-file source is not a collection directory — its siblings are not its assets.
|
|
120
|
-
if (dir === "" || dir.startsWith("..") || /\.[a-z0-9]+$/i.test(dir)) {
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (path.startsWith(`${dir}/`) && (!best || dir.length > best.dir.length)) {
|
|
124
|
-
best = { dir, urlPrefix: `/${SECTION_KEY}/${name}` };
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return best;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* The mounted URL for one authored reference, or null to leave it as written. A ref is rewritten
|
|
132
|
-
* only when it is relative and lands inside the collection directory — anything else (an absolute
|
|
133
|
-
* URL, a project-root path, a bound template, a `../` escape out of the collection) keeps the
|
|
134
|
-
* meaning it already had.
|
|
135
|
-
*/
|
|
136
|
-
export function mountedRefFor(value: string, entryDir: string, mount: AssetMount): string | null {
|
|
137
|
-
if (isNonRelativeRef(value)) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
const bare = value.split("#")[0]!.split("?")[0]!;
|
|
141
|
-
let decoded: string;
|
|
142
|
-
try {
|
|
143
|
-
decoded = decodeURIComponent(bare);
|
|
144
|
-
} catch {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
const target = resolveRelativePath(entryDir, decoded);
|
|
148
|
-
if (target === null) {
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
const url = assetUrlFor([mount], target);
|
|
152
|
-
if (!url) {
|
|
153
|
-
return null; // Outside the collection — not the mount's to publish.
|
|
154
|
-
}
|
|
155
|
-
return `${url}${value.slice(bare.length)}`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Rewrite one node's media refs, returning the node unchanged when nothing applies.
|
|
160
|
-
*
|
|
161
|
-
* PURE REBUILD: a node with a rewritten ref is replaced by a shallow copy, and so is every ancestor
|
|
162
|
-
* on the path to it. Untouched subtrees keep their original references, so the render doc still
|
|
163
|
-
* shares the vast majority of its nodes with the source doc.
|
|
164
|
-
*/
|
|
165
|
-
function rewriteNode(node: JxMutableNode, entryDir: string, mount: AssetMount): JxMutableNode {
|
|
166
|
-
const record = node as unknown as Record<string, unknown>;
|
|
167
|
-
/** Accumulated changed keys; stays null (and the node is returned as-is) when nothing applies. */
|
|
168
|
-
let patch: Record<string, unknown> | null = null;
|
|
169
|
-
|
|
170
|
-
for (const key of ASSET_KEYS) {
|
|
171
|
-
const value = record[key];
|
|
172
|
-
if (typeof value === "string") {
|
|
173
|
-
const rewritten = mountedRefFor(value, entryDir, mount);
|
|
174
|
-
if (rewritten) {
|
|
175
|
-
patch ??= {};
|
|
176
|
-
patch[key] = rewritten;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const attributes = record.attributes as Record<string, unknown> | undefined;
|
|
182
|
-
if (attributes) {
|
|
183
|
-
let nextAttrs: Record<string, unknown> | null = null;
|
|
184
|
-
for (const key of ASSET_KEYS) {
|
|
185
|
-
const value = attributes[key];
|
|
186
|
-
if (typeof value === "string") {
|
|
187
|
-
const rewritten = mountedRefFor(value, entryDir, mount);
|
|
188
|
-
if (rewritten) {
|
|
189
|
-
nextAttrs ??= { ...attributes };
|
|
190
|
-
nextAttrs[key] = rewritten;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
if (nextAttrs) {
|
|
195
|
-
patch ??= {};
|
|
196
|
-
patch.attributes = nextAttrs;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const children = record.children as unknown[] | undefined;
|
|
201
|
-
if (Array.isArray(children)) {
|
|
202
|
-
let nextChildren: unknown[] | null = null;
|
|
203
|
-
for (const [i, child] of children.entries()) {
|
|
204
|
-
if (!child || typeof child !== "object") {
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
const rewrittenChild = rewriteNode(child as JxMutableNode, entryDir, mount);
|
|
208
|
-
if (rewrittenChild !== child) {
|
|
209
|
-
nextChildren ??= [...children];
|
|
210
|
-
nextChildren[i] = rewrittenChild;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (nextChildren) {
|
|
214
|
-
patch ??= {};
|
|
215
|
-
patch.children = nextChildren;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return patch ? ({ ...node, ...patch } as JxMutableNode) : node;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Map a content entry's relative media refs onto its content type's asset mount, for rendering.
|
|
224
|
-
*
|
|
225
|
-
* Returns `doc` itself when the document is not a content entry or carries no rewritable ref, so
|
|
226
|
-
* the common case costs one walk and allocates nothing.
|
|
227
|
-
*/
|
|
228
|
-
export function rewriteContentAssets(
|
|
229
|
-
doc: JxMutableNode,
|
|
230
|
-
documentPath: string | null | undefined,
|
|
231
|
-
content: Record<string, ContentSectionEntry> | null | undefined,
|
|
232
|
-
): JxMutableNode {
|
|
233
|
-
const mount = contentMountFor(documentPath, content);
|
|
234
|
-
if (!mount) {
|
|
235
|
-
return doc;
|
|
236
|
-
}
|
|
237
|
-
return rewriteNode(doc, dirOf(documentPath!.replaceAll("\\", "/").replace(/^\.\//, "")), mount);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* The src a PARENT-REALM preview (a media-picker thumbnail) should load for an authored value.
|
|
242
|
-
*
|
|
243
|
-
* The canvas gets its mapping through {@link rewriteContentAssets} on the render doc, but panel
|
|
244
|
-
* chrome renders in the parent document, where a content-relative value would resolve against
|
|
245
|
-
* `index.html` and 404 exactly as it did in the canvas. Same mapping, same rule — resolved against
|
|
246
|
-
* the ACTIVE tab, which is the entry whose field is being edited.
|
|
247
|
-
*
|
|
248
|
-
* Non-content documents and already-absolute values pass through untouched.
|
|
249
|
-
*/
|
|
250
|
-
export function previewAssetSrc(value: string): string {
|
|
251
|
-
if (!value) {
|
|
252
|
-
return value;
|
|
253
|
-
}
|
|
254
|
-
const documentPath = activeTab.value?.documentPath;
|
|
255
|
-
const mount = contentMountFor(
|
|
256
|
-
documentPath,
|
|
257
|
-
projectState?.projectConfig?.content as Record<string, ContentSectionEntry> | undefined,
|
|
258
|
-
);
|
|
259
|
-
if (!mount) {
|
|
260
|
-
return value;
|
|
261
|
-
}
|
|
262
|
-
return mountedRefFor(value, dirOf(documentPath!), mount) ?? value;
|
|
263
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/// <reference lib="dom" />
|
|
2
|
-
/**
|
|
3
|
-
* Settings-store.js — sync of user settings between localStorage and the platform backend.
|
|
4
|
-
*
|
|
5
|
-
* LocalStorage stays the synchronous source of truth for every consumer (ai-settings getters read
|
|
6
|
-
* it directly). On platforms with a backend user-settings store (desktop/chromium — where
|
|
7
|
-
* localStorage does not roam across browser profiles), the persisted keys are hydrated from the
|
|
8
|
-
* backend at boot and written through on every change. The dev server omits the platform methods
|
|
9
|
-
* and settings remain localStorage-only.
|
|
10
|
-
*
|
|
11
|
-
* @license MIT
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { getPlatform, hasPlatform } from "../platform";
|
|
15
|
-
|
|
16
|
-
/** The localStorage keys mirrored into the platform's user-settings store. */
|
|
17
|
-
export const PERSISTED_SETTINGS_KEYS = [
|
|
18
|
-
"jx.ai.openaiKey",
|
|
19
|
-
"jx.ai.baseUrl",
|
|
20
|
-
"jx.ai.model",
|
|
21
|
-
"jx.cf.token",
|
|
22
|
-
"jx.cf.accountId",
|
|
23
|
-
] as const;
|
|
24
|
-
|
|
25
|
-
/** Read a localStorage value defensively, treating unavailable/throwing storage as empty. */
|
|
26
|
-
function readLocal(key: string): string {
|
|
27
|
-
try {
|
|
28
|
-
return globalThis.localStorage?.getItem(key) ?? "";
|
|
29
|
-
} catch {
|
|
30
|
-
return "";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Pull the persisted settings from the backend store into localStorage. Call once after the
|
|
36
|
-
* platform is registered; a no-op on platforms without a settings store (dev server).
|
|
37
|
-
*
|
|
38
|
-
* Migration: keys the backend does not know yet but localStorage does (an existing user upgrading
|
|
39
|
-
* from localStorage-only builds) are pushed back to the backend once, fire-and-forget.
|
|
40
|
-
*/
|
|
41
|
-
export async function hydrateSettings(): Promise<void> {
|
|
42
|
-
if (!hasPlatform()) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const platform = getPlatform();
|
|
46
|
-
if (!platform.getSettings) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
let stored: Record<string, string>;
|
|
50
|
-
try {
|
|
51
|
-
stored = await platform.getSettings();
|
|
52
|
-
} catch {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const merged: Record<string, string> = { ...stored };
|
|
56
|
-
let needsMigration = false;
|
|
57
|
-
for (const key of PERSISTED_SETTINGS_KEYS) {
|
|
58
|
-
const backendValue = stored[key];
|
|
59
|
-
if (backendValue) {
|
|
60
|
-
try {
|
|
61
|
-
globalThis.localStorage?.setItem(key, backendValue);
|
|
62
|
-
} catch {
|
|
63
|
-
/* Storage unavailable — consumers fall back to defaults. */
|
|
64
|
-
}
|
|
65
|
-
} else {
|
|
66
|
-
const localValue = readLocal(key);
|
|
67
|
-
if (localValue) {
|
|
68
|
-
merged[key] = localValue;
|
|
69
|
-
needsMigration = true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (needsMigration && platform.saveSettings) {
|
|
74
|
-
void platform.saveSettings(merged).catch(() => {});
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Write the current values of the persisted keys through to the backend store, fire-and-forget.
|
|
80
|
-
* Call after mutating any of {@link PERSISTED_SETTINGS_KEYS} in localStorage; a no-op on platforms
|
|
81
|
-
* without a settings store (dev server).
|
|
82
|
-
*/
|
|
83
|
-
export function persistSettings(): void {
|
|
84
|
-
if (!hasPlatform()) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const platform = getPlatform();
|
|
88
|
-
if (!platform.saveSettings) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const settings: Record<string, string> = {};
|
|
92
|
-
for (const key of PERSISTED_SETTINGS_KEYS) {
|
|
93
|
-
const value = readLocal(key);
|
|
94
|
-
if (value) {
|
|
95
|
-
settings[key] = value;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
void platform.saveSettings(settings).catch(() => {});
|
|
99
|
-
}
|