@wildwinter/app-shell 0.23.0 → 0.25.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/app-store.cjs +28 -7
- package/dist/app-store.cjs.map +1 -1
- package/dist/app-store.d.cts +41 -10
- package/dist/app-store.d.ts +41 -10
- package/dist/app-store.js +28 -7
- package/dist/app-store.js.map +1 -1
- package/package.json +1 -1
package/dist/app-store.cjs
CHANGED
|
@@ -13,10 +13,20 @@ function createAppStore(opts) {
|
|
|
13
13
|
} catch {
|
|
14
14
|
}
|
|
15
15
|
const state = {
|
|
16
|
-
recents: [],
|
|
17
16
|
panes: opts.panes ?? {},
|
|
18
17
|
windows: {},
|
|
19
18
|
...loaded,
|
|
19
|
+
// Migration: recents used to be bare paths. Keep them, without names, rather
|
|
20
|
+
// than emptying somebody's Open Recent menu on an update.
|
|
21
|
+
recents: (loaded.recents ?? []).map((r) => typeof r === "string" ? { path: r } : r),
|
|
22
|
+
// Migration: a settings file written before places were keyed by project has
|
|
23
|
+
// a single `place` belonging to `lastProject`. Carry it over rather than
|
|
24
|
+
// dropping it, or everyone's first launch after this update forgets where
|
|
25
|
+
// they were, which is the exact complaint the change is meant to fix.
|
|
26
|
+
places: {
|
|
27
|
+
...loaded.places ?? {},
|
|
28
|
+
...loaded.lastProject !== void 0 && loaded.place !== void 0 ? { [loaded.lastProject]: loaded.place } : {}
|
|
29
|
+
},
|
|
20
30
|
// The app's slice merges FIELD BY FIELD, so a key added in a new version
|
|
21
31
|
// arrives with its default rather than being absent for everybody who has
|
|
22
32
|
// run the app before.
|
|
@@ -38,22 +48,33 @@ function createAppStore(opts) {
|
|
|
38
48
|
Object.assign(state.app, patch);
|
|
39
49
|
save();
|
|
40
50
|
},
|
|
41
|
-
touchProject(path) {
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
touchProject(path, name) {
|
|
52
|
+
const known = state.recents.find((r) => r.path === path);
|
|
53
|
+
const entry = { path, ...name ?? known?.name ? { name: name ?? known?.name } : {} };
|
|
54
|
+
state.recents = [entry, ...state.recents.filter((r) => r.path !== path)].slice(0, maxRecents);
|
|
44
55
|
state.lastProject = path;
|
|
56
|
+
for (const key of Object.keys(state.places)) {
|
|
57
|
+
if (!state.recents.some((r) => r.path === key)) delete state.places[key];
|
|
58
|
+
}
|
|
45
59
|
save();
|
|
46
60
|
},
|
|
47
61
|
forgetProject(path) {
|
|
48
|
-
state.recents = state.recents.filter((
|
|
62
|
+
state.recents = state.recents.filter((r) => r.path !== path);
|
|
49
63
|
if (state.lastProject === path) delete state.lastProject;
|
|
64
|
+
delete state.places[path];
|
|
50
65
|
save();
|
|
51
66
|
},
|
|
52
67
|
setPlace(place) {
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
const path = state.lastProject;
|
|
69
|
+
if (path === void 0) return;
|
|
70
|
+
if (JSON.stringify(state.places[path]) === JSON.stringify(place)) return;
|
|
71
|
+
state.places[path] = place;
|
|
55
72
|
save();
|
|
56
73
|
},
|
|
74
|
+
placeOf: (path) => {
|
|
75
|
+
const key = path ?? state.lastProject;
|
|
76
|
+
return key === void 0 ? void 0 : structuredClone(state.places[key]);
|
|
77
|
+
},
|
|
57
78
|
setPanes(panes) {
|
|
58
79
|
state.panes = { ...state.panes, ...panes };
|
|
59
80
|
save();
|
package/dist/app-store.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/app-store.ts"],"names":["join","readFileSync","mkdirSync","writeFileSync","renameSync"],"mappings":"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","file":"app-store.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The user's settings for THIS app: a small JSON file in the app's userData.\n//\n// Every app in the family has one, and both of the two that exist wrote it\n// separately: the same tolerant read, the same merged defaults, the same capped\n// recents, the same panes, the same per-tool-window bounds and pin, the same\n// identity. That is the definition of shell work.\n//\n// WHAT BELONGS HERE is settings that follow the PERSON, not the project: where\n// their windows were, which panes they like open, what they are called, what\n// they had open last. Project content never comes near this file - it is not\n// version controlled, it is not shared, and losing it should cost an author\n// nothing but a re-arranged window.\n//\n// The shell owns the CORE it can reason about and hands the app one slice of\n// its own (`app`) for everything else - a theme value, a view mode, whatever it\n// has. Two stores in one file, one atomic write, so a crash cannot leave an app\n// with half its settings.\n//\n// Electron-free on purpose: it takes a directory. That keeps it unit-testable\n// and lets a test point it at a temp dir.\n// ---------------------------------------------------------------------------\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/** Who is at the keyboard. Never in a project: see identity.ts. */\nexport interface Identity { name: string; email?: string }\n\n/** A remembered window rectangle. Absent = the app's default, centred. */\nexport interface WindowBounds { x?: number; y?: number; width: number; height: number }\n\n/** One helper window's remembered state. */\nexport interface WindowState { bounds?: WindowBounds; pinned?: boolean }\n\n/** Side panes: open/closed, and any dragged width. */\nexport interface PaneState {\n nav?: boolean;\n inspector?: boolean;\n navW?: number;\n inspW?: number;\n}\n\n/**\n * The part every app has.\n *\n * `place` is deliberately opaque: \"open where you left off\" is a family idea,\n * but WHERE is a scene and a caret in one app and a box, a tab and a card in\n * another. The shell stores it and clears it when the project changes, which is\n * the one rule about it that is not app-specific.\n */\nexport interface AppSettingsCore<Place> {\n recents: string[];\n lastProject?: string;\n place?: Place;\n panes: PaneState;\n identity?: Identity;\n /** Keyed by whatever the app calls each window (\"board\", \"search\"). */\n windows: Record<string, WindowState>;\n}\n\nexport type AppSettings<Place, App> = AppSettingsCore<Place> & { app: App };\n\nexport interface AppStoreOptions<Place, App> {\n /** The app's userData directory. */\n dir: string;\n /** The file inside it. One per app; two apps never share. */\n fileName?: string;\n /** The app's own slice, as it should read on a first run. */\n defaults: App;\n /** How the panes should sit before anybody touches them. */\n panes?: PaneState;\n maxRecents?: number;\n}\n\nexport interface AppStore<Place, App> {\n /** A copy, always: nothing outside can mutate the state behind the store's back. */\n get(): AppSettings<Place, App>;\n /** Merge into the app's own slice and save. */\n patchApp(patch: Partial<App>): void;\n /** Record an opened project: front of recents, deduped, capped. */\n touchProject(path: string): void;\n /** Drop one that failed to open (moved, deleted). */\n forgetProject(path: string): void;\n /** Where the author was. Compared before writing: this is called on every\n * navigation and must not mean a disk write per click. */\n setPlace(place: Place): void;\n setPanes(panes: PaneState): void;\n setIdentity(identity: Identity): void;\n /** One window's bounds and/or pin, merged with what it already had. */\n setWindow(key: string, state: WindowState): void;\n}\n\nexport function createAppStore<Place, App extends object>(\n opts: AppStoreOptions<Place, App>,\n): AppStore<Place, App> {\n const file = join(opts.dir, opts.fileName ?? \"app-settings.json\");\n const maxRecents = opts.maxRecents ?? 8;\n\n let loaded: Partial<AppSettings<Place, App>> = {};\n try {\n loaded = JSON.parse(readFileSync(file, \"utf8\")) as Partial<AppSettings<Place, App>>;\n } catch {\n // First run, or a file we cannot read. Defaults, never a crash: settings\n // are not worth failing to start over.\n }\n\n const state: AppSettings<Place, App> = {\n recents: [],\n panes: opts.panes ?? {},\n windows: {},\n ...loaded,\n // The app's slice merges FIELD BY FIELD, so a key added in a new version\n // arrives with its default rather than being absent for everybody who has\n // run the app before.\n app: { ...opts.defaults, ...(loaded.app ?? {}) },\n };\n\n const save = (): void => {\n try {\n mkdirSync(opts.dir, { recursive: true });\n // Write beside, then rename: a rename is atomic on every platform this\n // ships to, so a crash mid-write cannot leave a half-written settings\n // file - which both apps' hand-written stores could.\n const temp = `${file}.tmp`;\n writeFileSync(temp, `${JSON.stringify(state, null, 2)}\\n`, \"utf8\");\n renameSync(temp, file);\n } catch {\n // A read-only home directory, a full disk: the app carries on with the\n // settings it has in memory rather than dying over a preference.\n }\n };\n\n return {\n get: () => structuredClone(state),\n patchApp(patch) {\n Object.assign(state.app, patch);\n save();\n },\n touchProject(path) {\n // A DIFFERENT project invalidates the remembered place; the same one must\n // keep it, or \"open where you left off\" could never work, since reopening\n // goes through here.\n if (state.lastProject !== path) delete state.place;\n state.recents = [path, ...state.recents.filter((p) => p !== path)].slice(0, maxRecents);\n state.lastProject = path;\n save();\n },\n forgetProject(path) {\n state.recents = state.recents.filter((p) => p !== path);\n if (state.lastProject === path) delete state.lastProject;\n save();\n },\n setPlace(place) {\n if (JSON.stringify(state.place) === JSON.stringify(place)) return;\n state.place = place;\n save();\n },\n setPanes(panes) {\n state.panes = { ...state.panes, ...panes };\n save();\n },\n setIdentity(identity) {\n state.identity = identity;\n save();\n },\n setWindow(key, next) {\n state.windows[key] = { ...state.windows[key], ...next };\n save();\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/app-store.ts"],"names":["join","readFileSync","mkdirSync","writeFileSync","renameSync"],"mappings":"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","file":"app-store.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The user's settings for THIS app: a small JSON file in the app's userData.\n//\n// Every app in the family has one, and both of the two that exist wrote it\n// separately: the same tolerant read, the same merged defaults, the same capped\n// recents, the same panes, the same per-tool-window bounds and pin, the same\n// identity. That is the definition of shell work.\n//\n// WHAT BELONGS HERE is settings that follow the PERSON, not the project: where\n// their windows were, which panes they like open, what they are called, what\n// they had open last. Project content never comes near this file - it is not\n// version controlled, it is not shared, and losing it should cost an author\n// nothing but a re-arranged window.\n//\n// The shell owns the CORE it can reason about and hands the app one slice of\n// its own (`app`) for everything else - a theme value, a view mode, whatever it\n// has. Two stores in one file, one atomic write, so a crash cannot leave an app\n// with half its settings.\n//\n// Electron-free on purpose: it takes a directory. That keeps it unit-testable\n// and lets a test point it at a temp dir.\n// ---------------------------------------------------------------------------\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/** Who is at the keyboard. Never in a project: see identity.ts. */\nexport interface Identity { name: string; email?: string }\n\n/** A remembered window rectangle. Absent = the app's default, centred. */\nexport interface WindowBounds { x?: number; y?: number; width: number; height: number }\n\n/** One helper window's remembered state. */\nexport interface WindowState { bounds?: WindowBounds; pinned?: boolean }\n\n/** Side panes: open/closed, and any dragged width. */\nexport interface PaneState {\n nav?: boolean;\n inspector?: boolean;\n navW?: number;\n inspW?: number;\n}\n\n/**\n * The part every app has.\n *\n * `places` is deliberately opaque: \"open where you left off\" is a family idea,\n * but WHERE is a scene and a caret in one app and a box, a tab and a card in\n * another.\n *\n * It is keyed BY PROJECT, and that is the interesting part. This held a single\n * `place` and dropped it whenever a different project was opened, on the\n * reasoning that clearing it was the one rule that could not be app-specific.\n * That reasoning was wrong, and Patterpad disproved it by having done the other\n * thing for a long time: it remembers the line you were on in every project you\n * have opened. With one slot, working on A, glancing at B and coming back to A\n * lands you at the top of A, having forgotten. Nobody wants that, and no editor\n * outside this package does it.\n */\n/**\n * A project in the Open Recent menu.\n *\n * The NAME is here rather than being derived from the path, because a project's\n * display name is what its file says it is, not what its folder is called: two\n * projects can sit in folders called `draft`, and one can be renamed without\n * moving. Every app in the family shows this menu, so both halves are shared.\n *\n * Optional because the path is the identity and the name is only how it reads.\n * An entry recorded before a name was known still works.\n */\nexport interface RecentProject {\n path: string;\n name?: string;\n}\n\nexport interface AppSettingsCore<Place> {\n recents: RecentProject[];\n lastProject?: string;\n /** Project path -> where the author was in it. Capped with `recents`, so a\n * project forgotten from the list stops carrying a place around too. */\n places: Record<string, Place>;\n panes: PaneState;\n identity?: Identity;\n /** Keyed by whatever the app calls each window (\"board\", \"search\"). */\n windows: Record<string, WindowState>;\n}\n\nexport type AppSettings<Place, App> = AppSettingsCore<Place> & { app: App };\n\nexport interface AppStoreOptions<Place, App> {\n /** The app's userData directory. */\n dir: string;\n /** The file inside it. One per app; two apps never share. */\n fileName?: string;\n /** The app's own slice, as it should read on a first run. */\n defaults: App;\n /** How the panes should sit before anybody touches them. */\n panes?: PaneState;\n maxRecents?: number;\n}\n\nexport interface AppStore<Place, App> {\n /** A copy, always: nothing outside can mutate the state behind the store's back. */\n get(): AppSettings<Place, App>;\n /** Merge into the app's own slice and save. */\n patchApp(patch: Partial<App>): void;\n /** Record an opened project: front of recents, deduped, capped. The name is\n * refreshed each time, so renaming a project updates the menu on next open;\n * omitting it keeps whatever was known before rather than blanking it. */\n touchProject(path: string, name?: string): void;\n /** Drop one that failed to open (moved, deleted). */\n forgetProject(path: string): void;\n /** Where the author was in the CURRENT project. Compared before writing: this\n * is called on every navigation and must not mean a disk write per click.\n * A no-op with no project open, since there is nothing to key it against. */\n setPlace(place: Place): void;\n /** Where the author was in a project, or undefined if it was never opened or\n * has aged out of `recents`. Defaults to the current one. */\n placeOf(path?: string): Place | undefined;\n setPanes(panes: PaneState): void;\n setIdentity(identity: Identity): void;\n /** One window's bounds and/or pin, merged with what it already had. */\n setWindow(key: string, state: WindowState): void;\n}\n\nexport function createAppStore<Place, App extends object>(\n opts: AppStoreOptions<Place, App>,\n): AppStore<Place, App> {\n const file = join(opts.dir, opts.fileName ?? \"app-settings.json\");\n const maxRecents = opts.maxRecents ?? 8;\n\n let loaded: Partial<AppSettings<Place, App>> = {};\n try {\n loaded = JSON.parse(readFileSync(file, \"utf8\")) as Partial<AppSettings<Place, App>>;\n } catch {\n // First run, or a file we cannot read. Defaults, never a crash: settings\n // are not worth failing to start over.\n }\n\n const state: AppSettings<Place, App> = {\n panes: opts.panes ?? {},\n windows: {},\n ...loaded,\n // Migration: recents used to be bare paths. Keep them, without names, rather\n // than emptying somebody's Open Recent menu on an update.\n recents: (loaded.recents ?? []).map((r) =>\n typeof r === \"string\" ? { path: r } : r) as RecentProject[],\n // Migration: a settings file written before places were keyed by project has\n // a single `place` belonging to `lastProject`. Carry it over rather than\n // dropping it, or everyone's first launch after this update forgets where\n // they were, which is the exact complaint the change is meant to fix.\n places: {\n ...(loaded.places ?? {}),\n ...(loaded.lastProject !== undefined && (loaded as { place?: Place }).place !== undefined\n ? { [loaded.lastProject]: (loaded as { place?: Place }).place as Place }\n : {}),\n },\n // The app's slice merges FIELD BY FIELD, so a key added in a new version\n // arrives with its default rather than being absent for everybody who has\n // run the app before.\n app: { ...opts.defaults, ...(loaded.app ?? {}) },\n };\n\n const save = (): void => {\n try {\n mkdirSync(opts.dir, { recursive: true });\n // Write beside, then rename: a rename is atomic on every platform this\n // ships to, so a crash mid-write cannot leave a half-written settings\n // file - which both apps' hand-written stores could.\n const temp = `${file}.tmp`;\n writeFileSync(temp, `${JSON.stringify(state, null, 2)}\\n`, \"utf8\");\n renameSync(temp, file);\n } catch {\n // A read-only home directory, a full disk: the app carries on with the\n // settings it has in memory rather than dying over a preference.\n }\n };\n\n return {\n get: () => structuredClone(state),\n patchApp(patch) {\n Object.assign(state.app, patch);\n save();\n },\n touchProject(path, name) {\n const known = state.recents.find((r) => r.path === path);\n // An omitted name keeps the one we had: reopening from a route that does\n // not know the name must not blank the menu entry.\n const entry: RecentProject = { path, ...(name ?? known?.name ? { name: name ?? known?.name } : {}) };\n state.recents = [entry, ...state.recents.filter((r) => r.path !== path)].slice(0, maxRecents);\n state.lastProject = path;\n // Places follow recents: one that has aged off the list should not keep a\n // place in the file for ever. Nothing is dropped for merely opening\n // something else, which is the whole point of keying them.\n for (const key of Object.keys(state.places)) {\n if (!state.recents.some((r) => r.path === key)) delete state.places[key];\n }\n save();\n },\n forgetProject(path) {\n state.recents = state.recents.filter((r) => r.path !== path);\n if (state.lastProject === path) delete state.lastProject;\n delete state.places[path]; // it moved or went: its place is meaningless now\n save();\n },\n setPlace(place) {\n const path = state.lastProject;\n if (path === undefined) return; // no project open: nothing to key it against\n if (JSON.stringify(state.places[path]) === JSON.stringify(place)) return;\n state.places[path] = place;\n save();\n },\n placeOf: (path) => {\n const key = path ?? state.lastProject;\n return key === undefined ? undefined : structuredClone(state.places[key]);\n },\n setPanes(panes) {\n state.panes = { ...state.panes, ...panes };\n save();\n },\n setIdentity(identity) {\n state.identity = identity;\n save();\n },\n setWindow(key, next) {\n state.windows[key] = { ...state.windows[key], ...next };\n save();\n },\n };\n}\n"]}
|
package/dist/app-store.d.cts
CHANGED
|
@@ -25,15 +25,40 @@ interface PaneState {
|
|
|
25
25
|
/**
|
|
26
26
|
* The part every app has.
|
|
27
27
|
*
|
|
28
|
-
* `
|
|
28
|
+
* `places` is deliberately opaque: "open where you left off" is a family idea,
|
|
29
29
|
* but WHERE is a scene and a caret in one app and a box, a tab and a card in
|
|
30
|
-
* another.
|
|
31
|
-
*
|
|
30
|
+
* another.
|
|
31
|
+
*
|
|
32
|
+
* It is keyed BY PROJECT, and that is the interesting part. This held a single
|
|
33
|
+
* `place` and dropped it whenever a different project was opened, on the
|
|
34
|
+
* reasoning that clearing it was the one rule that could not be app-specific.
|
|
35
|
+
* That reasoning was wrong, and Patterpad disproved it by having done the other
|
|
36
|
+
* thing for a long time: it remembers the line you were on in every project you
|
|
37
|
+
* have opened. With one slot, working on A, glancing at B and coming back to A
|
|
38
|
+
* lands you at the top of A, having forgotten. Nobody wants that, and no editor
|
|
39
|
+
* outside this package does it.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* A project in the Open Recent menu.
|
|
43
|
+
*
|
|
44
|
+
* The NAME is here rather than being derived from the path, because a project's
|
|
45
|
+
* display name is what its file says it is, not what its folder is called: two
|
|
46
|
+
* projects can sit in folders called `draft`, and one can be renamed without
|
|
47
|
+
* moving. Every app in the family shows this menu, so both halves are shared.
|
|
48
|
+
*
|
|
49
|
+
* Optional because the path is the identity and the name is only how it reads.
|
|
50
|
+
* An entry recorded before a name was known still works.
|
|
32
51
|
*/
|
|
52
|
+
interface RecentProject {
|
|
53
|
+
path: string;
|
|
54
|
+
name?: string;
|
|
55
|
+
}
|
|
33
56
|
interface AppSettingsCore<Place> {
|
|
34
|
-
recents:
|
|
57
|
+
recents: RecentProject[];
|
|
35
58
|
lastProject?: string;
|
|
36
|
-
|
|
59
|
+
/** Project path -> where the author was in it. Capped with `recents`, so a
|
|
60
|
+
* project forgotten from the list stops carrying a place around too. */
|
|
61
|
+
places: Record<string, Place>;
|
|
37
62
|
panes: PaneState;
|
|
38
63
|
identity?: Identity;
|
|
39
64
|
/** Keyed by whatever the app calls each window ("board", "search"). */
|
|
@@ -58,13 +83,19 @@ interface AppStore<Place, App> {
|
|
|
58
83
|
get(): AppSettings<Place, App>;
|
|
59
84
|
/** Merge into the app's own slice and save. */
|
|
60
85
|
patchApp(patch: Partial<App>): void;
|
|
61
|
-
/** Record an opened project: front of recents, deduped, capped.
|
|
62
|
-
|
|
86
|
+
/** Record an opened project: front of recents, deduped, capped. The name is
|
|
87
|
+
* refreshed each time, so renaming a project updates the menu on next open;
|
|
88
|
+
* omitting it keeps whatever was known before rather than blanking it. */
|
|
89
|
+
touchProject(path: string, name?: string): void;
|
|
63
90
|
/** Drop one that failed to open (moved, deleted). */
|
|
64
91
|
forgetProject(path: string): void;
|
|
65
|
-
/** Where the author was. Compared before writing: this
|
|
66
|
-
* navigation and must not mean a disk write per click.
|
|
92
|
+
/** Where the author was in the CURRENT project. Compared before writing: this
|
|
93
|
+
* is called on every navigation and must not mean a disk write per click.
|
|
94
|
+
* A no-op with no project open, since there is nothing to key it against. */
|
|
67
95
|
setPlace(place: Place): void;
|
|
96
|
+
/** Where the author was in a project, or undefined if it was never opened or
|
|
97
|
+
* has aged out of `recents`. Defaults to the current one. */
|
|
98
|
+
placeOf(path?: string): Place | undefined;
|
|
68
99
|
setPanes(panes: PaneState): void;
|
|
69
100
|
setIdentity(identity: Identity): void;
|
|
70
101
|
/** One window's bounds and/or pin, merged with what it already had. */
|
|
@@ -72,4 +103,4 @@ interface AppStore<Place, App> {
|
|
|
72
103
|
}
|
|
73
104
|
declare function createAppStore<Place, App extends object>(opts: AppStoreOptions<Place, App>): AppStore<Place, App>;
|
|
74
105
|
|
|
75
|
-
export { type AppSettings, type AppSettingsCore, type AppStore, type AppStoreOptions, type Identity, type PaneState, type WindowBounds, type WindowState, createAppStore };
|
|
106
|
+
export { type AppSettings, type AppSettingsCore, type AppStore, type AppStoreOptions, type Identity, type PaneState, type RecentProject, type WindowBounds, type WindowState, createAppStore };
|
package/dist/app-store.d.ts
CHANGED
|
@@ -25,15 +25,40 @@ interface PaneState {
|
|
|
25
25
|
/**
|
|
26
26
|
* The part every app has.
|
|
27
27
|
*
|
|
28
|
-
* `
|
|
28
|
+
* `places` is deliberately opaque: "open where you left off" is a family idea,
|
|
29
29
|
* but WHERE is a scene and a caret in one app and a box, a tab and a card in
|
|
30
|
-
* another.
|
|
31
|
-
*
|
|
30
|
+
* another.
|
|
31
|
+
*
|
|
32
|
+
* It is keyed BY PROJECT, and that is the interesting part. This held a single
|
|
33
|
+
* `place` and dropped it whenever a different project was opened, on the
|
|
34
|
+
* reasoning that clearing it was the one rule that could not be app-specific.
|
|
35
|
+
* That reasoning was wrong, and Patterpad disproved it by having done the other
|
|
36
|
+
* thing for a long time: it remembers the line you were on in every project you
|
|
37
|
+
* have opened. With one slot, working on A, glancing at B and coming back to A
|
|
38
|
+
* lands you at the top of A, having forgotten. Nobody wants that, and no editor
|
|
39
|
+
* outside this package does it.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* A project in the Open Recent menu.
|
|
43
|
+
*
|
|
44
|
+
* The NAME is here rather than being derived from the path, because a project's
|
|
45
|
+
* display name is what its file says it is, not what its folder is called: two
|
|
46
|
+
* projects can sit in folders called `draft`, and one can be renamed without
|
|
47
|
+
* moving. Every app in the family shows this menu, so both halves are shared.
|
|
48
|
+
*
|
|
49
|
+
* Optional because the path is the identity and the name is only how it reads.
|
|
50
|
+
* An entry recorded before a name was known still works.
|
|
32
51
|
*/
|
|
52
|
+
interface RecentProject {
|
|
53
|
+
path: string;
|
|
54
|
+
name?: string;
|
|
55
|
+
}
|
|
33
56
|
interface AppSettingsCore<Place> {
|
|
34
|
-
recents:
|
|
57
|
+
recents: RecentProject[];
|
|
35
58
|
lastProject?: string;
|
|
36
|
-
|
|
59
|
+
/** Project path -> where the author was in it. Capped with `recents`, so a
|
|
60
|
+
* project forgotten from the list stops carrying a place around too. */
|
|
61
|
+
places: Record<string, Place>;
|
|
37
62
|
panes: PaneState;
|
|
38
63
|
identity?: Identity;
|
|
39
64
|
/** Keyed by whatever the app calls each window ("board", "search"). */
|
|
@@ -58,13 +83,19 @@ interface AppStore<Place, App> {
|
|
|
58
83
|
get(): AppSettings<Place, App>;
|
|
59
84
|
/** Merge into the app's own slice and save. */
|
|
60
85
|
patchApp(patch: Partial<App>): void;
|
|
61
|
-
/** Record an opened project: front of recents, deduped, capped.
|
|
62
|
-
|
|
86
|
+
/** Record an opened project: front of recents, deduped, capped. The name is
|
|
87
|
+
* refreshed each time, so renaming a project updates the menu on next open;
|
|
88
|
+
* omitting it keeps whatever was known before rather than blanking it. */
|
|
89
|
+
touchProject(path: string, name?: string): void;
|
|
63
90
|
/** Drop one that failed to open (moved, deleted). */
|
|
64
91
|
forgetProject(path: string): void;
|
|
65
|
-
/** Where the author was. Compared before writing: this
|
|
66
|
-
* navigation and must not mean a disk write per click.
|
|
92
|
+
/** Where the author was in the CURRENT project. Compared before writing: this
|
|
93
|
+
* is called on every navigation and must not mean a disk write per click.
|
|
94
|
+
* A no-op with no project open, since there is nothing to key it against. */
|
|
67
95
|
setPlace(place: Place): void;
|
|
96
|
+
/** Where the author was in a project, or undefined if it was never opened or
|
|
97
|
+
* has aged out of `recents`. Defaults to the current one. */
|
|
98
|
+
placeOf(path?: string): Place | undefined;
|
|
68
99
|
setPanes(panes: PaneState): void;
|
|
69
100
|
setIdentity(identity: Identity): void;
|
|
70
101
|
/** One window's bounds and/or pin, merged with what it already had. */
|
|
@@ -72,4 +103,4 @@ interface AppStore<Place, App> {
|
|
|
72
103
|
}
|
|
73
104
|
declare function createAppStore<Place, App extends object>(opts: AppStoreOptions<Place, App>): AppStore<Place, App>;
|
|
74
105
|
|
|
75
|
-
export { type AppSettings, type AppSettingsCore, type AppStore, type AppStoreOptions, type Identity, type PaneState, type WindowBounds, type WindowState, createAppStore };
|
|
106
|
+
export { type AppSettings, type AppSettingsCore, type AppStore, type AppStoreOptions, type Identity, type PaneState, type RecentProject, type WindowBounds, type WindowState, createAppStore };
|
package/dist/app-store.js
CHANGED
|
@@ -11,10 +11,20 @@ function createAppStore(opts) {
|
|
|
11
11
|
} catch {
|
|
12
12
|
}
|
|
13
13
|
const state = {
|
|
14
|
-
recents: [],
|
|
15
14
|
panes: opts.panes ?? {},
|
|
16
15
|
windows: {},
|
|
17
16
|
...loaded,
|
|
17
|
+
// Migration: recents used to be bare paths. Keep them, without names, rather
|
|
18
|
+
// than emptying somebody's Open Recent menu on an update.
|
|
19
|
+
recents: (loaded.recents ?? []).map((r) => typeof r === "string" ? { path: r } : r),
|
|
20
|
+
// Migration: a settings file written before places were keyed by project has
|
|
21
|
+
// a single `place` belonging to `lastProject`. Carry it over rather than
|
|
22
|
+
// dropping it, or everyone's first launch after this update forgets where
|
|
23
|
+
// they were, which is the exact complaint the change is meant to fix.
|
|
24
|
+
places: {
|
|
25
|
+
...loaded.places ?? {},
|
|
26
|
+
...loaded.lastProject !== void 0 && loaded.place !== void 0 ? { [loaded.lastProject]: loaded.place } : {}
|
|
27
|
+
},
|
|
18
28
|
// The app's slice merges FIELD BY FIELD, so a key added in a new version
|
|
19
29
|
// arrives with its default rather than being absent for everybody who has
|
|
20
30
|
// run the app before.
|
|
@@ -36,22 +46,33 @@ function createAppStore(opts) {
|
|
|
36
46
|
Object.assign(state.app, patch);
|
|
37
47
|
save();
|
|
38
48
|
},
|
|
39
|
-
touchProject(path) {
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
touchProject(path, name) {
|
|
50
|
+
const known = state.recents.find((r) => r.path === path);
|
|
51
|
+
const entry = { path, ...name ?? known?.name ? { name: name ?? known?.name } : {} };
|
|
52
|
+
state.recents = [entry, ...state.recents.filter((r) => r.path !== path)].slice(0, maxRecents);
|
|
42
53
|
state.lastProject = path;
|
|
54
|
+
for (const key of Object.keys(state.places)) {
|
|
55
|
+
if (!state.recents.some((r) => r.path === key)) delete state.places[key];
|
|
56
|
+
}
|
|
43
57
|
save();
|
|
44
58
|
},
|
|
45
59
|
forgetProject(path) {
|
|
46
|
-
state.recents = state.recents.filter((
|
|
60
|
+
state.recents = state.recents.filter((r) => r.path !== path);
|
|
47
61
|
if (state.lastProject === path) delete state.lastProject;
|
|
62
|
+
delete state.places[path];
|
|
48
63
|
save();
|
|
49
64
|
},
|
|
50
65
|
setPlace(place) {
|
|
51
|
-
|
|
52
|
-
|
|
66
|
+
const path = state.lastProject;
|
|
67
|
+
if (path === void 0) return;
|
|
68
|
+
if (JSON.stringify(state.places[path]) === JSON.stringify(place)) return;
|
|
69
|
+
state.places[path] = place;
|
|
53
70
|
save();
|
|
54
71
|
},
|
|
72
|
+
placeOf: (path) => {
|
|
73
|
+
const key = path ?? state.lastProject;
|
|
74
|
+
return key === void 0 ? void 0 : structuredClone(state.places[key]);
|
|
75
|
+
},
|
|
55
76
|
setPanes(panes) {
|
|
56
77
|
state.panes = { ...state.panes, ...panes };
|
|
57
78
|
save();
|
package/dist/app-store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/app-store.ts"],"names":[],"mappings":"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","file":"app-store.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The user's settings for THIS app: a small JSON file in the app's userData.\n//\n// Every app in the family has one, and both of the two that exist wrote it\n// separately: the same tolerant read, the same merged defaults, the same capped\n// recents, the same panes, the same per-tool-window bounds and pin, the same\n// identity. That is the definition of shell work.\n//\n// WHAT BELONGS HERE is settings that follow the PERSON, not the project: where\n// their windows were, which panes they like open, what they are called, what\n// they had open last. Project content never comes near this file - it is not\n// version controlled, it is not shared, and losing it should cost an author\n// nothing but a re-arranged window.\n//\n// The shell owns the CORE it can reason about and hands the app one slice of\n// its own (`app`) for everything else - a theme value, a view mode, whatever it\n// has. Two stores in one file, one atomic write, so a crash cannot leave an app\n// with half its settings.\n//\n// Electron-free on purpose: it takes a directory. That keeps it unit-testable\n// and lets a test point it at a temp dir.\n// ---------------------------------------------------------------------------\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/** Who is at the keyboard. Never in a project: see identity.ts. */\nexport interface Identity { name: string; email?: string }\n\n/** A remembered window rectangle. Absent = the app's default, centred. */\nexport interface WindowBounds { x?: number; y?: number; width: number; height: number }\n\n/** One helper window's remembered state. */\nexport interface WindowState { bounds?: WindowBounds; pinned?: boolean }\n\n/** Side panes: open/closed, and any dragged width. */\nexport interface PaneState {\n nav?: boolean;\n inspector?: boolean;\n navW?: number;\n inspW?: number;\n}\n\n/**\n * The part every app has.\n *\n * `place` is deliberately opaque: \"open where you left off\" is a family idea,\n * but WHERE is a scene and a caret in one app and a box, a tab and a card in\n * another. The shell stores it and clears it when the project changes, which is\n * the one rule about it that is not app-specific.\n */\nexport interface AppSettingsCore<Place> {\n recents: string[];\n lastProject?: string;\n place?: Place;\n panes: PaneState;\n identity?: Identity;\n /** Keyed by whatever the app calls each window (\"board\", \"search\"). */\n windows: Record<string, WindowState>;\n}\n\nexport type AppSettings<Place, App> = AppSettingsCore<Place> & { app: App };\n\nexport interface AppStoreOptions<Place, App> {\n /** The app's userData directory. */\n dir: string;\n /** The file inside it. One per app; two apps never share. */\n fileName?: string;\n /** The app's own slice, as it should read on a first run. */\n defaults: App;\n /** How the panes should sit before anybody touches them. */\n panes?: PaneState;\n maxRecents?: number;\n}\n\nexport interface AppStore<Place, App> {\n /** A copy, always: nothing outside can mutate the state behind the store's back. */\n get(): AppSettings<Place, App>;\n /** Merge into the app's own slice and save. */\n patchApp(patch: Partial<App>): void;\n /** Record an opened project: front of recents, deduped, capped. */\n touchProject(path: string): void;\n /** Drop one that failed to open (moved, deleted). */\n forgetProject(path: string): void;\n /** Where the author was. Compared before writing: this is called on every\n * navigation and must not mean a disk write per click. */\n setPlace(place: Place): void;\n setPanes(panes: PaneState): void;\n setIdentity(identity: Identity): void;\n /** One window's bounds and/or pin, merged with what it already had. */\n setWindow(key: string, state: WindowState): void;\n}\n\nexport function createAppStore<Place, App extends object>(\n opts: AppStoreOptions<Place, App>,\n): AppStore<Place, App> {\n const file = join(opts.dir, opts.fileName ?? \"app-settings.json\");\n const maxRecents = opts.maxRecents ?? 8;\n\n let loaded: Partial<AppSettings<Place, App>> = {};\n try {\n loaded = JSON.parse(readFileSync(file, \"utf8\")) as Partial<AppSettings<Place, App>>;\n } catch {\n // First run, or a file we cannot read. Defaults, never a crash: settings\n // are not worth failing to start over.\n }\n\n const state: AppSettings<Place, App> = {\n recents: [],\n panes: opts.panes ?? {},\n windows: {},\n ...loaded,\n // The app's slice merges FIELD BY FIELD, so a key added in a new version\n // arrives with its default rather than being absent for everybody who has\n // run the app before.\n app: { ...opts.defaults, ...(loaded.app ?? {}) },\n };\n\n const save = (): void => {\n try {\n mkdirSync(opts.dir, { recursive: true });\n // Write beside, then rename: a rename is atomic on every platform this\n // ships to, so a crash mid-write cannot leave a half-written settings\n // file - which both apps' hand-written stores could.\n const temp = `${file}.tmp`;\n writeFileSync(temp, `${JSON.stringify(state, null, 2)}\\n`, \"utf8\");\n renameSync(temp, file);\n } catch {\n // A read-only home directory, a full disk: the app carries on with the\n // settings it has in memory rather than dying over a preference.\n }\n };\n\n return {\n get: () => structuredClone(state),\n patchApp(patch) {\n Object.assign(state.app, patch);\n save();\n },\n touchProject(path) {\n // A DIFFERENT project invalidates the remembered place; the same one must\n // keep it, or \"open where you left off\" could never work, since reopening\n // goes through here.\n if (state.lastProject !== path) delete state.place;\n state.recents = [path, ...state.recents.filter((p) => p !== path)].slice(0, maxRecents);\n state.lastProject = path;\n save();\n },\n forgetProject(path) {\n state.recents = state.recents.filter((p) => p !== path);\n if (state.lastProject === path) delete state.lastProject;\n save();\n },\n setPlace(place) {\n if (JSON.stringify(state.place) === JSON.stringify(place)) return;\n state.place = place;\n save();\n },\n setPanes(panes) {\n state.panes = { ...state.panes, ...panes };\n save();\n },\n setIdentity(identity) {\n state.identity = identity;\n save();\n },\n setWindow(key, next) {\n state.windows[key] = { ...state.windows[key], ...next };\n save();\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/app-store.ts"],"names":[],"mappings":"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","file":"app-store.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The user's settings for THIS app: a small JSON file in the app's userData.\n//\n// Every app in the family has one, and both of the two that exist wrote it\n// separately: the same tolerant read, the same merged defaults, the same capped\n// recents, the same panes, the same per-tool-window bounds and pin, the same\n// identity. That is the definition of shell work.\n//\n// WHAT BELONGS HERE is settings that follow the PERSON, not the project: where\n// their windows were, which panes they like open, what they are called, what\n// they had open last. Project content never comes near this file - it is not\n// version controlled, it is not shared, and losing it should cost an author\n// nothing but a re-arranged window.\n//\n// The shell owns the CORE it can reason about and hands the app one slice of\n// its own (`app`) for everything else - a theme value, a view mode, whatever it\n// has. Two stores in one file, one atomic write, so a crash cannot leave an app\n// with half its settings.\n//\n// Electron-free on purpose: it takes a directory. That keeps it unit-testable\n// and lets a test point it at a temp dir.\n// ---------------------------------------------------------------------------\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/** Who is at the keyboard. Never in a project: see identity.ts. */\nexport interface Identity { name: string; email?: string }\n\n/** A remembered window rectangle. Absent = the app's default, centred. */\nexport interface WindowBounds { x?: number; y?: number; width: number; height: number }\n\n/** One helper window's remembered state. */\nexport interface WindowState { bounds?: WindowBounds; pinned?: boolean }\n\n/** Side panes: open/closed, and any dragged width. */\nexport interface PaneState {\n nav?: boolean;\n inspector?: boolean;\n navW?: number;\n inspW?: number;\n}\n\n/**\n * The part every app has.\n *\n * `places` is deliberately opaque: \"open where you left off\" is a family idea,\n * but WHERE is a scene and a caret in one app and a box, a tab and a card in\n * another.\n *\n * It is keyed BY PROJECT, and that is the interesting part. This held a single\n * `place` and dropped it whenever a different project was opened, on the\n * reasoning that clearing it was the one rule that could not be app-specific.\n * That reasoning was wrong, and Patterpad disproved it by having done the other\n * thing for a long time: it remembers the line you were on in every project you\n * have opened. With one slot, working on A, glancing at B and coming back to A\n * lands you at the top of A, having forgotten. Nobody wants that, and no editor\n * outside this package does it.\n */\n/**\n * A project in the Open Recent menu.\n *\n * The NAME is here rather than being derived from the path, because a project's\n * display name is what its file says it is, not what its folder is called: two\n * projects can sit in folders called `draft`, and one can be renamed without\n * moving. Every app in the family shows this menu, so both halves are shared.\n *\n * Optional because the path is the identity and the name is only how it reads.\n * An entry recorded before a name was known still works.\n */\nexport interface RecentProject {\n path: string;\n name?: string;\n}\n\nexport interface AppSettingsCore<Place> {\n recents: RecentProject[];\n lastProject?: string;\n /** Project path -> where the author was in it. Capped with `recents`, so a\n * project forgotten from the list stops carrying a place around too. */\n places: Record<string, Place>;\n panes: PaneState;\n identity?: Identity;\n /** Keyed by whatever the app calls each window (\"board\", \"search\"). */\n windows: Record<string, WindowState>;\n}\n\nexport type AppSettings<Place, App> = AppSettingsCore<Place> & { app: App };\n\nexport interface AppStoreOptions<Place, App> {\n /** The app's userData directory. */\n dir: string;\n /** The file inside it. One per app; two apps never share. */\n fileName?: string;\n /** The app's own slice, as it should read on a first run. */\n defaults: App;\n /** How the panes should sit before anybody touches them. */\n panes?: PaneState;\n maxRecents?: number;\n}\n\nexport interface AppStore<Place, App> {\n /** A copy, always: nothing outside can mutate the state behind the store's back. */\n get(): AppSettings<Place, App>;\n /** Merge into the app's own slice and save. */\n patchApp(patch: Partial<App>): void;\n /** Record an opened project: front of recents, deduped, capped. The name is\n * refreshed each time, so renaming a project updates the menu on next open;\n * omitting it keeps whatever was known before rather than blanking it. */\n touchProject(path: string, name?: string): void;\n /** Drop one that failed to open (moved, deleted). */\n forgetProject(path: string): void;\n /** Where the author was in the CURRENT project. Compared before writing: this\n * is called on every navigation and must not mean a disk write per click.\n * A no-op with no project open, since there is nothing to key it against. */\n setPlace(place: Place): void;\n /** Where the author was in a project, or undefined if it was never opened or\n * has aged out of `recents`. Defaults to the current one. */\n placeOf(path?: string): Place | undefined;\n setPanes(panes: PaneState): void;\n setIdentity(identity: Identity): void;\n /** One window's bounds and/or pin, merged with what it already had. */\n setWindow(key: string, state: WindowState): void;\n}\n\nexport function createAppStore<Place, App extends object>(\n opts: AppStoreOptions<Place, App>,\n): AppStore<Place, App> {\n const file = join(opts.dir, opts.fileName ?? \"app-settings.json\");\n const maxRecents = opts.maxRecents ?? 8;\n\n let loaded: Partial<AppSettings<Place, App>> = {};\n try {\n loaded = JSON.parse(readFileSync(file, \"utf8\")) as Partial<AppSettings<Place, App>>;\n } catch {\n // First run, or a file we cannot read. Defaults, never a crash: settings\n // are not worth failing to start over.\n }\n\n const state: AppSettings<Place, App> = {\n panes: opts.panes ?? {},\n windows: {},\n ...loaded,\n // Migration: recents used to be bare paths. Keep them, without names, rather\n // than emptying somebody's Open Recent menu on an update.\n recents: (loaded.recents ?? []).map((r) =>\n typeof r === \"string\" ? { path: r } : r) as RecentProject[],\n // Migration: a settings file written before places were keyed by project has\n // a single `place` belonging to `lastProject`. Carry it over rather than\n // dropping it, or everyone's first launch after this update forgets where\n // they were, which is the exact complaint the change is meant to fix.\n places: {\n ...(loaded.places ?? {}),\n ...(loaded.lastProject !== undefined && (loaded as { place?: Place }).place !== undefined\n ? { [loaded.lastProject]: (loaded as { place?: Place }).place as Place }\n : {}),\n },\n // The app's slice merges FIELD BY FIELD, so a key added in a new version\n // arrives with its default rather than being absent for everybody who has\n // run the app before.\n app: { ...opts.defaults, ...(loaded.app ?? {}) },\n };\n\n const save = (): void => {\n try {\n mkdirSync(opts.dir, { recursive: true });\n // Write beside, then rename: a rename is atomic on every platform this\n // ships to, so a crash mid-write cannot leave a half-written settings\n // file - which both apps' hand-written stores could.\n const temp = `${file}.tmp`;\n writeFileSync(temp, `${JSON.stringify(state, null, 2)}\\n`, \"utf8\");\n renameSync(temp, file);\n } catch {\n // A read-only home directory, a full disk: the app carries on with the\n // settings it has in memory rather than dying over a preference.\n }\n };\n\n return {\n get: () => structuredClone(state),\n patchApp(patch) {\n Object.assign(state.app, patch);\n save();\n },\n touchProject(path, name) {\n const known = state.recents.find((r) => r.path === path);\n // An omitted name keeps the one we had: reopening from a route that does\n // not know the name must not blank the menu entry.\n const entry: RecentProject = { path, ...(name ?? known?.name ? { name: name ?? known?.name } : {}) };\n state.recents = [entry, ...state.recents.filter((r) => r.path !== path)].slice(0, maxRecents);\n state.lastProject = path;\n // Places follow recents: one that has aged off the list should not keep a\n // place in the file for ever. Nothing is dropped for merely opening\n // something else, which is the whole point of keying them.\n for (const key of Object.keys(state.places)) {\n if (!state.recents.some((r) => r.path === key)) delete state.places[key];\n }\n save();\n },\n forgetProject(path) {\n state.recents = state.recents.filter((r) => r.path !== path);\n if (state.lastProject === path) delete state.lastProject;\n delete state.places[path]; // it moved or went: its place is meaningless now\n save();\n },\n setPlace(place) {\n const path = state.lastProject;\n if (path === undefined) return; // no project open: nothing to key it against\n if (JSON.stringify(state.places[path]) === JSON.stringify(place)) return;\n state.places[path] = place;\n save();\n },\n placeOf: (path) => {\n const key = path ?? state.lastProject;\n return key === undefined ? undefined : structuredClone(state.places[key]);\n },\n setPanes(panes) {\n state.panes = { ...state.panes, ...panes };\n save();\n },\n setIdentity(identity) {\n state.identity = identity;\n save();\n },\n setWindow(key, next) {\n state.windows[key] = { ...state.windows[key], ...next };\n save();\n },\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wildwinter/app-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "The shared shell for the editor family (Patterpad, Storylet Studio): the app-agnostic design-token grammar, the per-entity colour hash, and the vanilla-TS DOM primitive. Two palettes, one grammar - each app supplies its own palette/fonts over these tokens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|