@wildwinter/app-shell 0.32.0 → 0.33.1

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.
@@ -22,16 +22,22 @@ function createAppStore(opts) {
22
22
  // Migration: a settings file written before places were keyed by project has
23
23
  // a single `place` belonging to `lastProject`. Carry it over rather than
24
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.
25
+ // they were - but only where no keyed entry exists. The first cut of this
26
+ // merge put the legacy value LAST, so a stale singular beat every fresh
27
+ // per-project write on every launch, and (because the `...loaded` spread
28
+ // above kept the key alive through save) it did so for ever: writes landed
29
+ // correctly all session and the next start quietly reset them. Found from
30
+ // a host's user report; the key is retired just below.
26
31
  places: {
27
- ...loaded.places ?? {},
28
- ...loaded.lastProject !== void 0 && loaded.place !== void 0 ? { [loaded.lastProject]: loaded.place } : {}
32
+ ...loaded.lastProject !== void 0 && loaded.place !== void 0 ? { [loaded.lastProject]: loaded.place } : {},
33
+ ...loaded.places ?? {}
29
34
  },
30
35
  // The app's slice merges FIELD BY FIELD, so a key added in a new version
31
36
  // arrives with its default rather than being absent for everybody who has
32
37
  // run the app before.
33
38
  app: { ...opts.defaults, ...loaded.app ?? {} }
34
39
  };
40
+ delete state.place;
35
41
  const save = () => {
36
42
  try {
37
43
  fs.mkdirSync(opts.dir, { recursive: true });
@@ -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 * `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"]}
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 - but only where no keyed entry exists. The first cut of this\n // merge put the legacy value LAST, so a stale singular beat every fresh\n // per-project write on every launch, and (because the `...loaded` spread\n // above kept the key alive through save) it did so for ever: writes landed\n // correctly all session and the next start quietly reset them. Found from\n // a host's user report; the key is retired just below.\n places: {\n ...(loaded.lastProject !== undefined && (loaded as { place?: Place }).place !== undefined\n ? { [loaded.lastProject]: (loaded as { place?: Place }).place as Place }\n : {}),\n ...(loaded.places ?? {}),\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 // Retire the legacy key: it rode in on `...loaded`, and a state that still\n // carries it re-runs the fold on every future load.\n delete (state as { place?: Place }).place;\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.js CHANGED
@@ -20,16 +20,22 @@ function createAppStore(opts) {
20
20
  // Migration: a settings file written before places were keyed by project has
21
21
  // a single `place` belonging to `lastProject`. Carry it over rather than
22
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.
23
+ // they were - but only where no keyed entry exists. The first cut of this
24
+ // merge put the legacy value LAST, so a stale singular beat every fresh
25
+ // per-project write on every launch, and (because the `...loaded` spread
26
+ // above kept the key alive through save) it did so for ever: writes landed
27
+ // correctly all session and the next start quietly reset them. Found from
28
+ // a host's user report; the key is retired just below.
24
29
  places: {
25
- ...loaded.places ?? {},
26
- ...loaded.lastProject !== void 0 && loaded.place !== void 0 ? { [loaded.lastProject]: loaded.place } : {}
30
+ ...loaded.lastProject !== void 0 && loaded.place !== void 0 ? { [loaded.lastProject]: loaded.place } : {},
31
+ ...loaded.places ?? {}
27
32
  },
28
33
  // The app's slice merges FIELD BY FIELD, so a key added in a new version
29
34
  // arrives with its default rather than being absent for everybody who has
30
35
  // run the app before.
31
36
  app: { ...opts.defaults, ...loaded.app ?? {} }
32
37
  };
38
+ delete state.place;
33
39
  const save = () => {
34
40
  try {
35
41
  mkdirSync(opts.dir, { recursive: true });
@@ -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 * `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"]}
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 - but only where no keyed entry exists. The first cut of this\n // merge put the legacy value LAST, so a stale singular beat every fresh\n // per-project write on every launch, and (because the `...loaded` spread\n // above kept the key alive through save) it did so for ever: writes landed\n // correctly all session and the next start quietly reset them. Found from\n // a host's user report; the key is retired just below.\n places: {\n ...(loaded.lastProject !== undefined && (loaded as { place?: Place }).place !== undefined\n ? { [loaded.lastProject]: (loaded as { place?: Place }).place as Place }\n : {}),\n ...(loaded.places ?? {}),\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 // Retire the legacy key: it rode in on `...loaded`, and a state that still\n // carries it re-runs the fold on every future load.\n delete (state as { place?: Place }).place;\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/settings.css CHANGED
@@ -208,6 +208,11 @@
208
208
  }
209
209
  .shell-icon {
210
210
  flex: none;
211
+ display: inline-flex;
212
+ align-items: center;
213
+ justify-content: center;
214
+ padding: 0;
215
+ line-height: 1;
211
216
  width: 24px;
212
217
  height: 24px;
213
218
  border: 1px solid var(--line);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/settings.css"],"sourcesContent":["/* ============================================================================\n The settings dialog + list-editor styling. Reads the shared token grammar\n (tokens.css) and the app's palette vars (--surface / --ink / --muted / --line\n / --accent / --danger / --bg, optional --line-soft / --accent-soft). Neutral\n class names; the app supplies section content.\n ============================================================================ */\n\n.settings-dialog {\n padding: 0; border: 1px solid var(--line); border-radius: var(--radius-lg);\n background: var(--surface); color: var(--ink);\n width: min(760px, 92vw); max-height: 82vh;\n box-shadow: var(--shadow-3);\n font: 14px var(--font-ui, system-ui, sans-serif);\n}\n.settings-dialog::backdrop { background: color-mix(in oklab, #000 42%, transparent); }\n.settings-form { display: flex; flex-direction: column; max-height: 82vh; }\n.settings-title { margin: 0; padding: 16px 20px; font-family: var(--font-read, inherit); font-size: 1.15rem; font-weight: 600; border-bottom: 1px solid var(--line-soft, var(--line)); }\n\n.settings-body { display: flex; min-height: 0; flex: 1; }\n.settings-tabs { flex: none; width: 12rem; overflow-y: auto; padding: 10px 8px; border-right: 1px solid var(--line-soft, var(--line)); display: flex; flex-direction: column; gap: 1px; }\n.settings-group { padding: 12px 8px 4px; font: 600 0.62rem/1 var(--font-ui); text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); }\n.settings-tab { text-align: left; width: 100%; padding: 6px 10px; border: 0; border-radius: var(--radius-sm); background: none; color: var(--ink); font: inherit; font-size: 0.88rem; cursor: pointer; }\n.settings-tab:hover { background: var(--accent-soft, color-mix(in oklab, var(--accent) 12%, transparent)); }\n.settings-tab.active { background: var(--accent-soft, color-mix(in oklab, var(--accent) 16%, transparent)); color: var(--accent); font-weight: 500; }\n\n.settings-panels { flex: 1; min-width: 0; overflow-y: auto; padding: 18px 22px; }\n.settings-panel { display: flex; flex-direction: column; gap: 14px; }\n/* A `display` on the class beats the UA's `[hidden] { display: none }`, so the\n panels the tab switcher hides would all render at once. Opt back out. */\n.settings-panel[hidden] { display: none; }\n\n.settings-actions { display: flex; align-items: center; gap: 10px; padding: 12px 20px; border-top: 1px solid var(--line-soft, var(--line)); }\n.settings-error { flex: 1; margin: 0; color: var(--danger); font-size: 0.82rem; }\n.settings-actions button { font: inherit; padding: 6px 16px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: none; color: var(--muted); cursor: pointer; }\n.settings-actions button:hover { color: var(--accent); border-color: var(--accent); }\n.settings-actions .primary { background: var(--accent); color: var(--surface); border-color: transparent; font-weight: 500; }\n.settings-actions .primary:hover { color: var(--surface); filter: brightness(1.06); }\n\n/* --- list editor (rows + disclosure + inline field kit) --------------------- */\n/* A row is a WELL, not a ruled line: bordered, rounded, and sitting on `--card`\n rather than on the panel behind it. Patterpad's design, adopted as the family's\n (author's ruling, 2026-08-16), reversing the usual direction the way the bars\n did. A flat rule reads as a table of text; a well reads as a thing you can\n edit, which is what every one of these rows is. Rows are separated by the\n list's `gap`, so nothing here draws a divider. */\n.set-row { display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--card); }\n.set-rowline { display: flex; align-items: center; gap: 8px; padding: 5px 7px; }\n/* Indented past the disclosure so the secondary fields hang under the row's\n content rather than under its triangle, and ruled off with a lighter line than\n the well's own border. */\n.set-details {\n display: flex; flex-direction: column; gap: 7px; align-items: stretch;\n padding: 6px 8px 8px 21px;\n border-top: 1px solid color-mix(in oklab, var(--line) 60%, transparent);\n}\n.set-details[hidden] { display: none; } /* see .settings-panel[hidden] */\n/* The disclosure triangle is drawn, not typed: U+25B8/25BE are \"small\"\n triangles that stay hairline-thin at any font size. Rotates when open. */\n.set-expand { flex: none; width: 18px; height: 18px; border: 0; background: none; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; }\n.set-expand::before {\n content: \"\"; width: 0; height: 0;\n border-left: 6px solid currentColor; border-top: 4.5px solid transparent; border-bottom: 4.5px solid transparent;\n transform-origin: 35% 50%; transition: transform 0.12s ease;\n}\n.set-expand { color: var(--muted); }\n.set-expand:hover, .set-expand.open { color: var(--accent); }\n.set-expand.open::before { transform: rotate(90deg); }\n\n.set-rowline input, .set-rowline select, .set-details input, .set-details select {\n font: inherit; font-size: 0.85rem; background: var(--bg); color: var(--ink);\n border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 4px 8px;\n}\n.set-rowline input.set-name { flex: 1; min-width: 6rem; }\n.set-rowline input.invalid, .set-details input.invalid { border-color: var(--danger); }\n/* `.illegal`: a value that breaks a RULE, as opposed to `.invalid`, which the dup guard\n owns for a value that clashes with another. Both can be true of one field, so they are\n separate classes and draw the same. */\n.set-rowline input.illegal, .set-details input.illegal { border-color: var(--danger); }\n.set-rowline input:focus-visible, .set-rowline select:focus-visible,\n.set-details input:focus-visible, .set-details select:focus-visible { outline: 2px solid var(--accent); outline-offset: 0; border-color: var(--accent); }\n\n.shell-icon { flex: none; width: 24px; height: 24px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: none; color: var(--muted); cursor: pointer; font-size: 0.8rem; }\n.shell-icon:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }\n.shell-icon:disabled { opacity: 0.4; cursor: default; }\n.shell-icon.danger:hover:not(:disabled) { color: var(--surface); background: var(--danger); border-color: var(--danger); }\n\n.shell-labelled { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; }\n.shell-fieldcap { color: var(--muted); min-width: 5rem; }\n\n.shell-tags { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }\n.shell-tag { display: inline-flex; align-items: center; gap: 4px; font-size: 0.76rem; padding: 2px 4px 2px 9px; border: 1px solid var(--line); border-radius: var(--radius-pill); color: var(--ink); }\n.shell-tag-x { border: 0; background: none; color: var(--muted); cursor: pointer; font-size: 0.7rem; padding: 0 2px; }\n.shell-tag-x:hover { color: var(--danger); }\n.shell-tag-input { min-width: 8rem; }\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/settings.css"],"sourcesContent":["/* ============================================================================\n The settings dialog + list-editor styling. Reads the shared token grammar\n (tokens.css) and the app's palette vars (--surface / --ink / --muted / --line\n / --accent / --danger / --bg, optional --line-soft / --accent-soft). Neutral\n class names; the app supplies section content.\n ============================================================================ */\n\n.settings-dialog {\n padding: 0; border: 1px solid var(--line); border-radius: var(--radius-lg);\n background: var(--surface); color: var(--ink);\n width: min(760px, 92vw); max-height: 82vh;\n box-shadow: var(--shadow-3);\n font: 14px var(--font-ui, system-ui, sans-serif);\n}\n.settings-dialog::backdrop { background: color-mix(in oklab, #000 42%, transparent); }\n.settings-form { display: flex; flex-direction: column; max-height: 82vh; }\n.settings-title { margin: 0; padding: 16px 20px; font-family: var(--font-read, inherit); font-size: 1.15rem; font-weight: 600; border-bottom: 1px solid var(--line-soft, var(--line)); }\n\n.settings-body { display: flex; min-height: 0; flex: 1; }\n.settings-tabs { flex: none; width: 12rem; overflow-y: auto; padding: 10px 8px; border-right: 1px solid var(--line-soft, var(--line)); display: flex; flex-direction: column; gap: 1px; }\n.settings-group { padding: 12px 8px 4px; font: 600 0.62rem/1 var(--font-ui); text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); }\n.settings-tab { text-align: left; width: 100%; padding: 6px 10px; border: 0; border-radius: var(--radius-sm); background: none; color: var(--ink); font: inherit; font-size: 0.88rem; cursor: pointer; }\n.settings-tab:hover { background: var(--accent-soft, color-mix(in oklab, var(--accent) 12%, transparent)); }\n.settings-tab.active { background: var(--accent-soft, color-mix(in oklab, var(--accent) 16%, transparent)); color: var(--accent); font-weight: 500; }\n\n.settings-panels { flex: 1; min-width: 0; overflow-y: auto; padding: 18px 22px; }\n.settings-panel { display: flex; flex-direction: column; gap: 14px; }\n/* A `display` on the class beats the UA's `[hidden] { display: none }`, so the\n panels the tab switcher hides would all render at once. Opt back out. */\n.settings-panel[hidden] { display: none; }\n\n.settings-actions { display: flex; align-items: center; gap: 10px; padding: 12px 20px; border-top: 1px solid var(--line-soft, var(--line)); }\n.settings-error { flex: 1; margin: 0; color: var(--danger); font-size: 0.82rem; }\n.settings-actions button { font: inherit; padding: 6px 16px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: none; color: var(--muted); cursor: pointer; }\n.settings-actions button:hover { color: var(--accent); border-color: var(--accent); }\n.settings-actions .primary { background: var(--accent); color: var(--surface); border-color: transparent; font-weight: 500; }\n.settings-actions .primary:hover { color: var(--surface); filter: brightness(1.06); }\n\n/* --- list editor (rows + disclosure + inline field kit) --------------------- */\n/* A row is a WELL, not a ruled line: bordered, rounded, and sitting on `--card`\n rather than on the panel behind it. Patterpad's design, adopted as the family's\n (author's ruling, 2026-08-16), reversing the usual direction the way the bars\n did. A flat rule reads as a table of text; a well reads as a thing you can\n edit, which is what every one of these rows is. Rows are separated by the\n list's `gap`, so nothing here draws a divider. */\n.set-row { display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--card); }\n.set-rowline { display: flex; align-items: center; gap: 8px; padding: 5px 7px; }\n/* Indented past the disclosure so the secondary fields hang under the row's\n content rather than under its triangle, and ruled off with a lighter line than\n the well's own border. */\n.set-details {\n display: flex; flex-direction: column; gap: 7px; align-items: stretch;\n padding: 6px 8px 8px 21px;\n border-top: 1px solid color-mix(in oklab, var(--line) 60%, transparent);\n}\n.set-details[hidden] { display: none; } /* see .settings-panel[hidden] */\n/* The disclosure triangle is drawn, not typed: U+25B8/25BE are \"small\"\n triangles that stay hairline-thin at any font size. Rotates when open. */\n.set-expand { flex: none; width: 18px; height: 18px; border: 0; background: none; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; }\n.set-expand::before {\n content: \"\"; width: 0; height: 0;\n border-left: 6px solid currentColor; border-top: 4.5px solid transparent; border-bottom: 4.5px solid transparent;\n transform-origin: 35% 50%; transition: transform 0.12s ease;\n}\n.set-expand { color: var(--muted); }\n.set-expand:hover, .set-expand.open { color: var(--accent); }\n.set-expand.open::before { transform: rotate(90deg); }\n\n.set-rowline input, .set-rowline select, .set-details input, .set-details select {\n font: inherit; font-size: 0.85rem; background: var(--bg); color: var(--ink);\n border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 4px 8px;\n}\n.set-rowline input.set-name { flex: 1; min-width: 6rem; }\n.set-rowline input.invalid, .set-details input.invalid { border-color: var(--danger); }\n/* `.illegal`: a value that breaks a RULE, as opposed to `.invalid`, which the dup guard\n owns for a value that clashes with another. Both can be true of one field, so they are\n separate classes and draw the same. */\n.set-rowline input.illegal, .set-details input.illegal { border-color: var(--danger); }\n.set-rowline input:focus-visible, .set-rowline select:focus-visible,\n.set-details input:focus-visible, .set-details select:focus-visible { outline: 2px solid var(--accent); outline-offset: 0; border-color: var(--accent); }\n\n/* Centre the glyph EXPLICITLY: a bare <button> centres its text by the font's\n own metrics plus the UA's default padding, which parks arrow and cross\n glyphs up-left of the square's visual centre (reported from Storyletter's\n property lists, 2026-08-26; the same buttons everywhere shared the offset). */\n.shell-icon { flex: none; display: inline-flex; align-items: center; justify-content: center; padding: 0; line-height: 1; width: 24px; height: 24px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: none; color: var(--muted); cursor: pointer; font-size: 0.8rem; }\n.shell-icon:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }\n.shell-icon:disabled { opacity: 0.4; cursor: default; }\n.shell-icon.danger:hover:not(:disabled) { color: var(--surface); background: var(--danger); border-color: var(--danger); }\n\n.shell-labelled { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; }\n.shell-fieldcap { color: var(--muted); min-width: 5rem; }\n\n.shell-tags { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }\n.shell-tag { display: inline-flex; align-items: center; gap: 4px; font-size: 0.76rem; padding: 2px 4px 2px 9px; border: 1px solid var(--line); border-radius: var(--radius-pill); color: var(--ink); }\n.shell-tag-x { border: 0; background: none; color: var(--muted); cursor: pointer; font-size: 0.7rem; padding: 0 2px; }\n.shell-tag-x:hover { color: var(--danger); }\n.shell-tag-input { min-width: 8rem; }\n"],"mappings":";AAOA,CAAC;AACC,WAAS;AAAG,UAAQ,IAAI,MAAM,IAAI;AAAS,iBAAe,IAAI;AAC9D,cAAY,IAAI;AAAY,SAAO,IAAI;AACvC,SAAO,IAAI,KAAK,EAAE;AAAO,cAAY;AACrC,cAAY,IAAI;AAChB,QAAM,KAAK,IAAI,SAAS,EAAE,SAAS,EAAE;AACvC;AACA,CAPC,eAOe;AAAa,cAAY,UAAU,GAAG,KAAK,EAAE,KAAK,GAAG,EAAE;AAAc;AACrF,CAAC;AAAgB,WAAS;AAAM,kBAAgB;AAAQ,cAAY;AAAM;AAC1E,CAAC;AAAiB,UAAQ;AAAG,WAAS,KAAK;AAAM,eAAa,IAAI,WAAW,EAAE;AAAU,aAAW;AAAS,eAAa;AAAK,iBAAe,IAAI,MAAM,IAAI,WAAW,EAAE,IAAI;AAAU;AAEvL,CAAC;AAAgB,WAAS;AAAM,cAAY;AAAG,QAAM;AAAG;AACxD,CAAC;AAAgB,QAAM;AAAM,SAAO;AAAO,cAAY;AAAM,WAAS,KAAK;AAAK,gBAAc,IAAI,MAAM,IAAI,WAAW,EAAE,IAAI;AAAU,WAAS;AAAM,kBAAgB;AAAQ,OAAK;AAAK;AACxL,CAAC;AAAiB,WAAS,KAAK,IAAI;AAAK,QAAM,IAAI,OAAO,CAAC,EAAE,IAAI;AAAY,kBAAgB;AAAW,kBAAgB;AAAQ,SAAO,IAAI;AAAU;AACrJ,CAAC;AAAe,cAAY;AAAM,SAAO;AAAM,WAAS,IAAI;AAAM,UAAQ;AAAG,iBAAe,IAAI;AAAc,cAAY;AAAM,SAAO,IAAI;AAAQ,QAAM;AAAS,aAAW;AAAS,UAAQ;AAAS;AACvM,CADC,YACY;AAAS,cAAY,IAAI,aAAa,EAAE,UAAU,GAAG,KAAK,EAAE,IAAI,UAAU,GAAG,EAAE;AAAe;AAC3G,CAFC,YAEY,CAAC;AAAS,cAAY,IAAI,aAAa,EAAE,UAAU,GAAG,KAAK,EAAE,IAAI,UAAU,GAAG,EAAE;AAAe,SAAO,IAAI;AAAW,eAAa;AAAK;AAEpJ,CAAC;AAAkB,QAAM;AAAG,aAAW;AAAG,cAAY;AAAM,WAAS,KAAK;AAAM;AAChF,CAAC;AAAiB,WAAS;AAAM,kBAAgB;AAAQ,OAAK;AAAM;AAGpE,CAHC,cAGc,CAAC;AAAU,WAAS;AAAM;AAEzC,CAAC;AAAmB,WAAS;AAAM,eAAa;AAAQ,OAAK;AAAM,WAAS,KAAK;AAAM,cAAY,IAAI,MAAM,IAAI,WAAW,EAAE,IAAI;AAAU;AAC5I,CAAC;AAAiB,QAAM;AAAG,UAAQ;AAAG,SAAO,IAAI;AAAW,aAAW;AAAS;AAChF,CAFC,iBAEiB;AAAS,QAAM;AAAS,WAAS,IAAI;AAAM,UAAQ,IAAI,MAAM,IAAI;AAAS,iBAAe,IAAI;AAAc,cAAY;AAAM,SAAO,IAAI;AAAU,UAAQ;AAAS;AACrL,CAHC,iBAGiB,MAAM;AAAS,SAAO,IAAI;AAAW,gBAAc,IAAI;AAAW;AACpF,CAJC,iBAIiB,CAAC;AAAU,cAAY,IAAI;AAAW,SAAO,IAAI;AAAY,gBAAc;AAAa,eAAa;AAAK;AAC5H,CALC,iBAKiB,CADC,OACO;AAAS,SAAO,IAAI;AAAY,UAAQ,WAAW;AAAO;AASpF,CAAC;AAAU,WAAS;AAAM,kBAAgB;AAAQ,UAAQ,IAAI,MAAM,IAAI;AAAS,iBAAe,IAAI;AAAc,cAAY,IAAI;AAAS;AAC3I,CAAC;AAAc,WAAS;AAAM,eAAa;AAAQ,OAAK;AAAK,WAAS,IAAI;AAAK;AAI/E,CAAC;AACC,WAAS;AAAM,kBAAgB;AAAQ,OAAK;AAAK,eAAa;AAC9D,WAAS,IAAI,IAAI,IAAI;AACrB,cAAY,IAAI,MAAM,UAAU,GAAG,KAAK,EAAE,IAAI,QAAQ,GAAG,EAAE;AAC7D;AACA,CALC,WAKW,CAAC;AAAU,WAAS;AAAM;AAGtC,CAAC;AAAa,QAAM;AAAM,SAAO;AAAM,UAAQ;AAAM,UAAQ;AAAG,cAAY;AAAM,UAAQ;AAAS,WAAS;AAAM,eAAa;AAAQ,mBAAiB;AAAQ,WAAS;AAAG;AAC5K,CADC,UACU;AACT,WAAS;AAAI,SAAO;AAAG,UAAQ;AAC/B,eAAa,IAAI,MAAM;AAAc,cAAY,MAAM,MAAM;AAAa,iBAAe,MAAM,MAAM;AACrG,oBAAkB,IAAI;AAAK,cAAY,UAAU,MAAM;AACzD;AACA,CANC;AAMa,SAAO,IAAI;AAAU;AACnC,CAPC,UAOU;AAAQ,CAPlB,UAO6B,CAAC;AAAO,SAAO,IAAI;AAAW;AAC5D,CARC,UAQU,CADoB,IACf;AAAW,aAAW,OAAO;AAAQ;AAErD,CAtBC,YAsBY;AAAO,CAtBnB,YAsBgC;AAAQ,CAlBxC,YAkBqD;AAAO,CAlB5D,YAkByE;AACxE,QAAM;AAAS,aAAW;AAAS,cAAY,IAAI;AAAO,SAAO,IAAI;AACrE,UAAQ,IAAI,MAAM,IAAI;AAAS,iBAAe,IAAI;AAAc,WAAS,IAAI;AAC/E;AACA,CA1BC,YA0BY,KAAK,CAAC;AAAW,QAAM;AAAG,aAAW;AAAM;AACxD,CA3BC,YA2BY,KAAK,CAAC;AAAS,CAvB3B,YAuBwC,KAAK,CAA3B;AAAsC,gBAAc,IAAI;AAAW;AAItF,CA/BC,YA+BY,KAAK,CAAC;AAAS,CA3B3B,YA2BwC,KAAK,CAA3B;AAAsC,gBAAc,IAAI;AAAW;AACtF,CAhCC,YAgCY,KAAK;AAAgB,CAhCjC,YAgC8C,MAAM;AACrD,CA7BC,YA6BY,KAAK;AAAgB,CA7BjC,YA6B8C,MAAM;AAAiB,WAAS,IAAI,MAAM,IAAI;AAAW,kBAAgB;AAAG,gBAAc,IAAI;AAAW;AAMxJ,CAAC;AAAa,QAAM;AAAM,WAAS;AAAa,eAAa;AAAQ,mBAAiB;AAAQ,WAAS;AAAG,eAAa;AAAG,SAAO;AAAM,UAAQ;AAAM,UAAQ,IAAI,MAAM,IAAI;AAAS,iBAAe,IAAI;AAAc,cAAY;AAAM,SAAO,IAAI;AAAU,UAAQ;AAAS,aAAW;AAAQ;AAChS,CADC,UACU,MAAM,KAAK;AAAa,SAAO,IAAI;AAAW,gBAAc,IAAI;AAAW;AACtF,CAFC,UAEU;AAAY,WAAS;AAAK,UAAQ;AAAS;AACtD,CAHC,UAGU,CAAC,MAAM,MAAM,KAAK;AAAa,SAAO,IAAI;AAAY,cAAY,IAAI;AAAW,gBAAc,IAAI;AAAW;AAEzH,CAAC;AAAiB,WAAS;AAAM,eAAa;AAAQ,OAAK;AAAK,aAAW;AAAS;AACpF,CAAC;AAAiB,SAAO,IAAI;AAAU,aAAW;AAAM;AAExD,CAAC;AAAa,WAAS;AAAM,aAAW;AAAM,OAAK;AAAK,eAAa;AAAQ;AAC7E,CAAC;AAAY,WAAS;AAAa,eAAa;AAAQ,OAAK;AAAK,aAAW;AAAS,WAAS,IAAI,IAAI,IAAI;AAAK,UAAQ,IAAI,MAAM,IAAI;AAAS,iBAAe,IAAI;AAAgB,SAAO,IAAI;AAAQ;AACrM,CAAC;AAAc,UAAQ;AAAG,cAAY;AAAM,SAAO,IAAI;AAAU,UAAQ;AAAS,aAAW;AAAQ,WAAS,EAAE;AAAK;AACrH,CADC,WACW;AAAS,SAAO,IAAI;AAAW;AAC3C,CAAC;AAAkB,aAAW;AAAM;","names":[]}
@@ -39,9 +39,14 @@ function centeredOnPrimary(size) {
39
39
  function rescueToolWindow(w, def) {
40
40
  if (!w || w.isDestroyed()) return;
41
41
  if (w.isMinimized()) w.restore();
42
- w.setAlwaysOnTop(true);
43
42
  w.setBounds({ ...def, ...centeredOnPrimary(def) });
44
43
  w.show();
44
+ w.moveTop();
45
+ }
46
+ function pinToolWindow(w, parent, on) {
47
+ if (!w || w.isDestroyed()) return;
48
+ w.setAlwaysOnTop(false);
49
+ w.setParentWindow(on && parent && !parent.isDestroyed() ? parent : null);
45
50
  }
46
51
  function openToolWindow(existing, opts) {
47
52
  if (existing && !existing.isDestroyed()) {
@@ -54,7 +59,6 @@ function openToolWindow(existing, opts) {
54
59
  show: false,
55
60
  title: opts.title,
56
61
  ...opts.frame !== void 0 ? { frame: opts.frame } : {},
57
- ...opts.alwaysOnTop !== void 0 ? { alwaysOnTop: opts.alwaysOnTop } : {},
58
62
  webPreferences: {
59
63
  contextIsolation: true,
60
64
  nodeIntegration: false,
@@ -62,6 +66,7 @@ function openToolWindow(existing, opts) {
62
66
  preload: opts.preload
63
67
  }
64
68
  });
69
+ if (opts.pinTo !== void 0) pinToolWindow(w, opts.pinTo(), opts.pinned ?? false);
65
70
  if (opts.remember) rememberBounds(w, opts.remember);
66
71
  w.once("ready-to-show", () => w.show());
67
72
  if (opts.onClosed) w.on("closed", opts.onClosed);
@@ -72,6 +77,7 @@ function openToolWindow(existing, opts) {
72
77
 
73
78
  exports.centeredOnPrimary = centeredOnPrimary;
74
79
  exports.openToolWindow = openToolWindow;
80
+ exports.pinToolWindow = pinToolWindow;
75
81
  exports.rememberBounds = rememberBounds;
76
82
  exports.rescueToolWindow = rescueToolWindow;
77
83
  exports.savedWindowRect = savedWindowRect;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tool-window.ts"],"names":["screen","BrowserWindow","join"],"mappings":"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","file":"tool-window.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The tool-window kit, MAIN-PROCESS half: the detached helper-window\n// machinery both apps had grown four copies of (Patterpad's Play + Search +\n// Coverage, Storyletter's Board + Find). Remembered bounds with a\n// disconnected-monitor guard, debounced persistence, the Reset View rescue,\n// and the pinnable window factory they compose into.\n//\n// This module imports electron, so it ships as its own subpath\n// (@wildwinter/app-shell/tool-window) - never import it from renderer code.\n// The renderer half (pin button, .swin drag-bar grammar) lives in the root\n// export + tool-window.css.\n//\n// Two deliberate corrections over the Patterpad originals, for when it\n// migrates here: rescueToolWindow re-pins EVERY window it rescues (Patterpad\n// re-pinned only Play, so its store lied about Search + Coverage), and the\n// factory always wires closed-cleanup through an identity guard.\n// ---------------------------------------------------------------------------\n\nimport { BrowserWindow, screen } from \"electron\";\nimport { join } from \"node:path\";\n\nexport interface ToolWindowSize {\n width: number;\n height: number;\n}\n\nexport interface ToolWindowBounds {\n x?: number;\n y?: number;\n width: number;\n height: number;\n}\n\n/** The per-window store slice every app remembers: where it was, whether it\n * floats. (Patterpad's PlayWindowState, generalised.) */\nexport interface ToolWindowState {\n bounds?: ToolWindowBounds;\n pinned: boolean;\n}\n\n/** Use a remembered rect only if it still intersects a live display (a window\n * saved on a now-disconnected monitor must not open offscreen). Falls back\n * to the default size; a remembered size is kept even when the position is\n * dropped (Electron then centres it). */\nexport function savedWindowRect(\n saved: ToolWindowBounds | undefined,\n def: ToolWindowSize,\n min: ToolWindowSize,\n): ToolWindowBounds {\n if (!saved) return { ...def };\n const w = Math.max(min.width, saved.width), h = Math.max(min.height, saved.height);\n if (saved.x != null && saved.y != null) {\n const onScreen = screen.getAllDisplays().some((d) => {\n const a = d.workArea;\n return saved.x! + w > a.x + 40 && saved.x! < a.x + a.width - 40 && saved.y! + h > a.y + 20 && saved.y! < a.y + a.height - 20;\n });\n if (onScreen) return { x: saved.x, y: saved.y, width: w, height: h };\n }\n return { width: w, height: h };\n}\n\n/** Persist a helper window's bounds as the user moves / resizes / closes it\n * (debounced 400ms; close flushes synchronously). */\nexport function rememberBounds(w: BrowserWindow, write: (bounds: ToolWindowBounds) => void): void {\n const saveBounds = (): void => { if (!w.isDestroyed()) write(w.getBounds()); };\n let boundsTimer: ReturnType<typeof setTimeout> | undefined;\n const queueSave = (): void => { clearTimeout(boundsTimer); boundsTimer = setTimeout(saveBounds, 400); };\n w.on(\"resize\", queueSave);\n w.on(\"move\", queueSave);\n w.on(\"close\", () => { clearTimeout(boundsTimer); saveBounds(); });\n}\n\n/** Centre a rect on the primary display's work area. */\nexport function centeredOnPrimary(size: ToolWindowSize): { x: number; y: number } {\n const a = screen.getPrimaryDisplay().workArea;\n return { x: Math.round(a.x + (a.width - size.width) / 2), y: Math.round(a.y + (a.height - size.height) / 2) };\n}\n\n/** Reset View's per-window half: un-minimise, re-pin (always - the store\n * says pinned:true after a rescue, so the window must agree), default size,\n * centred on the primary display, shown. The host clears its remembered\n * bounds itself (that's a store concern). No focus steal: helpers show\n * behind the editor. */\nexport function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void {\n if (!w || w.isDestroyed()) return;\n if (w.isMinimized()) w.restore();\n w.setAlwaysOnTop(true);\n w.setBounds({ ...def, ...centeredOnPrimary(def) });\n w.show();\n}\n\nexport interface ToolWindowOptions {\n title: string;\n /** Renderer entry, relative to rendererDir (e.g. \"table.html\", \"search/index.html\"). */\n page: string;\n /** Absolute path of the built renderer dir (join(import.meta.dirname, \"../renderer\")). */\n rendererDir: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n rect: ToolWindowBounds;\n min?: ToolWindowSize;\n /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */\n frame?: boolean;\n /** Floats over the editor (the pin); remembered by the host. */\n alwaysOnTop?: boolean;\n /** Wire debounced bounds persistence into the host's store slice. */\n remember?: (bounds: ToolWindowBounds) => void;\n /** Fired when THIS window closes (identity-guarded; a stale close never\n * clears a newer window). Null the host's handle here. */\n onClosed?: () => void;\n}\n\n/** The pinnable detached-window factory (hardened webPreferences, one\n * preload, electron-vite's dev-URL/file split). Focuses and returns the\n * existing window when there is one; otherwise creates, wires, and loads. */\nexport function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow {\n if (existing && !existing.isDestroyed()) { existing.focus(); return existing; }\n const w = new BrowserWindow({\n ...opts.rect,\n ...(opts.min !== undefined ? { minWidth: opts.min.width, minHeight: opts.min.height } : {}),\n show: false,\n title: opts.title,\n ...(opts.frame !== undefined ? { frame: opts.frame } : {}),\n ...(opts.alwaysOnTop !== undefined ? { alwaysOnTop: opts.alwaysOnTop } : {}),\n webPreferences: {\n contextIsolation: true, nodeIntegration: false, sandbox: true,\n preload: opts.preload,\n },\n });\n if (opts.remember) rememberBounds(w, opts.remember);\n w.once(\"ready-to-show\", () => w.show());\n if (opts.onClosed) w.on(\"closed\", opts.onClosed);\n if (process.env[\"ELECTRON_RENDERER_URL\"]) void w.loadURL(`${process.env[\"ELECTRON_RENDERER_URL\"]}/${opts.page}`);\n else void w.loadFile(join(opts.rendererDir, opts.page));\n return w;\n}\n"]}
1
+ {"version":3,"sources":["../src/tool-window.ts"],"names":["screen","BrowserWindow","join"],"mappings":"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","file":"tool-window.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The tool-window kit, MAIN-PROCESS half: the detached helper-window\n// machinery both apps had grown four copies of (Patterpad's Play + Search +\n// Coverage, Storyletter's Board + Find). Remembered bounds with a\n// disconnected-monitor guard, debounced persistence, the Reset View rescue,\n// and the pinnable window factory they compose into.\n//\n// This module imports electron, so it ships as its own subpath\n// (@wildwinter/app-shell/tool-window) - never import it from renderer code.\n// The renderer half (pin button, .swin drag-bar grammar) lives in the root\n// export + tool-window.css.\n//\n// Two deliberate corrections over the Patterpad originals, for when it\n// migrates here: rescueToolWindow re-pins EVERY window it rescues (Patterpad\n// re-pinned only Play, so its store lied about Search + Coverage), and the\n// factory always wires closed-cleanup through an identity guard.\n// ---------------------------------------------------------------------------\n\nimport { BrowserWindow, screen } from \"electron\";\nimport { join } from \"node:path\";\n\nexport interface ToolWindowSize {\n width: number;\n height: number;\n}\n\nexport interface ToolWindowBounds {\n x?: number;\n y?: number;\n width: number;\n height: number;\n}\n\n/** The per-window store slice every app remembers: where it was, whether it\n * floats. (Patterpad's PlayWindowState, generalised.) */\nexport interface ToolWindowState {\n bounds?: ToolWindowBounds;\n pinned: boolean;\n}\n\n/** Use a remembered rect only if it still intersects a live display (a window\n * saved on a now-disconnected monitor must not open offscreen). Falls back\n * to the default size; a remembered size is kept even when the position is\n * dropped (Electron then centres it). */\nexport function savedWindowRect(\n saved: ToolWindowBounds | undefined,\n def: ToolWindowSize,\n min: ToolWindowSize,\n): ToolWindowBounds {\n if (!saved) return { ...def };\n const w = Math.max(min.width, saved.width), h = Math.max(min.height, saved.height);\n if (saved.x != null && saved.y != null) {\n const onScreen = screen.getAllDisplays().some((d) => {\n const a = d.workArea;\n return saved.x! + w > a.x + 40 && saved.x! < a.x + a.width - 40 && saved.y! + h > a.y + 20 && saved.y! < a.y + a.height - 20;\n });\n if (onScreen) return { x: saved.x, y: saved.y, width: w, height: h };\n }\n return { width: w, height: h };\n}\n\n/** Persist a helper window's bounds as the user moves / resizes / closes it\n * (debounced 400ms; close flushes synchronously). */\nexport function rememberBounds(w: BrowserWindow, write: (bounds: ToolWindowBounds) => void): void {\n const saveBounds = (): void => { if (!w.isDestroyed()) write(w.getBounds()); };\n let boundsTimer: ReturnType<typeof setTimeout> | undefined;\n const queueSave = (): void => { clearTimeout(boundsTimer); boundsTimer = setTimeout(saveBounds, 400); };\n w.on(\"resize\", queueSave);\n w.on(\"move\", queueSave);\n w.on(\"close\", () => { clearTimeout(boundsTimer); saveBounds(); });\n}\n\n/** Centre a rect on the primary display's work area. */\nexport function centeredOnPrimary(size: ToolWindowSize): { x: number; y: number } {\n const a = screen.getPrimaryDisplay().workArea;\n return { x: Math.round(a.x + (a.width - size.width) / 2), y: Math.round(a.y + (a.height - size.height) / 2) };\n}\n\n/** Reset View's per-window half: un-minimise, re-pin (always - the store\n * says pinned:true after a rescue, so the window must agree), default size,\n * centred on the primary display, shown. The host clears its remembered\n * bounds itself (that's a store concern). No focus steal: helpers show\n * behind the editor. */\nexport function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void {\n if (!w || w.isDestroyed()) return;\n if (w.isMinimized()) w.restore();\n w.setBounds({ ...def, ...centeredOnPrimary(def) });\n w.show();\n // Raise within OUR stack. This used to setAlwaysOnTop(true), which on macOS\n // and Windows floats the window above every other APPLICATION - and stuck\n // that way, since nothing ever unset it.\n w.moveTop();\n}\n\n/**\n * The pin: keep a tool window above THIS APP'S MAIN WINDOW, not above the\n * world. `alwaysOnTop` was the first implementation and it floats the window\n * over every other application on macOS and Windows, so a pinned Board meant\n * nothing else on the machine could come to the front (a Storyletter user\n * report, 2026-08-25). A CHILD window is the semantic actually wanted on both\n * platforms: it stays over its parent and stacks normally against everything\n * else. Clearing any alwaysOnTop first also heals a window an older build\n * pinned the old way.\n */\nexport function pinToolWindow(\n w: BrowserWindow | undefined | null, parent: BrowserWindow | undefined | null, on: boolean,\n): void {\n if (!w || w.isDestroyed()) return;\n w.setAlwaysOnTop(false);\n w.setParentWindow(on && parent && !parent.isDestroyed() ? parent : null);\n}\n\nexport interface ToolWindowOptions {\n title: string;\n /** Renderer entry, relative to rendererDir (e.g. \"table.html\", \"search/index.html\"). */\n page: string;\n /** Absolute path of the built renderer dir (join(import.meta.dirname, \"../renderer\")). */\n rendererDir: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n rect: ToolWindowBounds;\n min?: ToolWindowSize;\n /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */\n frame?: boolean;\n /** The window to PIN ABOVE: the app's main window, resolved at open time (a\n * getter, because the main window can be recreated). With `pinned` true the\n * new tool window is parented to it, which keeps it above the editor and\n * stacks it normally against every other application - the semantic\n * `alwaysOnTop` (this option's late predecessor) got wrong on macOS and\n * Windows, where it floats over the whole machine. */\n pinTo?: () => BrowserWindow | undefined;\n /** Start pinned (the host remembers); flip later with `pinToolWindow`. */\n pinned?: boolean;\n /** Wire debounced bounds persistence into the host's store slice. */\n remember?: (bounds: ToolWindowBounds) => void;\n /** Fired when THIS window closes (identity-guarded; a stale close never\n * clears a newer window). Null the host's handle here. */\n onClosed?: () => void;\n}\n\n/** The pinnable detached-window factory (hardened webPreferences, one\n * preload, electron-vite's dev-URL/file split). Focuses and returns the\n * existing window when there is one; otherwise creates, wires, and loads. */\nexport function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow {\n if (existing && !existing.isDestroyed()) { existing.focus(); return existing; }\n const w = new BrowserWindow({\n ...opts.rect,\n ...(opts.min !== undefined ? { minWidth: opts.min.width, minHeight: opts.min.height } : {}),\n show: false,\n title: opts.title,\n ...(opts.frame !== undefined ? { frame: opts.frame } : {}),\n webPreferences: {\n contextIsolation: true, nodeIntegration: false, sandbox: true,\n preload: opts.preload,\n },\n });\n if (opts.pinTo !== undefined) pinToolWindow(w, opts.pinTo(), opts.pinned ?? false);\n if (opts.remember) rememberBounds(w, opts.remember);\n w.once(\"ready-to-show\", () => w.show());\n if (opts.onClosed) w.on(\"closed\", opts.onClosed);\n if (process.env[\"ELECTRON_RENDERER_URL\"]) void w.loadURL(`${process.env[\"ELECTRON_RENDERER_URL\"]}/${opts.page}`);\n else void w.loadFile(join(opts.rendererDir, opts.page));\n return w;\n}\n"]}
@@ -35,6 +35,17 @@ declare function centeredOnPrimary(size: ToolWindowSize): {
35
35
  * bounds itself (that's a store concern). No focus steal: helpers show
36
36
  * behind the editor. */
37
37
  declare function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void;
38
+ /**
39
+ * The pin: keep a tool window above THIS APP'S MAIN WINDOW, not above the
40
+ * world. `alwaysOnTop` was the first implementation and it floats the window
41
+ * over every other application on macOS and Windows, so a pinned Board meant
42
+ * nothing else on the machine could come to the front (a Storyletter user
43
+ * report, 2026-08-25). A CHILD window is the semantic actually wanted on both
44
+ * platforms: it stays over its parent and stacks normally against everything
45
+ * else. Clearing any alwaysOnTop first also heals a window an older build
46
+ * pinned the old way.
47
+ */
48
+ declare function pinToolWindow(w: BrowserWindow | undefined | null, parent: BrowserWindow | undefined | null, on: boolean): void;
38
49
  interface ToolWindowOptions {
39
50
  title: string;
40
51
  /** Renderer entry, relative to rendererDir (e.g. "table.html", "search/index.html"). */
@@ -47,8 +58,15 @@ interface ToolWindowOptions {
47
58
  min?: ToolWindowSize;
48
59
  /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */
49
60
  frame?: boolean;
50
- /** Floats over the editor (the pin); remembered by the host. */
51
- alwaysOnTop?: boolean;
61
+ /** The window to PIN ABOVE: the app's main window, resolved at open time (a
62
+ * getter, because the main window can be recreated). With `pinned` true the
63
+ * new tool window is parented to it, which keeps it above the editor and
64
+ * stacks it normally against every other application - the semantic
65
+ * `alwaysOnTop` (this option's late predecessor) got wrong on macOS and
66
+ * Windows, where it floats over the whole machine. */
67
+ pinTo?: () => BrowserWindow | undefined;
68
+ /** Start pinned (the host remembers); flip later with `pinToolWindow`. */
69
+ pinned?: boolean;
52
70
  /** Wire debounced bounds persistence into the host's store slice. */
53
71
  remember?: (bounds: ToolWindowBounds) => void;
54
72
  /** Fired when THIS window closes (identity-guarded; a stale close never
@@ -60,4 +78,4 @@ interface ToolWindowOptions {
60
78
  * existing window when there is one; otherwise creates, wires, and loads. */
61
79
  declare function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow;
62
80
 
63
- export { type ToolWindowBounds, type ToolWindowOptions, type ToolWindowSize, type ToolWindowState, centeredOnPrimary, openToolWindow, rememberBounds, rescueToolWindow, savedWindowRect };
81
+ export { type ToolWindowBounds, type ToolWindowOptions, type ToolWindowSize, type ToolWindowState, centeredOnPrimary, openToolWindow, pinToolWindow, rememberBounds, rescueToolWindow, savedWindowRect };
@@ -35,6 +35,17 @@ declare function centeredOnPrimary(size: ToolWindowSize): {
35
35
  * bounds itself (that's a store concern). No focus steal: helpers show
36
36
  * behind the editor. */
37
37
  declare function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void;
38
+ /**
39
+ * The pin: keep a tool window above THIS APP'S MAIN WINDOW, not above the
40
+ * world. `alwaysOnTop` was the first implementation and it floats the window
41
+ * over every other application on macOS and Windows, so a pinned Board meant
42
+ * nothing else on the machine could come to the front (a Storyletter user
43
+ * report, 2026-08-25). A CHILD window is the semantic actually wanted on both
44
+ * platforms: it stays over its parent and stacks normally against everything
45
+ * else. Clearing any alwaysOnTop first also heals a window an older build
46
+ * pinned the old way.
47
+ */
48
+ declare function pinToolWindow(w: BrowserWindow | undefined | null, parent: BrowserWindow | undefined | null, on: boolean): void;
38
49
  interface ToolWindowOptions {
39
50
  title: string;
40
51
  /** Renderer entry, relative to rendererDir (e.g. "table.html", "search/index.html"). */
@@ -47,8 +58,15 @@ interface ToolWindowOptions {
47
58
  min?: ToolWindowSize;
48
59
  /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */
49
60
  frame?: boolean;
50
- /** Floats over the editor (the pin); remembered by the host. */
51
- alwaysOnTop?: boolean;
61
+ /** The window to PIN ABOVE: the app's main window, resolved at open time (a
62
+ * getter, because the main window can be recreated). With `pinned` true the
63
+ * new tool window is parented to it, which keeps it above the editor and
64
+ * stacks it normally against every other application - the semantic
65
+ * `alwaysOnTop` (this option's late predecessor) got wrong on macOS and
66
+ * Windows, where it floats over the whole machine. */
67
+ pinTo?: () => BrowserWindow | undefined;
68
+ /** Start pinned (the host remembers); flip later with `pinToolWindow`. */
69
+ pinned?: boolean;
52
70
  /** Wire debounced bounds persistence into the host's store slice. */
53
71
  remember?: (bounds: ToolWindowBounds) => void;
54
72
  /** Fired when THIS window closes (identity-guarded; a stale close never
@@ -60,4 +78,4 @@ interface ToolWindowOptions {
60
78
  * existing window when there is one; otherwise creates, wires, and loads. */
61
79
  declare function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow;
62
80
 
63
- export { type ToolWindowBounds, type ToolWindowOptions, type ToolWindowSize, type ToolWindowState, centeredOnPrimary, openToolWindow, rememberBounds, rescueToolWindow, savedWindowRect };
81
+ export { type ToolWindowBounds, type ToolWindowOptions, type ToolWindowSize, type ToolWindowState, centeredOnPrimary, openToolWindow, pinToolWindow, rememberBounds, rescueToolWindow, savedWindowRect };
@@ -37,9 +37,14 @@ function centeredOnPrimary(size) {
37
37
  function rescueToolWindow(w, def) {
38
38
  if (!w || w.isDestroyed()) return;
39
39
  if (w.isMinimized()) w.restore();
40
- w.setAlwaysOnTop(true);
41
40
  w.setBounds({ ...def, ...centeredOnPrimary(def) });
42
41
  w.show();
42
+ w.moveTop();
43
+ }
44
+ function pinToolWindow(w, parent, on) {
45
+ if (!w || w.isDestroyed()) return;
46
+ w.setAlwaysOnTop(false);
47
+ w.setParentWindow(on && parent && !parent.isDestroyed() ? parent : null);
43
48
  }
44
49
  function openToolWindow(existing, opts) {
45
50
  if (existing && !existing.isDestroyed()) {
@@ -52,7 +57,6 @@ function openToolWindow(existing, opts) {
52
57
  show: false,
53
58
  title: opts.title,
54
59
  ...opts.frame !== void 0 ? { frame: opts.frame } : {},
55
- ...opts.alwaysOnTop !== void 0 ? { alwaysOnTop: opts.alwaysOnTop } : {},
56
60
  webPreferences: {
57
61
  contextIsolation: true,
58
62
  nodeIntegration: false,
@@ -60,6 +64,7 @@ function openToolWindow(existing, opts) {
60
64
  preload: opts.preload
61
65
  }
62
66
  });
67
+ if (opts.pinTo !== void 0) pinToolWindow(w, opts.pinTo(), opts.pinned ?? false);
63
68
  if (opts.remember) rememberBounds(w, opts.remember);
64
69
  w.once("ready-to-show", () => w.show());
65
70
  if (opts.onClosed) w.on("closed", opts.onClosed);
@@ -68,6 +73,6 @@ function openToolWindow(existing, opts) {
68
73
  return w;
69
74
  }
70
75
 
71
- export { centeredOnPrimary, openToolWindow, rememberBounds, rescueToolWindow, savedWindowRect };
76
+ export { centeredOnPrimary, openToolWindow, pinToolWindow, rememberBounds, rescueToolWindow, savedWindowRect };
72
77
  //# sourceMappingURL=tool-window.js.map
73
78
  //# sourceMappingURL=tool-window.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tool-window.ts"],"names":[],"mappings":"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","file":"tool-window.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The tool-window kit, MAIN-PROCESS half: the detached helper-window\n// machinery both apps had grown four copies of (Patterpad's Play + Search +\n// Coverage, Storyletter's Board + Find). Remembered bounds with a\n// disconnected-monitor guard, debounced persistence, the Reset View rescue,\n// and the pinnable window factory they compose into.\n//\n// This module imports electron, so it ships as its own subpath\n// (@wildwinter/app-shell/tool-window) - never import it from renderer code.\n// The renderer half (pin button, .swin drag-bar grammar) lives in the root\n// export + tool-window.css.\n//\n// Two deliberate corrections over the Patterpad originals, for when it\n// migrates here: rescueToolWindow re-pins EVERY window it rescues (Patterpad\n// re-pinned only Play, so its store lied about Search + Coverage), and the\n// factory always wires closed-cleanup through an identity guard.\n// ---------------------------------------------------------------------------\n\nimport { BrowserWindow, screen } from \"electron\";\nimport { join } from \"node:path\";\n\nexport interface ToolWindowSize {\n width: number;\n height: number;\n}\n\nexport interface ToolWindowBounds {\n x?: number;\n y?: number;\n width: number;\n height: number;\n}\n\n/** The per-window store slice every app remembers: where it was, whether it\n * floats. (Patterpad's PlayWindowState, generalised.) */\nexport interface ToolWindowState {\n bounds?: ToolWindowBounds;\n pinned: boolean;\n}\n\n/** Use a remembered rect only if it still intersects a live display (a window\n * saved on a now-disconnected monitor must not open offscreen). Falls back\n * to the default size; a remembered size is kept even when the position is\n * dropped (Electron then centres it). */\nexport function savedWindowRect(\n saved: ToolWindowBounds | undefined,\n def: ToolWindowSize,\n min: ToolWindowSize,\n): ToolWindowBounds {\n if (!saved) return { ...def };\n const w = Math.max(min.width, saved.width), h = Math.max(min.height, saved.height);\n if (saved.x != null && saved.y != null) {\n const onScreen = screen.getAllDisplays().some((d) => {\n const a = d.workArea;\n return saved.x! + w > a.x + 40 && saved.x! < a.x + a.width - 40 && saved.y! + h > a.y + 20 && saved.y! < a.y + a.height - 20;\n });\n if (onScreen) return { x: saved.x, y: saved.y, width: w, height: h };\n }\n return { width: w, height: h };\n}\n\n/** Persist a helper window's bounds as the user moves / resizes / closes it\n * (debounced 400ms; close flushes synchronously). */\nexport function rememberBounds(w: BrowserWindow, write: (bounds: ToolWindowBounds) => void): void {\n const saveBounds = (): void => { if (!w.isDestroyed()) write(w.getBounds()); };\n let boundsTimer: ReturnType<typeof setTimeout> | undefined;\n const queueSave = (): void => { clearTimeout(boundsTimer); boundsTimer = setTimeout(saveBounds, 400); };\n w.on(\"resize\", queueSave);\n w.on(\"move\", queueSave);\n w.on(\"close\", () => { clearTimeout(boundsTimer); saveBounds(); });\n}\n\n/** Centre a rect on the primary display's work area. */\nexport function centeredOnPrimary(size: ToolWindowSize): { x: number; y: number } {\n const a = screen.getPrimaryDisplay().workArea;\n return { x: Math.round(a.x + (a.width - size.width) / 2), y: Math.round(a.y + (a.height - size.height) / 2) };\n}\n\n/** Reset View's per-window half: un-minimise, re-pin (always - the store\n * says pinned:true after a rescue, so the window must agree), default size,\n * centred on the primary display, shown. The host clears its remembered\n * bounds itself (that's a store concern). No focus steal: helpers show\n * behind the editor. */\nexport function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void {\n if (!w || w.isDestroyed()) return;\n if (w.isMinimized()) w.restore();\n w.setAlwaysOnTop(true);\n w.setBounds({ ...def, ...centeredOnPrimary(def) });\n w.show();\n}\n\nexport interface ToolWindowOptions {\n title: string;\n /** Renderer entry, relative to rendererDir (e.g. \"table.html\", \"search/index.html\"). */\n page: string;\n /** Absolute path of the built renderer dir (join(import.meta.dirname, \"../renderer\")). */\n rendererDir: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n rect: ToolWindowBounds;\n min?: ToolWindowSize;\n /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */\n frame?: boolean;\n /** Floats over the editor (the pin); remembered by the host. */\n alwaysOnTop?: boolean;\n /** Wire debounced bounds persistence into the host's store slice. */\n remember?: (bounds: ToolWindowBounds) => void;\n /** Fired when THIS window closes (identity-guarded; a stale close never\n * clears a newer window). Null the host's handle here. */\n onClosed?: () => void;\n}\n\n/** The pinnable detached-window factory (hardened webPreferences, one\n * preload, electron-vite's dev-URL/file split). Focuses and returns the\n * existing window when there is one; otherwise creates, wires, and loads. */\nexport function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow {\n if (existing && !existing.isDestroyed()) { existing.focus(); return existing; }\n const w = new BrowserWindow({\n ...opts.rect,\n ...(opts.min !== undefined ? { minWidth: opts.min.width, minHeight: opts.min.height } : {}),\n show: false,\n title: opts.title,\n ...(opts.frame !== undefined ? { frame: opts.frame } : {}),\n ...(opts.alwaysOnTop !== undefined ? { alwaysOnTop: opts.alwaysOnTop } : {}),\n webPreferences: {\n contextIsolation: true, nodeIntegration: false, sandbox: true,\n preload: opts.preload,\n },\n });\n if (opts.remember) rememberBounds(w, opts.remember);\n w.once(\"ready-to-show\", () => w.show());\n if (opts.onClosed) w.on(\"closed\", opts.onClosed);\n if (process.env[\"ELECTRON_RENDERER_URL\"]) void w.loadURL(`${process.env[\"ELECTRON_RENDERER_URL\"]}/${opts.page}`);\n else void w.loadFile(join(opts.rendererDir, opts.page));\n return w;\n}\n"]}
1
+ {"version":3,"sources":["../src/tool-window.ts"],"names":[],"mappings":"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","file":"tool-window.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The tool-window kit, MAIN-PROCESS half: the detached helper-window\n// machinery both apps had grown four copies of (Patterpad's Play + Search +\n// Coverage, Storyletter's Board + Find). Remembered bounds with a\n// disconnected-monitor guard, debounced persistence, the Reset View rescue,\n// and the pinnable window factory they compose into.\n//\n// This module imports electron, so it ships as its own subpath\n// (@wildwinter/app-shell/tool-window) - never import it from renderer code.\n// The renderer half (pin button, .swin drag-bar grammar) lives in the root\n// export + tool-window.css.\n//\n// Two deliberate corrections over the Patterpad originals, for when it\n// migrates here: rescueToolWindow re-pins EVERY window it rescues (Patterpad\n// re-pinned only Play, so its store lied about Search + Coverage), and the\n// factory always wires closed-cleanup through an identity guard.\n// ---------------------------------------------------------------------------\n\nimport { BrowserWindow, screen } from \"electron\";\nimport { join } from \"node:path\";\n\nexport interface ToolWindowSize {\n width: number;\n height: number;\n}\n\nexport interface ToolWindowBounds {\n x?: number;\n y?: number;\n width: number;\n height: number;\n}\n\n/** The per-window store slice every app remembers: where it was, whether it\n * floats. (Patterpad's PlayWindowState, generalised.) */\nexport interface ToolWindowState {\n bounds?: ToolWindowBounds;\n pinned: boolean;\n}\n\n/** Use a remembered rect only if it still intersects a live display (a window\n * saved on a now-disconnected monitor must not open offscreen). Falls back\n * to the default size; a remembered size is kept even when the position is\n * dropped (Electron then centres it). */\nexport function savedWindowRect(\n saved: ToolWindowBounds | undefined,\n def: ToolWindowSize,\n min: ToolWindowSize,\n): ToolWindowBounds {\n if (!saved) return { ...def };\n const w = Math.max(min.width, saved.width), h = Math.max(min.height, saved.height);\n if (saved.x != null && saved.y != null) {\n const onScreen = screen.getAllDisplays().some((d) => {\n const a = d.workArea;\n return saved.x! + w > a.x + 40 && saved.x! < a.x + a.width - 40 && saved.y! + h > a.y + 20 && saved.y! < a.y + a.height - 20;\n });\n if (onScreen) return { x: saved.x, y: saved.y, width: w, height: h };\n }\n return { width: w, height: h };\n}\n\n/** Persist a helper window's bounds as the user moves / resizes / closes it\n * (debounced 400ms; close flushes synchronously). */\nexport function rememberBounds(w: BrowserWindow, write: (bounds: ToolWindowBounds) => void): void {\n const saveBounds = (): void => { if (!w.isDestroyed()) write(w.getBounds()); };\n let boundsTimer: ReturnType<typeof setTimeout> | undefined;\n const queueSave = (): void => { clearTimeout(boundsTimer); boundsTimer = setTimeout(saveBounds, 400); };\n w.on(\"resize\", queueSave);\n w.on(\"move\", queueSave);\n w.on(\"close\", () => { clearTimeout(boundsTimer); saveBounds(); });\n}\n\n/** Centre a rect on the primary display's work area. */\nexport function centeredOnPrimary(size: ToolWindowSize): { x: number; y: number } {\n const a = screen.getPrimaryDisplay().workArea;\n return { x: Math.round(a.x + (a.width - size.width) / 2), y: Math.round(a.y + (a.height - size.height) / 2) };\n}\n\n/** Reset View's per-window half: un-minimise, re-pin (always - the store\n * says pinned:true after a rescue, so the window must agree), default size,\n * centred on the primary display, shown. The host clears its remembered\n * bounds itself (that's a store concern). No focus steal: helpers show\n * behind the editor. */\nexport function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void {\n if (!w || w.isDestroyed()) return;\n if (w.isMinimized()) w.restore();\n w.setBounds({ ...def, ...centeredOnPrimary(def) });\n w.show();\n // Raise within OUR stack. This used to setAlwaysOnTop(true), which on macOS\n // and Windows floats the window above every other APPLICATION - and stuck\n // that way, since nothing ever unset it.\n w.moveTop();\n}\n\n/**\n * The pin: keep a tool window above THIS APP'S MAIN WINDOW, not above the\n * world. `alwaysOnTop` was the first implementation and it floats the window\n * over every other application on macOS and Windows, so a pinned Board meant\n * nothing else on the machine could come to the front (a Storyletter user\n * report, 2026-08-25). A CHILD window is the semantic actually wanted on both\n * platforms: it stays over its parent and stacks normally against everything\n * else. Clearing any alwaysOnTop first also heals a window an older build\n * pinned the old way.\n */\nexport function pinToolWindow(\n w: BrowserWindow | undefined | null, parent: BrowserWindow | undefined | null, on: boolean,\n): void {\n if (!w || w.isDestroyed()) return;\n w.setAlwaysOnTop(false);\n w.setParentWindow(on && parent && !parent.isDestroyed() ? parent : null);\n}\n\nexport interface ToolWindowOptions {\n title: string;\n /** Renderer entry, relative to rendererDir (e.g. \"table.html\", \"search/index.html\"). */\n page: string;\n /** Absolute path of the built renderer dir (join(import.meta.dirname, \"../renderer\")). */\n rendererDir: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n rect: ToolWindowBounds;\n min?: ToolWindowSize;\n /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */\n frame?: boolean;\n /** The window to PIN ABOVE: the app's main window, resolved at open time (a\n * getter, because the main window can be recreated). With `pinned` true the\n * new tool window is parented to it, which keeps it above the editor and\n * stacks it normally against every other application - the semantic\n * `alwaysOnTop` (this option's late predecessor) got wrong on macOS and\n * Windows, where it floats over the whole machine. */\n pinTo?: () => BrowserWindow | undefined;\n /** Start pinned (the host remembers); flip later with `pinToolWindow`. */\n pinned?: boolean;\n /** Wire debounced bounds persistence into the host's store slice. */\n remember?: (bounds: ToolWindowBounds) => void;\n /** Fired when THIS window closes (identity-guarded; a stale close never\n * clears a newer window). Null the host's handle here. */\n onClosed?: () => void;\n}\n\n/** The pinnable detached-window factory (hardened webPreferences, one\n * preload, electron-vite's dev-URL/file split). Focuses and returns the\n * existing window when there is one; otherwise creates, wires, and loads. */\nexport function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow {\n if (existing && !existing.isDestroyed()) { existing.focus(); return existing; }\n const w = new BrowserWindow({\n ...opts.rect,\n ...(opts.min !== undefined ? { minWidth: opts.min.width, minHeight: opts.min.height } : {}),\n show: false,\n title: opts.title,\n ...(opts.frame !== undefined ? { frame: opts.frame } : {}),\n webPreferences: {\n contextIsolation: true, nodeIntegration: false, sandbox: true,\n preload: opts.preload,\n },\n });\n if (opts.pinTo !== undefined) pinToolWindow(w, opts.pinTo(), opts.pinned ?? false);\n if (opts.remember) rememberBounds(w, opts.remember);\n w.once(\"ready-to-show\", () => w.show());\n if (opts.onClosed) w.on(\"closed\", opts.onClosed);\n if (process.env[\"ELECTRON_RENDERER_URL\"]) void w.loadURL(`${process.env[\"ELECTRON_RENDERER_URL\"]}/${opts.page}`);\n else void w.loadFile(join(opts.rendererDir, opts.page));\n return w;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wildwinter/app-shell",
3
- "version": "0.32.0",
3
+ "version": "0.33.1",
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",
@@ -132,6 +132,7 @@
132
132
  "access": "public"
133
133
  },
134
134
  "scripts": {
135
+ "prepare": "git config core.hooksPath scripts/git-hooks || true",
135
136
  "build": "tsup",
136
137
  "test": "vitest run",
137
138
  "typecheck": "tsc --noEmit",