isotone 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.2
4
+
5
+ - Clarify that existing projects must move the whole local data directory,
6
+ including knob definitions and active variant state.
7
+ - Remove the obsolete overlay selector management client after its server
8
+ endpoint was retired.
9
+ - Keep `lastSave` scoped to the note just saved while the panel independently
10
+ reports older unsynced notes.
11
+
3
12
  ## 0.4.1
4
13
 
5
14
  - Keep long feedback in the composer with a clear 4,000-character limit; older
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  OVERLAY_GROUP_ID
3
- } from "./chunk-SNGBNVH2.js";
3
+ } from "./chunk-OSETWKGE.js";
4
4
  import {
5
5
  ensureFileIgnored,
6
6
  withFileLock,
@@ -524,4 +524,4 @@ export {
524
524
  writeActive,
525
525
  variantsResponse
526
526
  };
527
- //# sourceMappingURL=chunk-ZBULKB3S.js.map
527
+ //# sourceMappingURL=chunk-DD6BKC3R.js.map
@@ -7,7 +7,7 @@ import { randomUUID } from "crypto";
7
7
 
8
8
  // package.json
9
9
  var name = "isotone";
10
- var version = "0.4.1";
10
+ var version = "0.4.2";
11
11
 
12
12
  // src/update.ts
13
13
  var UPDATE_ENDPOINT = "/api/iteration/update";
@@ -228,4 +228,4 @@ export {
228
228
  checkUpdate,
229
229
  startUpdateCheck
230
230
  };
231
- //# sourceMappingURL=chunk-UMVPMPVE.js.map
231
+ //# sourceMappingURL=chunk-LHCQL5PA.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/update.ts","../package.json"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { mkdir, open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { randomUUID } from 'node:crypto';\nimport type { UpdateInfo } from './core/update';\nimport { name as packageName, version as currentVersion } from '../package.json';\n\nexport const UPDATE_ENDPOINT = '/api/iteration/update';\nconst REGISTRY_URL = `https://registry.npmjs.org/${packageName}/latest`;\nconst CHECK_TIMEOUT = 5_000;\nconst CACHE_TTL = 24 * 60 * 60 * 1000;\nconst CACHE_FILE = join(tmpdir(), `isotone-update-${process.getuid?.() ?? 'user'}-v1.json`);\n\ntype Manager = 'npm' | 'pnpm' | 'yarn';\nexport type { UpdateInfo } from './core/update';\n\ninterface Version { parts: bigint[]; prerelease: string[] }\nfunction parseVersion(value: string): Version {\n if (value.length > 256) throw new Error('Invalid version.');\n const match = /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-([\\da-zA-Z-]+(?:\\.[\\da-zA-Z-]+)*))?(?:\\+([\\da-zA-Z-]+(?:\\.[\\da-zA-Z-]+)*))?$/.exec(value);\n if (!match) throw new Error('Invalid version.');\n const prerelease = match[4]?.split('.') ?? [];\n if (prerelease.some((part) => /^0\\d+$/.test(part))) throw new Error('Invalid version.');\n return { parts: match.slice(1, 4).map(BigInt), prerelease };\n}\n\n/** SemVer precedence, including numeric prereleases and ignored build metadata. */\nexport function compareVersions(a: string, b: string): number {\n const left = parseVersion(a), right = parseVersion(b);\n for (let i = 0; i < 3; i++) {\n if (left.parts[i] !== right.parts[i]) return left.parts[i] > right.parts[i] ? 1 : -1;\n }\n if (!left.prerelease.length || !right.prerelease.length) {\n return left.prerelease.length === right.prerelease.length ? 0 : left.prerelease.length ? -1 : 1;\n }\n for (let i = 0; i < Math.max(left.prerelease.length, right.prerelease.length); i++) {\n const x = left.prerelease[i], y = right.prerelease[i];\n if (x === y) continue;\n if (x === undefined || y === undefined) return x === undefined ? -1 : 1;\n const nx = /^\\d+$/.test(x), ny = /^\\d+$/.test(y);\n if (nx && ny) return BigInt(x) > BigInt(y) ? 1 : -1;\n if (nx !== ny) return nx ? -1 : 1;\n return x > y ? 1 : -1;\n }\n return 0;\n}\n\nfunction manifest(directory: string): Record<string, unknown> | null {\n try {\n const value = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));\n return value && typeof value === 'object' && !Array.isArray(value) ? value : null;\n } catch { return null; }\n}\n\nfunction findProject(cwd: string): { directory: string; json: Record<string, unknown> | null } {\n let directory = resolve(cwd);\n while (true) {\n const json = manifest(directory);\n if (json) return { directory, json };\n if (dirname(directory) === directory || existsSync(join(directory, '.git'))) return { directory: resolve(cwd), json: null };\n directory = dirname(directory);\n }\n}\n\nfunction dependencyOwner(project: ReturnType<typeof findProject>): ReturnType<typeof findProject> {\n let directory = project.directory;\n while (true) {\n const json = manifest(directory);\n if (['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'].some((key) => {\n const dependencies = json?.[key];\n return dependencies && typeof dependencies === 'object' && Object.hasOwn(dependencies, packageName);\n })) return { directory, json };\n if (dirname(directory) === directory || existsSync(join(directory, '.git'))) return project;\n directory = dirname(directory);\n }\n}\n\nexport function upgradePlan(cwd: string, latest: string, override?: Manager): Pick<UpdateInfo, 'upgrade' | 'message'> {\n parseVersion(latest); // Only a validated version can enter a copied command.\n const project = dependencyOwner(findProject(cwd));\n if (project.json?.name === packageName) return { upgrade: null, message: 'This is the source checkout; update its source instead of adding itself as a dependency.' };\n let manager = override;\n let directory = project.directory;\n const locks = new Set<Manager>();\n while (!manager) {\n const declared = manifest(directory)?.packageManager;\n if (typeof declared === 'string') {\n const match = /^(npm|pnpm|yarn)@/.exec(declared);\n if (!match) return { upgrade: null, message: 'The declared package manager is not supported by this command. Upgrade with your project’s package manager.' };\n manager = match[1] as Manager;\n break;\n }\n if (['bun.lock', 'bun.lockb'].some((file) => existsSync(join(directory, file)))) return { upgrade: null, message: 'This project uses Bun. Upgrade with the project’s package manager.' };\n if (existsSync(join(directory, 'pnpm-lock.yaml'))) locks.add('pnpm');\n if (existsSync(join(directory, 'yarn.lock'))) locks.add('yarn');\n if (['package-lock.json', 'npm-shrinkwrap.json'].some((file) => existsSync(join(directory, file)))) locks.add('npm');\n if (locks.size || dirname(directory) === directory || existsSync(join(directory, '.git'))) break;\n directory = dirname(directory);\n }\n if (!manager && locks.size > 1) return { upgrade: null, message: 'Multiple package-manager lockfiles found. Pass --manager npm, pnpm or yarn.' };\n manager ??= [...locks][0] ?? 'npm';\n const production = project.json?.dependencies as Record<string, unknown> | undefined;\n const optional = project.json?.optionalDependencies as Record<string, unknown> | undefined;\n const peers = project.json?.peerDependencies as Record<string, unknown> | undefined;\n if (optional?.[packageName] || peers?.[packageName]) return { upgrade: null, message: 'Review this optional/peer dependency manually with your package manager.' };\n const dev = production?.[packageName] ? '' : ' -D';\n let workspace = manager === 'pnpm' && existsSync(join(project.directory, 'pnpm-workspace.yaml')) ? ' -w' : '';\n if (manager === 'yarn' && project.json?.workspaces) {\n const declared = project.json.packageManager;\n if (typeof declared !== 'string' || !/^yarn@\\d+\\./.test(declared)) return { upgrade: null, message: 'Declare the Yarn version in packageManager before updating a workspace-root dependency; Yarn Classic and modern Yarn use different root flags.' };\n if (declared.startsWith('yarn@1.')) workspace = ' -W';\n }\n const command = manager === 'npm'\n ? `npm install${dev} --save-exact ${packageName}@${latest}`\n : `${manager} add${dev}${workspace} --exact ${packageName}@${latest}`;\n return { upgrade: { manager, command, cwd: project.directory } };\n}\n\nexport async function fetchLatest(fetcher: typeof fetch = fetch, signal?: AbortSignal): Promise<string> {\n const controller = new AbortController();\n const abort = () => controller.abort();\n if (signal?.aborted) abort();\n else signal?.addEventListener('abort', abort, { once: true });\n const timer = setTimeout(abort, CHECK_TIMEOUT);\n try {\n if (controller.signal.aborted) throw new Error('Update check cancelled.');\n const response = await fetcher(REGISTRY_URL, { signal: controller.signal, redirect: 'error', headers: { accept: 'application/json' } });\n if (!response.ok) throw new Error(`Registry returned HTTP ${response.status}.`);\n if (!response.body) throw new Error('Registry returned an empty response.');\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let bytes = 0;\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n bytes += value.byteLength;\n if (bytes > 64 * 1024) {\n await reader.cancel();\n throw new Error('Registry response was too large.');\n }\n chunks.push(value);\n }\n } finally { reader.releaseLock(); }\n const data = JSON.parse(Buffer.concat(chunks).toString('utf8'));\n if (typeof data.version !== 'string') throw new Error('Registry returned no version.');\n parseVersion(data.version);\n return data.version;\n } finally { clearTimeout(timer); signal?.removeEventListener('abort', abort); }\n}\n\nexport function updateInfo(latest: string, cwd: string, manager?: Manager): UpdateInfo {\n const order = compareVersions(latest, currentVersion);\n return {\n currentVersion, latestVersion: latest,\n status: order > 0 ? 'available' : order < 0 ? 'ahead' : 'current',\n ...(order > 0 ? upgradePlan(cwd, latest, manager) : { upgrade: null }),\n };\n}\n\nexport async function checkUpdate(cwd: string, manager?: Manager, fetcher?: typeof fetch, signal?: AbortSignal): Promise<UpdateInfo> {\n try { return updateInfo(await fetchLatest(fetcher, signal), cwd, manager); }\n catch { return { currentVersion, latestVersion: null, status: 'unavailable', upgrade: null, message: 'Could not check npm for updates. The installed version remains usable; try again when online.' }; }\n}\n\ninterface CachedCheck { checkedAt: number; latestVersion: string | null }\nasync function readCache(file: string): Promise<CachedCheck | null> {\n try {\n if ((await stat(file)).size > 4096) return null;\n const data = JSON.parse(await readFile(file, 'utf8'));\n if (!Number.isFinite(data.checkedAt) || data.checkedAt > Date.now() || Date.now() - data.checkedAt >= CACHE_TTL) return null;\n if (data.latestVersion !== null) parseVersion(data.latestVersion);\n return data;\n } catch { return null; }\n}\n\nasync function saveCache(file: string, latestVersion: string | null): Promise<void> {\n const temporary = `${file}.${randomUUID()}.tmp`;\n try {\n await writeFile(temporary, JSON.stringify({ checkedAt: Date.now(), latestVersion }), { mode: 0o600, flag: 'wx' });\n await rename(temporary, file);\n } finally { await rm(temporary, { force: true }); }\n}\n\n/** One bounded request a day, including failed attempts, shared across dev servers. */\nexport function startUpdateCheck(cwd: string, enabled = true, options: { cacheFile?: string; fetcher?: typeof fetch; env?: NodeJS.ProcessEnv } = {}): { getSnapshot(): UpdateInfo; ready: Promise<void>; dispose(): void } {\n const controller = new AbortController();\n const env = options.env ?? process.env;\n const off = !enabled || env.ISOTONE_UPDATE_CHECK === '0' || (!!env.CI && env.CI !== 'false' && env.CI !== '0') || env.NODE_ENV === 'test' || !!env.VITEST;\n let info: UpdateInfo = { currentVersion, latestVersion: null, status: off ? 'disabled' : 'checking', upgrade: null };\n const ready = off ? Promise.resolve() : (async () => {\n const file = options.cacheFile ?? CACHE_FILE;\n let locked = false;\n try {\n const cache = await readCache(file);\n if (cache) { info = cache.latestVersion ? updateInfo(cache.latestVersion, cwd) : { ...info, status: 'unavailable' }; return; }\n await mkdir(dirname(file), { recursive: true });\n try { await (await open(`${file}.lock`, 'wx', 0o600)).close(); locked = true; }\n catch {\n // A concurrent server owns the request. Recover an abandoned lease next time.\n if (Date.now() - (await stat(`${file}.lock`)).mtimeMs > CHECK_TIMEOUT * 3) await rm(`${file}.lock`, { force: true });\n info = { ...info, status: 'unavailable' };\n return;\n }\n const refreshed = await readCache(file);\n if (refreshed) { info = refreshed.latestVersion ? updateInfo(refreshed.latestVersion, cwd) : { ...info, status: 'unavailable' }; return; }\n await saveCache(file, null);\n info = await checkUpdate(cwd, undefined, options.fetcher, controller.signal);\n if (info.latestVersion) await saveCache(file, info.latestVersion);\n } catch { info = { currentVersion, latestVersion: null, status: 'unavailable', upgrade: null }; }\n finally { if (locked) await rm(`${file}.lock`, { force: true }).catch(() => {}); }\n })();\n return { getSnapshot: () => info, ready, dispose: () => controller.abort() };\n}\n","{\n \"name\": \"isotone\",\n \"version\": \"0.4.1\",\n \"type\": \"module\",\n \"license\": \"MIT\",\n \"description\": \"Local feedback and design variants for your running web app. Pin elements, leave notes, and give your coding agent the context to make the change.\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"exports\": {\n \"./react\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n },\n \"./core\": {\n \"types\": \"./dist/core.d.ts\",\n \"import\": \"./dist/core.js\"\n },\n \"./server\": {\n \"types\": \"./dist/server.d.ts\",\n \"import\": \"./dist/server.js\"\n },\n \"./serve\": {\n \"types\": \"./dist/serve.d.ts\",\n \"import\": \"./dist/serve.js\"\n },\n \"./vite\": {\n \"types\": \"./dist/vite-plugin.d.ts\",\n \"import\": \"./dist/vite-plugin.js\"\n },\n \"./cli\": {\n \"types\": \"./dist/cli.d.ts\",\n \"import\": \"./dist/cli.js\"\n }\n },\n \"files\": [\n \"dist\",\n \"docs\",\n \"AGENTS.md\",\n \"CHANGELOG.md\"\n ],\n \"bin\": {\n \"isotone\": \"./dist/cli.js\"\n },\n \"scripts\": {\n \"typecheck\": \"tsc --noEmit\",\n \"lint\": \"biome lint .\",\n \"lint:fix\": \"biome lint --write .\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"build\": \"tsup\",\n \"check\": \"pnpm typecheck && pnpm lint && pnpm build && pnpm test\",\n \"prepublishOnly\": \"pnpm check\",\n \"dev\": \"vite --config examples/vite.config.ts\",\n \"check:package\": \"node scripts/check-package.mjs\"\n },\n \"peerDependencies\": {\n \"react\": \"^18 || ^19\",\n \"react-dom\": \"^18 || ^19\",\n \"vite\": \"^5 || ^6 || ^7 || ^8\"\n },\n \"devDependencies\": {\n \"@biomejs/biome\": \"2.5.7\",\n \"@types/node\": \"24.13.3\",\n \"@types/react\": \"^19\",\n \"@types/react-dom\": \"^19\",\n \"jsdom\": \"^30.0.1\",\n \"react\": \"^19\",\n \"react-dom\": \"^19\",\n \"tsup\": \"^8\",\n \"typescript\": \"^5\",\n \"vite\": \"^7\",\n \"vitest\": \"^4.1.10\",\n \"lucide-react\": \"^1.55.0\"\n },\n \"keywords\": [\n \"feedback\",\n \"review\",\n \"dev-tool\",\n \"vite-plugin\",\n \"coding-agent\",\n \"annotations\",\n \"design-review\",\n \"variants\",\n \"iteration\"\n ],\n \"sideEffects\": false,\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/aaroi/isotone.git\"\n },\n \"homepage\": \"https://github.com/aaroi/isotone#readme\",\n \"bugs\": \"https://github.com/aaroi/isotone/issues\",\n \"peerDependenciesMeta\": {\n \"react\": {\n \"optional\": true\n },\n \"react-dom\": {\n \"optional\": true\n },\n \"vite\": {\n \"optional\": true\n }\n }\n}\n"],"mappings":";AAAA,SAAS,YAAY,oBAAoB;AACzC,SAAS,OAAO,MAAM,UAAU,QAAQ,IAAI,MAAM,iBAAiB;AACnE,SAAS,SAAS,MAAM,eAAe;AACvC,SAAS,cAAc;AACvB,SAAS,kBAAkB;;;ACHzB,WAAQ;AACR,cAAW;;;ADMN,IAAM,kBAAkB;AAC/B,IAAM,eAAe,8BAA8B,IAAW;AAC9D,IAAM,gBAAgB;AACtB,IAAM,YAAY,KAAK,KAAK,KAAK;AACjC,IAAM,aAAa,KAAK,OAAO,GAAG,kBAAkB,QAAQ,SAAS,KAAK,MAAM,UAAU;AAM1F,SAAS,aAAa,OAAwB;AAC5C,MAAI,MAAM,SAAS,IAAK,OAAM,IAAI,MAAM,kBAAkB;AAC1D,QAAM,QAAQ,4HAA4H,KAAK,KAAK;AACpJ,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,kBAAkB;AAC9C,QAAM,aAAa,MAAM,CAAC,GAAG,MAAM,GAAG,KAAK,CAAC;AAC5C,MAAI,WAAW,KAAK,CAAC,SAAS,SAAS,KAAK,IAAI,CAAC,EAAG,OAAM,IAAI,MAAM,kBAAkB;AACtF,SAAO,EAAE,OAAO,MAAM,MAAM,GAAG,CAAC,EAAE,IAAI,MAAM,GAAG,WAAW;AAC5D;AAGO,SAAS,gBAAgB,GAAW,GAAmB;AAC5D,QAAM,OAAO,aAAa,CAAC,GAAG,QAAQ,aAAa,CAAC;AACpD,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,QAAI,KAAK,MAAM,CAAC,MAAM,MAAM,MAAM,CAAC,EAAG,QAAO,KAAK,MAAM,CAAC,IAAI,MAAM,MAAM,CAAC,IAAI,IAAI;AAAA,EACpF;AACA,MAAI,CAAC,KAAK,WAAW,UAAU,CAAC,MAAM,WAAW,QAAQ;AACvD,WAAO,KAAK,WAAW,WAAW,MAAM,WAAW,SAAS,IAAI,KAAK,WAAW,SAAS,KAAK;AAAA,EAChG;AACA,WAAS,IAAI,GAAG,IAAI,KAAK,IAAI,KAAK,WAAW,QAAQ,MAAM,WAAW,MAAM,GAAG,KAAK;AAClF,UAAM,IAAI,KAAK,WAAW,CAAC,GAAG,IAAI,MAAM,WAAW,CAAC;AACpD,QAAI,MAAM,EAAG;AACb,QAAI,MAAM,UAAa,MAAM,OAAW,QAAO,MAAM,SAAY,KAAK;AACtE,UAAM,KAAK,QAAQ,KAAK,CAAC,GAAG,KAAK,QAAQ,KAAK,CAAC;AAC/C,QAAI,MAAM,GAAI,QAAO,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,IAAI;AACjD,QAAI,OAAO,GAAI,QAAO,KAAK,KAAK;AAChC,WAAO,IAAI,IAAI,IAAI;AAAA,EACrB;AACA,SAAO;AACT;AAEA,SAAS,SAAS,WAAmD;AACnE,MAAI;AACF,UAAM,QAAQ,KAAK,MAAM,aAAa,KAAK,WAAW,cAAc,GAAG,MAAM,CAAC;AAC9E,WAAO,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAAI,QAAQ;AAAA,EAC/E,QAAQ;AAAE,WAAO;AAAA,EAAM;AACzB;AAEA,SAAS,YAAY,KAA0E;AAC7F,MAAI,YAAY,QAAQ,GAAG;AAC3B,SAAO,MAAM;AACX,UAAM,OAAO,SAAS,SAAS;AAC/B,QAAI,KAAM,QAAO,EAAE,WAAW,KAAK;AACnC,QAAI,QAAQ,SAAS,MAAM,aAAa,WAAW,KAAK,WAAW,MAAM,CAAC,EAAG,QAAO,EAAE,WAAW,QAAQ,GAAG,GAAG,MAAM,KAAK;AAC1H,gBAAY,QAAQ,SAAS;AAAA,EAC/B;AACF;AAEA,SAAS,gBAAgB,SAAyE;AAChG,MAAI,YAAY,QAAQ;AACxB,SAAO,MAAM;AACX,UAAM,OAAO,SAAS,SAAS;AAC/B,QAAI,CAAC,gBAAgB,mBAAmB,wBAAwB,kBAAkB,EAAE,KAAK,CAAC,QAAQ;AAChG,YAAM,eAAe,OAAO,GAAG;AAC/B,aAAO,gBAAgB,OAAO,iBAAiB,YAAY,OAAO,OAAO,cAAc,IAAW;AAAA,IACpG,CAAC,EAAG,QAAO,EAAE,WAAW,KAAK;AAC7B,QAAI,QAAQ,SAAS,MAAM,aAAa,WAAW,KAAK,WAAW,MAAM,CAAC,EAAG,QAAO;AACpF,gBAAY,QAAQ,SAAS;AAAA,EAC/B;AACF;AAEO,SAAS,YAAY,KAAa,QAAgB,UAA6D;AACpH,eAAa,MAAM;AACnB,QAAM,UAAU,gBAAgB,YAAY,GAAG,CAAC;AAChD,MAAI,QAAQ,MAAM,SAAS,KAAa,QAAO,EAAE,SAAS,MAAM,SAAS,2FAA2F;AACpK,MAAI,UAAU;AACd,MAAI,YAAY,QAAQ;AACxB,QAAM,QAAQ,oBAAI,IAAa;AAC/B,SAAO,CAAC,SAAS;AACf,UAAM,WAAW,SAAS,SAAS,GAAG;AACtC,QAAI,OAAO,aAAa,UAAU;AAChC,YAAM,QAAQ,oBAAoB,KAAK,QAAQ;AAC/C,UAAI,CAAC,MAAO,QAAO,EAAE,SAAS,MAAM,SAAS,mHAA8G;AAC3J,gBAAU,MAAM,CAAC;AACjB;AAAA,IACF;AACA,QAAI,CAAC,YAAY,WAAW,EAAE,KAAK,CAAC,SAAS,WAAW,KAAK,WAAW,IAAI,CAAC,CAAC,EAAG,QAAO,EAAE,SAAS,MAAM,SAAS,0EAAqE;AACvL,QAAI,WAAW,KAAK,WAAW,gBAAgB,CAAC,EAAG,OAAM,IAAI,MAAM;AACnE,QAAI,WAAW,KAAK,WAAW,WAAW,CAAC,EAAG,OAAM,IAAI,MAAM;AAC9D,QAAI,CAAC,qBAAqB,qBAAqB,EAAE,KAAK,CAAC,SAAS,WAAW,KAAK,WAAW,IAAI,CAAC,CAAC,EAAG,OAAM,IAAI,KAAK;AACnH,QAAI,MAAM,QAAQ,QAAQ,SAAS,MAAM,aAAa,WAAW,KAAK,WAAW,MAAM,CAAC,EAAG;AAC3F,gBAAY,QAAQ,SAAS;AAAA,EAC/B;AACA,MAAI,CAAC,WAAW,MAAM,OAAO,EAAG,QAAO,EAAE,SAAS,MAAM,SAAS,8EAA8E;AAC/I,cAAY,CAAC,GAAG,KAAK,EAAE,CAAC,KAAK;AAC7B,QAAM,aAAa,QAAQ,MAAM;AACjC,QAAM,WAAW,QAAQ,MAAM;AAC/B,QAAM,QAAQ,QAAQ,MAAM;AAC5B,MAAI,WAAW,IAAW,KAAK,QAAQ,IAAW,EAAG,QAAO,EAAE,SAAS,MAAM,SAAS,2EAA2E;AACjK,QAAM,MAAM,aAAa,IAAW,IAAI,KAAK;AAC7C,MAAI,YAAY,YAAY,UAAU,WAAW,KAAK,QAAQ,WAAW,qBAAqB,CAAC,IAAI,QAAQ;AAC3G,MAAI,YAAY,UAAU,QAAQ,MAAM,YAAY;AAClD,UAAM,WAAW,QAAQ,KAAK;AAC9B,QAAI,OAAO,aAAa,YAAY,CAAC,cAAc,KAAK,QAAQ,EAAG,QAAO,EAAE,SAAS,MAAM,SAAS,iJAAiJ;AACrP,QAAI,SAAS,WAAW,SAAS,EAAG,aAAY;AAAA,EAClD;AACA,QAAM,UAAU,YAAY,QACxB,cAAc,GAAG,iBAAiB,IAAW,IAAI,MAAM,KACvD,GAAG,OAAO,OAAO,GAAG,GAAG,SAAS,YAAY,IAAW,IAAI,MAAM;AACrE,SAAO,EAAE,SAAS,EAAE,SAAS,SAAS,KAAK,QAAQ,UAAU,EAAE;AACjE;AAEA,eAAsB,YAAY,UAAwB,OAAO,QAAuC;AACtG,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,MAAM,WAAW,MAAM;AACrC,MAAI,QAAQ,QAAS,OAAM;AAAA,MACtB,SAAQ,iBAAiB,SAAS,OAAO,EAAE,MAAM,KAAK,CAAC;AAC5D,QAAM,QAAQ,WAAW,OAAO,aAAa;AAC7C,MAAI;AACF,QAAI,WAAW,OAAO,QAAS,OAAM,IAAI,MAAM,yBAAyB;AACxE,UAAM,WAAW,MAAM,QAAQ,cAAc,EAAE,QAAQ,WAAW,QAAQ,UAAU,SAAS,SAAS,EAAE,QAAQ,mBAAmB,EAAE,CAAC;AACtI,QAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,0BAA0B,SAAS,MAAM,GAAG;AAC9E,QAAI,CAAC,SAAS,KAAM,OAAM,IAAI,MAAM,sCAAsC;AAC1E,UAAM,SAAS,SAAS,KAAK,UAAU;AACvC,UAAM,SAAuB,CAAC;AAC9B,QAAI,QAAQ;AACZ,QAAI;AACF,aAAO,MAAM;AACX,cAAM,EAAE,OAAO,KAAK,IAAI,MAAM,OAAO,KAAK;AAC1C,YAAI,KAAM;AACV,iBAAS,MAAM;AACf,YAAI,QAAQ,KAAK,MAAM;AACrB,gBAAM,OAAO,OAAO;AACpB,gBAAM,IAAI,MAAM,kCAAkC;AAAA,QACpD;AACA,eAAO,KAAK,KAAK;AAAA,MACnB;AAAA,IACF,UAAE;AAAU,aAAO,YAAY;AAAA,IAAG;AAClC,UAAM,OAAO,KAAK,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM,CAAC;AAC9D,QAAI,OAAO,KAAK,YAAY,SAAU,OAAM,IAAI,MAAM,+BAA+B;AACrF,iBAAa,KAAK,OAAO;AACzB,WAAO,KAAK;AAAA,EACd,UAAE;AAAU,iBAAa,KAAK;AAAG,YAAQ,oBAAoB,SAAS,KAAK;AAAA,EAAG;AAChF;AAEO,SAAS,WAAW,QAAgB,KAAa,SAA+B;AACrF,QAAM,QAAQ,gBAAgB,QAAQ,OAAc;AACpD,SAAO;AAAA,IACL;AAAA,IAAgB,eAAe;AAAA,IAC/B,QAAQ,QAAQ,IAAI,cAAc,QAAQ,IAAI,UAAU;AAAA,IACxD,GAAI,QAAQ,IAAI,YAAY,KAAK,QAAQ,OAAO,IAAI,EAAE,SAAS,KAAK;AAAA,EACtE;AACF;AAEA,eAAsB,YAAY,KAAa,SAAmB,SAAwB,QAA2C;AACnI,MAAI;AAAE,WAAO,WAAW,MAAM,YAAY,SAAS,MAAM,GAAG,KAAK,OAAO;AAAA,EAAG,QACrE;AAAE,WAAO,EAAE,yBAAgB,eAAe,MAAM,QAAQ,eAAe,SAAS,MAAM,SAAS,gGAAgG;AAAA,EAAG;AAC1M;AAGA,eAAe,UAAU,MAA2C;AAClE,MAAI;AACF,SAAK,MAAM,KAAK,IAAI,GAAG,OAAO,KAAM,QAAO;AAC3C,UAAM,OAAO,KAAK,MAAM,MAAM,SAAS,MAAM,MAAM,CAAC;AACpD,QAAI,CAAC,OAAO,SAAS,KAAK,SAAS,KAAK,KAAK,YAAY,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,aAAa,UAAW,QAAO;AACxH,QAAI,KAAK,kBAAkB,KAAM,cAAa,KAAK,aAAa;AAChE,WAAO;AAAA,EACT,QAAQ;AAAE,WAAO;AAAA,EAAM;AACzB;AAEA,eAAe,UAAU,MAAc,eAA6C;AAClF,QAAM,YAAY,GAAG,IAAI,IAAI,WAAW,CAAC;AACzC,MAAI;AACF,UAAM,UAAU,WAAW,KAAK,UAAU,EAAE,WAAW,KAAK,IAAI,GAAG,cAAc,CAAC,GAAG,EAAE,MAAM,KAAO,MAAM,KAAK,CAAC;AAChH,UAAM,OAAO,WAAW,IAAI;AAAA,EAC9B,UAAE;AAAU,UAAM,GAAG,WAAW,EAAE,OAAO,KAAK,CAAC;AAAA,EAAG;AACpD;AAGO,SAAS,iBAAiB,KAAa,UAAU,MAAM,UAAmF,CAAC,GAAyE;AACzN,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,MAAM,QAAQ,OAAO,QAAQ;AACnC,QAAM,MAAM,CAAC,WAAW,IAAI,yBAAyB,OAAQ,CAAC,CAAC,IAAI,MAAM,IAAI,OAAO,WAAW,IAAI,OAAO,OAAQ,IAAI,aAAa,UAAU,CAAC,CAAC,IAAI;AACnJ,MAAI,OAAmB,EAAE,yBAAgB,eAAe,MAAM,QAAQ,MAAM,aAAa,YAAY,SAAS,KAAK;AACnH,QAAM,QAAQ,MAAM,QAAQ,QAAQ,KAAK,YAAY;AACnD,UAAM,OAAO,QAAQ,aAAa;AAClC,QAAI,SAAS;AACb,QAAI;AACF,YAAM,QAAQ,MAAM,UAAU,IAAI;AAClC,UAAI,OAAO;AAAE,eAAO,MAAM,gBAAgB,WAAW,MAAM,eAAe,GAAG,IAAI,EAAE,GAAG,MAAM,QAAQ,cAAc;AAAG;AAAA,MAAQ;AAC7H,YAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9C,UAAI;AAAE,eAAO,MAAM,KAAK,GAAG,IAAI,SAAS,MAAM,GAAK,GAAG,MAAM;AAAG,iBAAS;AAAA,MAAM,QACxE;AAEJ,YAAI,KAAK,IAAI,KAAK,MAAM,KAAK,GAAG,IAAI,OAAO,GAAG,UAAU,gBAAgB,EAAG,OAAM,GAAG,GAAG,IAAI,SAAS,EAAE,OAAO,KAAK,CAAC;AACnH,eAAO,EAAE,GAAG,MAAM,QAAQ,cAAc;AACxC;AAAA,MACF;AACA,YAAM,YAAY,MAAM,UAAU,IAAI;AACtC,UAAI,WAAW;AAAE,eAAO,UAAU,gBAAgB,WAAW,UAAU,eAAe,GAAG,IAAI,EAAE,GAAG,MAAM,QAAQ,cAAc;AAAG;AAAA,MAAQ;AACzI,YAAM,UAAU,MAAM,IAAI;AAC1B,aAAO,MAAM,YAAY,KAAK,QAAW,QAAQ,SAAS,WAAW,MAAM;AAC3E,UAAI,KAAK,cAAe,OAAM,UAAU,MAAM,KAAK,aAAa;AAAA,IAClE,QAAQ;AAAE,aAAO,EAAE,yBAAgB,eAAe,MAAM,QAAQ,eAAe,SAAS,KAAK;AAAA,IAAG,UAChG;AAAU,UAAI,OAAQ,OAAM,GAAG,GAAG,IAAI,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IAAG;AAAA,EACnF,GAAG;AACH,SAAO,EAAE,aAAa,MAAM,MAAM,OAAO,SAAS,MAAM,WAAW,MAAM,EAAE;AAC7E;","names":[]}
1
+ {"version":3,"sources":["../src/update.ts","../package.json"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { mkdir, open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { randomUUID } from 'node:crypto';\nimport type { UpdateInfo } from './core/update';\nimport { name as packageName, version as currentVersion } from '../package.json';\n\nexport const UPDATE_ENDPOINT = '/api/iteration/update';\nconst REGISTRY_URL = `https://registry.npmjs.org/${packageName}/latest`;\nconst CHECK_TIMEOUT = 5_000;\nconst CACHE_TTL = 24 * 60 * 60 * 1000;\nconst CACHE_FILE = join(tmpdir(), `isotone-update-${process.getuid?.() ?? 'user'}-v1.json`);\n\ntype Manager = 'npm' | 'pnpm' | 'yarn';\nexport type { UpdateInfo } from './core/update';\n\ninterface Version { parts: bigint[]; prerelease: string[] }\nfunction parseVersion(value: string): Version {\n if (value.length > 256) throw new Error('Invalid version.');\n const match = /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-([\\da-zA-Z-]+(?:\\.[\\da-zA-Z-]+)*))?(?:\\+([\\da-zA-Z-]+(?:\\.[\\da-zA-Z-]+)*))?$/.exec(value);\n if (!match) throw new Error('Invalid version.');\n const prerelease = match[4]?.split('.') ?? [];\n if (prerelease.some((part) => /^0\\d+$/.test(part))) throw new Error('Invalid version.');\n return { parts: match.slice(1, 4).map(BigInt), prerelease };\n}\n\n/** SemVer precedence, including numeric prereleases and ignored build metadata. */\nexport function compareVersions(a: string, b: string): number {\n const left = parseVersion(a), right = parseVersion(b);\n for (let i = 0; i < 3; i++) {\n if (left.parts[i] !== right.parts[i]) return left.parts[i] > right.parts[i] ? 1 : -1;\n }\n if (!left.prerelease.length || !right.prerelease.length) {\n return left.prerelease.length === right.prerelease.length ? 0 : left.prerelease.length ? -1 : 1;\n }\n for (let i = 0; i < Math.max(left.prerelease.length, right.prerelease.length); i++) {\n const x = left.prerelease[i], y = right.prerelease[i];\n if (x === y) continue;\n if (x === undefined || y === undefined) return x === undefined ? -1 : 1;\n const nx = /^\\d+$/.test(x), ny = /^\\d+$/.test(y);\n if (nx && ny) return BigInt(x) > BigInt(y) ? 1 : -1;\n if (nx !== ny) return nx ? -1 : 1;\n return x > y ? 1 : -1;\n }\n return 0;\n}\n\nfunction manifest(directory: string): Record<string, unknown> | null {\n try {\n const value = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));\n return value && typeof value === 'object' && !Array.isArray(value) ? value : null;\n } catch { return null; }\n}\n\nfunction findProject(cwd: string): { directory: string; json: Record<string, unknown> | null } {\n let directory = resolve(cwd);\n while (true) {\n const json = manifest(directory);\n if (json) return { directory, json };\n if (dirname(directory) === directory || existsSync(join(directory, '.git'))) return { directory: resolve(cwd), json: null };\n directory = dirname(directory);\n }\n}\n\nfunction dependencyOwner(project: ReturnType<typeof findProject>): ReturnType<typeof findProject> {\n let directory = project.directory;\n while (true) {\n const json = manifest(directory);\n if (['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'].some((key) => {\n const dependencies = json?.[key];\n return dependencies && typeof dependencies === 'object' && Object.hasOwn(dependencies, packageName);\n })) return { directory, json };\n if (dirname(directory) === directory || existsSync(join(directory, '.git'))) return project;\n directory = dirname(directory);\n }\n}\n\nexport function upgradePlan(cwd: string, latest: string, override?: Manager): Pick<UpdateInfo, 'upgrade' | 'message'> {\n parseVersion(latest); // Only a validated version can enter a copied command.\n const project = dependencyOwner(findProject(cwd));\n if (project.json?.name === packageName) return { upgrade: null, message: 'This is the source checkout; update its source instead of adding itself as a dependency.' };\n let manager = override;\n let directory = project.directory;\n const locks = new Set<Manager>();\n while (!manager) {\n const declared = manifest(directory)?.packageManager;\n if (typeof declared === 'string') {\n const match = /^(npm|pnpm|yarn)@/.exec(declared);\n if (!match) return { upgrade: null, message: 'The declared package manager is not supported by this command. Upgrade with your project’s package manager.' };\n manager = match[1] as Manager;\n break;\n }\n if (['bun.lock', 'bun.lockb'].some((file) => existsSync(join(directory, file)))) return { upgrade: null, message: 'This project uses Bun. Upgrade with the project’s package manager.' };\n if (existsSync(join(directory, 'pnpm-lock.yaml'))) locks.add('pnpm');\n if (existsSync(join(directory, 'yarn.lock'))) locks.add('yarn');\n if (['package-lock.json', 'npm-shrinkwrap.json'].some((file) => existsSync(join(directory, file)))) locks.add('npm');\n if (locks.size || dirname(directory) === directory || existsSync(join(directory, '.git'))) break;\n directory = dirname(directory);\n }\n if (!manager && locks.size > 1) return { upgrade: null, message: 'Multiple package-manager lockfiles found. Pass --manager npm, pnpm or yarn.' };\n manager ??= [...locks][0] ?? 'npm';\n const production = project.json?.dependencies as Record<string, unknown> | undefined;\n const optional = project.json?.optionalDependencies as Record<string, unknown> | undefined;\n const peers = project.json?.peerDependencies as Record<string, unknown> | undefined;\n if (optional?.[packageName] || peers?.[packageName]) return { upgrade: null, message: 'Review this optional/peer dependency manually with your package manager.' };\n const dev = production?.[packageName] ? '' : ' -D';\n let workspace = manager === 'pnpm' && existsSync(join(project.directory, 'pnpm-workspace.yaml')) ? ' -w' : '';\n if (manager === 'yarn' && project.json?.workspaces) {\n const declared = project.json.packageManager;\n if (typeof declared !== 'string' || !/^yarn@\\d+\\./.test(declared)) return { upgrade: null, message: 'Declare the Yarn version in packageManager before updating a workspace-root dependency; Yarn Classic and modern Yarn use different root flags.' };\n if (declared.startsWith('yarn@1.')) workspace = ' -W';\n }\n const command = manager === 'npm'\n ? `npm install${dev} --save-exact ${packageName}@${latest}`\n : `${manager} add${dev}${workspace} --exact ${packageName}@${latest}`;\n return { upgrade: { manager, command, cwd: project.directory } };\n}\n\nexport async function fetchLatest(fetcher: typeof fetch = fetch, signal?: AbortSignal): Promise<string> {\n const controller = new AbortController();\n const abort = () => controller.abort();\n if (signal?.aborted) abort();\n else signal?.addEventListener('abort', abort, { once: true });\n const timer = setTimeout(abort, CHECK_TIMEOUT);\n try {\n if (controller.signal.aborted) throw new Error('Update check cancelled.');\n const response = await fetcher(REGISTRY_URL, { signal: controller.signal, redirect: 'error', headers: { accept: 'application/json' } });\n if (!response.ok) throw new Error(`Registry returned HTTP ${response.status}.`);\n if (!response.body) throw new Error('Registry returned an empty response.');\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let bytes = 0;\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n bytes += value.byteLength;\n if (bytes > 64 * 1024) {\n await reader.cancel();\n throw new Error('Registry response was too large.');\n }\n chunks.push(value);\n }\n } finally { reader.releaseLock(); }\n const data = JSON.parse(Buffer.concat(chunks).toString('utf8'));\n if (typeof data.version !== 'string') throw new Error('Registry returned no version.');\n parseVersion(data.version);\n return data.version;\n } finally { clearTimeout(timer); signal?.removeEventListener('abort', abort); }\n}\n\nexport function updateInfo(latest: string, cwd: string, manager?: Manager): UpdateInfo {\n const order = compareVersions(latest, currentVersion);\n return {\n currentVersion, latestVersion: latest,\n status: order > 0 ? 'available' : order < 0 ? 'ahead' : 'current',\n ...(order > 0 ? upgradePlan(cwd, latest, manager) : { upgrade: null }),\n };\n}\n\nexport async function checkUpdate(cwd: string, manager?: Manager, fetcher?: typeof fetch, signal?: AbortSignal): Promise<UpdateInfo> {\n try { return updateInfo(await fetchLatest(fetcher, signal), cwd, manager); }\n catch { return { currentVersion, latestVersion: null, status: 'unavailable', upgrade: null, message: 'Could not check npm for updates. The installed version remains usable; try again when online.' }; }\n}\n\ninterface CachedCheck { checkedAt: number; latestVersion: string | null }\nasync function readCache(file: string): Promise<CachedCheck | null> {\n try {\n if ((await stat(file)).size > 4096) return null;\n const data = JSON.parse(await readFile(file, 'utf8'));\n if (!Number.isFinite(data.checkedAt) || data.checkedAt > Date.now() || Date.now() - data.checkedAt >= CACHE_TTL) return null;\n if (data.latestVersion !== null) parseVersion(data.latestVersion);\n return data;\n } catch { return null; }\n}\n\nasync function saveCache(file: string, latestVersion: string | null): Promise<void> {\n const temporary = `${file}.${randomUUID()}.tmp`;\n try {\n await writeFile(temporary, JSON.stringify({ checkedAt: Date.now(), latestVersion }), { mode: 0o600, flag: 'wx' });\n await rename(temporary, file);\n } finally { await rm(temporary, { force: true }); }\n}\n\n/** One bounded request a day, including failed attempts, shared across dev servers. */\nexport function startUpdateCheck(cwd: string, enabled = true, options: { cacheFile?: string; fetcher?: typeof fetch; env?: NodeJS.ProcessEnv } = {}): { getSnapshot(): UpdateInfo; ready: Promise<void>; dispose(): void } {\n const controller = new AbortController();\n const env = options.env ?? process.env;\n const off = !enabled || env.ISOTONE_UPDATE_CHECK === '0' || (!!env.CI && env.CI !== 'false' && env.CI !== '0') || env.NODE_ENV === 'test' || !!env.VITEST;\n let info: UpdateInfo = { currentVersion, latestVersion: null, status: off ? 'disabled' : 'checking', upgrade: null };\n const ready = off ? Promise.resolve() : (async () => {\n const file = options.cacheFile ?? CACHE_FILE;\n let locked = false;\n try {\n const cache = await readCache(file);\n if (cache) { info = cache.latestVersion ? updateInfo(cache.latestVersion, cwd) : { ...info, status: 'unavailable' }; return; }\n await mkdir(dirname(file), { recursive: true });\n try { await (await open(`${file}.lock`, 'wx', 0o600)).close(); locked = true; }\n catch {\n // A concurrent server owns the request. Recover an abandoned lease next time.\n if (Date.now() - (await stat(`${file}.lock`)).mtimeMs > CHECK_TIMEOUT * 3) await rm(`${file}.lock`, { force: true });\n info = { ...info, status: 'unavailable' };\n return;\n }\n const refreshed = await readCache(file);\n if (refreshed) { info = refreshed.latestVersion ? updateInfo(refreshed.latestVersion, cwd) : { ...info, status: 'unavailable' }; return; }\n await saveCache(file, null);\n info = await checkUpdate(cwd, undefined, options.fetcher, controller.signal);\n if (info.latestVersion) await saveCache(file, info.latestVersion);\n } catch { info = { currentVersion, latestVersion: null, status: 'unavailable', upgrade: null }; }\n finally { if (locked) await rm(`${file}.lock`, { force: true }).catch(() => {}); }\n })();\n return { getSnapshot: () => info, ready, dispose: () => controller.abort() };\n}\n","{\n \"name\": \"isotone\",\n \"version\": \"0.4.2\",\n \"type\": \"module\",\n \"license\": \"MIT\",\n \"description\": \"Local feedback and design variants for your running web app. Pin elements, leave notes, and give your coding agent the context to make the change.\",\n \"engines\": {\n \"node\": \">=20\"\n },\n \"exports\": {\n \"./react\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n },\n \"./core\": {\n \"types\": \"./dist/core.d.ts\",\n \"import\": \"./dist/core.js\"\n },\n \"./server\": {\n \"types\": \"./dist/server.d.ts\",\n \"import\": \"./dist/server.js\"\n },\n \"./serve\": {\n \"types\": \"./dist/serve.d.ts\",\n \"import\": \"./dist/serve.js\"\n },\n \"./vite\": {\n \"types\": \"./dist/vite-plugin.d.ts\",\n \"import\": \"./dist/vite-plugin.js\"\n },\n \"./cli\": {\n \"types\": \"./dist/cli.d.ts\",\n \"import\": \"./dist/cli.js\"\n }\n },\n \"files\": [\n \"dist\",\n \"docs\",\n \"AGENTS.md\",\n \"CHANGELOG.md\"\n ],\n \"bin\": {\n \"isotone\": \"./dist/cli.js\"\n },\n \"scripts\": {\n \"typecheck\": \"tsc --noEmit\",\n \"lint\": \"biome lint .\",\n \"lint:fix\": \"biome lint --write .\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"build\": \"tsup\",\n \"check\": \"pnpm typecheck && pnpm lint && pnpm build && pnpm test\",\n \"prepublishOnly\": \"pnpm check\",\n \"dev\": \"vite --config examples/vite.config.ts\",\n \"check:package\": \"node scripts/check-package.mjs\"\n },\n \"peerDependencies\": {\n \"react\": \"^18 || ^19\",\n \"react-dom\": \"^18 || ^19\",\n \"vite\": \"^5 || ^6 || ^7 || ^8\"\n },\n \"devDependencies\": {\n \"@biomejs/biome\": \"2.5.7\",\n \"@types/node\": \"24.13.3\",\n \"@types/react\": \"^19\",\n \"@types/react-dom\": \"^19\",\n \"jsdom\": \"^30.0.1\",\n \"react\": \"^19\",\n \"react-dom\": \"^19\",\n \"tsup\": \"^8\",\n \"typescript\": \"^5\",\n \"vite\": \"^7\",\n \"vitest\": \"^4.1.10\",\n \"lucide-react\": \"^1.55.0\"\n },\n \"keywords\": [\n \"feedback\",\n \"review\",\n \"dev-tool\",\n \"vite-plugin\",\n \"coding-agent\",\n \"annotations\",\n \"design-review\",\n \"variants\",\n \"iteration\"\n ],\n \"sideEffects\": false,\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/aaroi/isotone.git\"\n },\n \"homepage\": \"https://github.com/aaroi/isotone#readme\",\n \"bugs\": \"https://github.com/aaroi/isotone/issues\",\n \"peerDependenciesMeta\": {\n \"react\": {\n \"optional\": true\n },\n \"react-dom\": {\n \"optional\": true\n },\n \"vite\": {\n \"optional\": true\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -493,32 +493,6 @@ function createOverlayVariantsSource(cfg) {
493
493
  emit();
494
494
  },
495
495
  getAxes: axes,
496
- async manage(action) {
497
- const next = await request(`${cfg.endpoint}/manage`, {
498
- method: "POST",
499
- headers: { "Content-Type": "application/json" },
500
- body: JSON.stringify({ ...action, route: cfg.getRoute() })
501
- }, async (res) => {
502
- if (!res.ok) {
503
- const body = await res.json().catch(() => ({}));
504
- throw new Error(body.error ?? "Could not update this selector.");
505
- }
506
- return await res.json();
507
- });
508
- const previousKnobs = list?.knobs ?? [];
509
- list = next;
510
- const previous = previousKnobs.find((k) => k.id === action.axisId);
511
- if (action.action === "remove-option" && previous?.kind === "color") {
512
- const removed = previous.swatches.find((s) => s.id === action.optionId);
513
- if (removed && knobValues[previous.id]?.toLowerCase() === removed.value.toLowerCase()) delete knobValues[previous.id];
514
- }
515
- if (knobRoot) applyKnobValues(previousKnobs, {}, knobRoot);
516
- const remaining = new Set(list.knobs?.map((k) => k.id));
517
- knobValues = Object.fromEntries(Object.entries(knobValues).filter(([id]) => remaining.has(id)));
518
- persistKnobs();
519
- applyKnobs();
520
- emit();
521
- },
522
496
  getSelections() {
523
497
  const active = new Set(intended ?? list?.active ?? []);
524
498
  const picks = Object.fromEntries(
@@ -593,4 +567,4 @@ export {
593
567
  overlayAxes,
594
568
  createOverlayVariantsSource
595
569
  };
596
- //# sourceMappingURL=chunk-SNGBNVH2.js.map
570
+ //# sourceMappingURL=chunk-OSETWKGE.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/browser-storage.ts","../src/core/request.ts","../src/core/variants.ts"],"sourcesContent":["/** Browser-only migration from the former package's default namespaces. */\nexport const FEEDBACK_STORAGE_KEY = 'isotone-feedback';\ntype BrowserStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;\n\nfunction legacyKey(key: string): string | undefined {\n if (key === FEEDBACK_STORAGE_KEY || key.startsWith(`${FEEDBACK_STORAGE_KEY}::draft::`))\n return `iso-iterate-feedback${key.slice(FEEDBACK_STORAGE_KEY.length)}`;\n if (key === 'isotone:knobs') return 'iso-iterate:knobs';\n return undefined;\n}\n\n/** A failed migration remains readable under its old key for the next visit. */\nexport function readBrowserStorage(\n storage: BrowserStorage,\n key: string,\n merge: (current: string | null, previous: string) => string = (current, previous) => current ?? previous,\n): string | null {\n let current: string | null = null;\n try { current = storage.getItem(key); }\n catch { return null; }\n const previousKey = legacyKey(key);\n if (!previousKey) return current;\n let previous: string | null;\n try { previous = storage.getItem(previousKey); }\n catch { return current; }\n if (previous === null) return current;\n const next = merge(current, previous);\n try { writeBrowserStorage(storage, key, next); }\n catch { /* Quota or access restrictions must not hide the previous value. */ }\n return next;\n}\n\nexport function writeBrowserStorage(storage: BrowserStorage, key: string, value: string): void {\n storage.setItem(key, value);\n const previousKey = legacyKey(key);\n if (!previousKey) return;\n try {\n if (storage.getItem(key) === value) storage.removeItem(previousKey);\n } catch { /* Keep the old value unless its replacement was verified. */ }\n}\n\n/** Explicitly clearing a value also clears its legacy fallback. */\nexport function removeBrowserStorage(storage: BrowserStorage, key: string): void {\n storage.removeItem(key);\n const previousKey = legacyKey(key);\n if (previousKey) storage.removeItem(previousKey);\n}\n","type RequestInput = Parameters<typeof fetch>[0];\n\nexport function request(input: RequestInput, init?: RequestInit): Promise<Response>;\nexport function request<T>(input: RequestInput, init: RequestInit | undefined, consume: (response: Response) => Promise<T>): Promise<T>;\n\n/** The deadline includes body consumption, not just receipt of the headers. */\nexport async function request<T>(input: RequestInput, init?: RequestInit, consume?: (response: Response) => Promise<T>): Promise<Response | T> {\n const native = (globalThis as { __isotoneFetch?: typeof fetch }).__isotoneFetch;\n const controller = new AbortController();\n const signal = init?.signal ?? (typeof Request !== 'undefined' && input instanceof Request ? input.signal : undefined);\n const abort = () => controller.abort(signal?.reason);\n if (signal?.aborted) abort();\n else signal?.addEventListener('abort', abort, { once: true });\n const timeout = setTimeout(() => controller.abort(new DOMException('The local server did not respond in time.', 'TimeoutError')), 5_000);\n let aborted: () => void = () => {};\n const cancelled = new Promise<never>((_, reject) => {\n aborted = () => reject(controller.signal.reason);\n if (controller.signal.aborted) aborted();\n else controller.signal.addEventListener('abort', aborted, { once: true });\n });\n try {\n return await Promise.race([cancelled, (async () => {\n if (controller.signal.aborted) throw controller.signal.reason;\n const response = await (native ?? globalThis.fetch)(input, { ...init, signal: controller.signal });\n return consume ? consume(response) : response;\n })()]);\n } finally {\n clearTimeout(timeout);\n signal?.removeEventListener('abort', abort);\n controller.signal.removeEventListener('abort', aborted);\n }\n}\n","/**\n * isotone — variants: the framework-free model.\n *\n * A variant axis is one tunable decision about a page: a discrete choice\n * (select), a continuous value (range) or a colour. The panel renders axes;\n * where their state lives is the host's business. Two hosts ship today:\n *\n * - A host with its own variant system (its own axes and\n * `useVariant` hooks) hands the panel a {@link VariantsSource} over that\n * state. The panel is then a view; the host still owns `?v=` sync,\n * defaults and promotion.\n * - A host with no variant system gets the overlay adapter: alternative\n * source files under `.isotone/variants/<id>/…` that the Vite plugin\n * swaps in for the real ones while a variant is active. The browser sees\n * one select axis per group of alternatives, on the route they belong to\n * ({@link overlayGroups}, {@link createOverlayVariantsSource}).\n *\n * Everything in this module is pure or DOM-free and runs in a `node` test.\n */\n\nimport type { IterationElement } from './index';\nimport { request } from './request';\nimport { readBrowserStorage, removeBrowserStorage, writeBrowserStorage } from './browser-storage';\n\n/* ── Axes ─────────────────────────────────────────────────────────────── */\n\nexport interface VariantOption {\n /** Stable short id, e.g. \"a\", \"b\". */\n id: string;\n /** Short label (1–2 words); it renders as a segment and truncates. */\n label: string;\n /** Optional preview image URL. When every option of an axis has one, the\n * panel renders the axis as a thumbnail grid. */\n image?: string;\n /** Optional secondary text shown as a tooltip on the option. */\n hint?: string;\n}\n\n/** Show an axis only while another axis holds (`equals`) or does not hold\n * (`not`) a value. Exactly one of the two should be set. */\nexport interface VariantAxisCondition {\n axis: string;\n equals?: string;\n not?: string;\n}\n\nexport interface SelectVariantAxis {\n kind?: 'select';\n id: string;\n label: string;\n options: VariantOption[];\n /** The primary option; what a host renders when nothing is selected. */\n defaultOption: string;\n showWhen?: VariantAxisCondition;\n /** Panel grouping: a hairline separates consecutive different sections. */\n section?: string;\n}\n\nexport interface RangeVariantAxis {\n kind: 'range';\n id: string;\n label: string;\n min: number;\n max: number;\n step: number;\n defaultValue: number;\n /** How the panel prints the value (\"px\", \"°\", \"em\"). Omitted: a 0–1 axis\n * prints as a percentage, anything else as the raw number. */\n unit?: string;\n showWhen?: VariantAxisCondition;\n section?: string;\n}\n\nexport interface ColorSwatch {\n id: string;\n label: string;\n /** Hex, e.g. \"#ea382e\". */\n value: string;\n}\n\nexport interface ColorVariantAxis {\n kind: 'color';\n id: string;\n label: string;\n /** Hex. */\n defaultColor: string;\n /** Quick-pick presets shown as swatches (include the default). */\n swatches: ColorSwatch[];\n showWhen?: VariantAxisCondition;\n section?: string;\n}\n\nexport type VariantAxis = SelectVariantAxis | RangeVariantAxis | ColorVariantAxis;\n\n/** Axis id → selected value. Ranges stringify their number. */\nexport type VariantSelections = Record<string, string>;\n\nexport interface VariantAction {\n axisId: string;\n action: 'remove-option' | 'remove-axis' | 'keep-default';\n optionId?: string;\n}\n\n/* ── Pure helpers ─────────────────────────────────────────────────────── */\n\n/** The primary value of an axis, as the selection string. */\nexport function axisDefault(axis: VariantAxis): string {\n if (axis.kind === 'range') return String(axis.defaultValue);\n if (axis.kind === 'color') return axis.defaultColor;\n return axis.defaultOption;\n}\n\n/** True when `value` is legal for the axis. */\nexport function isValidSelection(axis: VariantAxis, value: string): boolean {\n if (axis.kind === 'range') {\n const n = Number(value);\n return value.trim() !== '' && Number.isFinite(n) && n >= axis.min && n <= axis.max;\n }\n if (axis.kind === 'color') return /^#[0-9a-fA-F]{6}$/.test(value);\n return axis.options.some((o) => o.id === value);\n}\n\nexport function defaultSelections(axes: VariantAxis[]): VariantSelections {\n return Object.fromEntries(axes.map((a) => [a.id, axisDefault(a)]));\n}\n\n/** `\"headline:b,haze-speed:0.7\"` → validated selections. Unknown axes and\n * illegal values are dropped rather than failing the whole string. */\nexport function parseVariantParam(axes: VariantAxis[], raw: string): VariantSelections {\n const out = new Map<string, string>();\n const byId = new Map(axes.map((axis) => [axis.id, axis]));\n for (const pair of raw.split(',')) {\n const at = pair.indexOf(':');\n if (at < 0) continue;\n const axisId = pair.slice(0, at);\n const value = pair.slice(at + 1);\n const axis = byId.get(axisId);\n if (axis && isValidSelection(axis, value)) out.set(axisId, value);\n }\n return Object.fromEntries(out);\n}\n\n/** Non-default selections only, `\"\"` when everything is at its default. */\nexport function serializeVariantParam(\n axes: VariantAxis[],\n selections: VariantSelections,\n): string {\n return axes\n .filter((a) => selections[a.id] !== undefined && selections[a.id] !== axisDefault(a))\n .map((a) => `${a.id}:${selections[a.id]}`)\n .join(',');\n}\n\n/** Whether any axis differs from its default. */\nexport function isDirty(axes: VariantAxis[], selections: VariantSelections): boolean {\n return axes.some((a) => selections[a.id] !== undefined && selections[a.id] !== axisDefault(a));\n}\n\n/**\n * Whether an axis shows, given every axis and the current selections.\n * A hidden parent hides its children. Cycles are hidden, and long chains\n * are evaluated iteratively rather than consuming the call stack.\n */\nexport function isAxisVisible(\n axis: VariantAxis,\n axes: VariantAxis[],\n selections: VariantSelections,\n _depth = 0,\n): boolean {\n return visibilityById(axes.includes(axis) ? axes : [...axes, axis], selections).get(axis.id) ?? false;\n}\n\nexport function visibleAxes(axes: VariantAxis[], selections: VariantSelections): VariantAxis[] {\n const visibility = visibilityById(axes, selections);\n return axes.filter((axis) => visibility.get(axis.id));\n}\n\nfunction visibilityById(axes: VariantAxis[], selections: VariantSelections): Map<string, boolean> {\n const byId = new Map(axes.map((axis) => [axis.id, axis]));\n const visible = new Map<string, boolean>();\n for (const axis of axes) {\n const trail = new Set<string>();\n let current: VariantAxis | undefined = axis;\n let result = true;\n while (current) {\n const known = visible.get(current.id);\n if (known !== undefined) { result = known; break; }\n if (trail.has(current.id)) { result = false; break; }\n trail.add(current.id);\n const condition: VariantAxisCondition | undefined = current.showWhen;\n if (!condition) break;\n const parent = byId.get(condition.axis);\n const value = selections[condition.axis] ?? (parent ? axisDefault(parent) : undefined);\n if ((condition.equals !== undefined && value !== condition.equals) ||\n (condition.not !== undefined && value === condition.not)) { result = false; break; }\n current = parent;\n }\n for (const id of trail) visible.set(id, result);\n }\n return visible;\n}\n\n/** How the panel prints a range value: its unit when declared, a percentage\n * for a bare 0–1 axis, else the raw number. */\nexport function formatRangeValue(axis: RangeVariantAxis, value: string): string {\n if (axis.unit !== undefined) return `${value}${axis.unit}`;\n if (axis.min === 0 && axis.max === 1) return `${Math.round(Number(value) * 100)}%`;\n return value;\n}\n\n/* ── The source contract ──────────────────────────────────────────────── */\n\n/**\n * What the panel renders variants from. The host owns the state; the panel\n * reads it through these calls and re-renders on `subscribe` notifications.\n * Every method must be safe to call at any time after mount, including\n * before the host has any axes (return `[]`).\n */\nexport interface VariantsSource {\n /** The axes for the current page. May change over time (route change). */\n getAxes(): VariantAxis[];\n /** The current selections. Missing keys mean \"at default\". */\n getSelections(): VariantSelections;\n setSelection(axisId: string, value: string): void;\n /** Return every axis to its default. */\n reset(): void;\n /** Notify on any change to axes or selections. Returns an unsubscribe. */\n subscribe(listener: () => void): () => void;\n /** A recoverable transport error; notify subscribers when it changes. */\n getError?: () => string | null;\n /**\n * When present, the panel shows \"Save as defaults\" and calls this with the\n * current selections. A host that promotes by editing source (one that\n * files a `[set-defaults]` note for the agent) implements it; a host\n * whose variants are whole alternatives (the overlay adapter) leaves it\n * out and promotes from the CLI instead.\n */\n saveDefaults?: (selections: VariantSelections) => Promise<void> | void;\n /** Panel heading; defaults to \"Variants\". */\n title?: string;\n /** Optional durable host mutation. Reject to leave the UI unchanged. */\n manage?: (action: VariantAction) => Promise<void> | void;\n}\n\n/**\n * A `VariantsSource` over plain state, for hosts that just want to hand the\n * panel an axes array and be told when a selection changes. Handles\n * subscriptions, validation and defaults. `onChange` fires after every\n * selection or reset with the full selections.\n */\nexport function createVariantsSource(init: {\n axes: VariantAxis[];\n selections?: VariantSelections;\n onChange?: (selections: VariantSelections) => void;\n saveDefaults?: VariantsSource['saveDefaults'];\n title?: string;\n /** Persist edited definitions in a host's own storage, before notifying. */\n onManage?: (axes: VariantAxis[], selections: VariantSelections, action: VariantAction) => Promise<void> | void;\n}): VariantsSource & {\n /** Replace the axes (route change); selections not valid for the new set\n * are dropped. */\n setAxes(axes: VariantAxis[]): void;\n /** Replace the selections wholesale (URL hydration). */\n setSelections(selections: VariantSelections): void;\n} {\n let axes = init.axes;\n let selections = defaultSelections(axes);\n for (const axis of axes) {\n const value = init.selections?.[axis.id];\n if (value !== undefined && isValidSelection(axis, value)) selections[axis.id] = value;\n }\n const listeners = new Set<() => void>();\n const emit = () => {\n for (const l of listeners) l();\n };\n const changed = () => {\n init.onChange?.({ ...selections });\n emit();\n };\n return {\n title: init.title,\n saveDefaults: init.saveDefaults,\n getAxes: () => axes,\n getSelections: () => selections,\n async manage(action) {\n const axis = axes.find((a) => a.id === action.axisId);\n if (!axis) throw new Error('This selector is no longer available.');\n let next = [...axes];\n const picks = { ...selections };\n if (action.action === 'remove-option') {\n if (axis.kind === 'range') throw new Error('This selector has no removable options.');\n if (axis.kind === 'color') {\n const swatch = axis.swatches.find(s => s.id === action.optionId);\n if (!swatch || swatch.value.toLowerCase() === axis.defaultColor.toLowerCase()) throw new Error('Keep the default colour, or remove the whole selector.');\n const swatches = axis.swatches.filter(s => s.id !== action.optionId);\n if (picks[axis.id]?.toLowerCase() === swatch.value.toLowerCase()) picks[axis.id] = axis.defaultColor;\n next = next.map(a => a.id === axis.id ? { ...axis, swatches } : a);\n } else {\n if (action.optionId === axis.defaultOption) throw new Error('Keep the default option, or remove the whole selector.');\n if (!axis.options.some((o) => o.id === action.optionId)) throw new Error('This option is no longer available.');\n const options = axis.options.filter((o) => o.id !== action.optionId);\n if (picks[axis.id] === action.optionId) picks[axis.id] = axis.defaultOption;\n next = next.map((a) => a.id === axis.id ? { ...axis, options } : a);\n if (options.length === 1) next = next.filter((a) => a.id !== axis.id);\n }\n } else {\n next = next.filter((a) => a.id !== axis.id);\n if (action.action === 'keep-default') picks[axis.id] = axisDefault(axis);\n else delete picks[axis.id];\n }\n // A dependent control cannot outlive the decision that reveals it.\n const kept = new Set(next.map((a) => a.id));\n const removed = new Set(axes.filter((a) => !kept.has(a.id)).map((a) => a.id));\n const children = new Map<string, string[]>();\n for (const a of next) {\n if (!a.showWhen) continue;\n const dependents = children.get(a.showWhen.axis) ?? [];\n dependents.push(a.id);\n children.set(a.showWhen.axis, dependents);\n }\n // Set iteration includes newly added descendants and terminates on cycles.\n for (const id of removed) for (const child of children.get(id) ?? []) removed.add(child);\n next = next.filter((a) => !removed.has(a.id));\n for (const id of removed) if (id !== axis.id) delete picks[id];\n await init.onManage?.(next, picks, action);\n axes = next;\n selections = picks;\n changed();\n },\n setSelection(axisId, value) {\n const axis = axes.find((a) => a.id === axisId);\n if (!axis || !isValidSelection(axis, value)) return;\n if (selections[axisId] === value) return;\n selections = { ...selections, [axisId]: value };\n changed();\n },\n reset() {\n selections = defaultSelections(axes);\n changed();\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n setAxes(next) {\n axes = next;\n const kept: VariantSelections = defaultSelections(next);\n for (const a of next) {\n const v = selections[a.id];\n if (v !== undefined && isValidSelection(a, v)) kept[a.id] = v;\n }\n selections = kept;\n emit();\n },\n setSelections(next) {\n selections = { ...defaultSelections(axes) };\n for (const a of axes) {\n const v = next[a.id];\n if (v !== undefined && isValidSelection(a, v)) selections[a.id] = v;\n }\n changed();\n },\n };\n}\n\n/* ── The overlay adapter's wire shape ─────────────────────────────── */\n\n/**\n * One overlay variant: a directory of alternative source files that the dev\n * server swaps in for the real ones while the variant is active. Stored as\n * `variant.json` in `.isotone/variants/<id>/`, with the files mirrored\n * under it at their repo-relative paths.\n */\nexport interface OverlayVariant {\n /** Directory name. Lowercase, `a-z0-9-` only. */\n id: string;\n /** Panel label. Defaults to the id. */\n label: string;\n /** The route the variant belongs to; `\"\"` (or absent) shows on every route.\n * Only a scope string, like a note's route. */\n route?: string;\n /** The element the variant was asked about — the handle, not a boundary;\n * the changed files may affect more than this element. */\n element?: IterationElement | null;\n /** The note id the variant answers, when it came from one. */\n note?: string;\n /** What the variant tries, in a sentence. */\n description?: string;\n /**\n * The decision this variant is one answer to. Variants in the same group\n * are alternatives of one element and share a selector in the panel;\n * activating one deactivates its siblings. Same shape as an id. Absent,\n * the group is inferred from file overlap (see {@link overlayGroups}).\n */\n group?: string;\n /** What the panel calls the group's selector. Any member may carry it. */\n groupLabel?: string;\n createdAt: string;\n /**\n * Repo-relative source path → what the override was derived from. `base`\n * is the git blob id of the real file when the override was made (`null`\n * when the file was not hashable), so a later change to the real file can\n * be detected and three-way merged into the override.\n */\n files: Record<string, { base: string | null }>;\n}\n\n/** An overlay variant as the endpoint lists it: the manifest plus what the\n * server computed about it. */\nexport interface OverlayVariantStatus extends OverlayVariant {\n /** Files whose real source has changed since the override was made. */\n stale: string[];\n /** Files the manifest names but the variant directory no longer holds. */\n missing: string[];\n}\n\n/**\n * A knob: a range or colour axis the host declares in `.isotone/knobs.json`\n * next to its overlay variants. The panel renders it as a slider (or a colour\n * pick) on the routes it names, and the value reaches the page as a CSS\n * custom property on `<html>`, `--knob-<id>`, with the axis unit appended\n * (`--knob-fade-from: 40%`). The host reads it with a fallback that is the\n * committed default: `var(--knob-fade-from, 0%)`. \"Save as defaults\" files\n * the moved knobs in the same `[set-defaults]` note as the variant picks, so\n * the agent writes the numbers into the source and the knob file.\n */\nexport type KnobAxis = (RangeVariantAxis | ColorVariantAxis) & {\n /** Route pattern(s) the knob belongs on, like a variant's `route`; absent means every route. */\n route?: string | string[];\n};\n\n/** `GET` body of the variants endpoint. */\nexport interface OverlayVariantsList {\n variants: OverlayVariantStatus[];\n /** The active variant ids, at most one per group; `[]` is the default. */\n active: string[];\n /** The host's knobs, every route; the client filters by route. */\n knobs?: KnobAxis[];\n}\n\n/** The custom property a knob writes: `--knob-<id>`. */\nexport function knobVar(id: string): string {\n return `--knob-${id}`;\n}\n\n/** The property's value: a range carries its unit, a colour is its hex. */\nexport function knobCssValue(axis: KnobAxis, value: string): string {\n return axis.kind === 'range' ? `${value}${axis.unit ?? ''}` : value;\n}\n\n/** The knobs that belong on `route`. */\nexport function knobsFor(knobs: KnobAxis[] | undefined, route: string): KnobAxis[] {\n return (knobs ?? []).filter((k) => {\n const patterns = k.route === undefined ? [undefined] : ([] as (string | undefined)[]).concat(k.route);\n return patterns.some((p) => variantAppliesTo(p, route));\n });\n}\n\n/** Write every knob's value onto the root element; missing values clear the property. */\nexport function applyKnobValues(\n knobs: KnobAxis[],\n values: VariantSelections,\n root: { style: { setProperty(name: string, value: string): void; removeProperty(name: string): void } },\n): void {\n for (const k of knobs) {\n const v = values[k.id];\n if (v === undefined || v === axisDefault(k)) root.style.removeProperty(knobVar(k.id));\n else root.style.setProperty(knobVar(k.id), knobCssValue(k, v));\n }\n}\n\n/** The knobs that sit off their default, with the value the panel showed. */\nexport function knobDelta(\n knobs: KnobAxis[],\n values: VariantSelections,\n): { axis: KnobAxis; value: string; shown: string }[] {\n return knobs.flatMap((k) => {\n const v = values[k.id];\n if (v === undefined || v === axisDefault(k)) return [];\n const shown = k.kind === 'range' ? formatRangeValue(k, v) : v;\n return [{ axis: k, value: v, shown }];\n });\n}\n\n/** The endpoint path the client calls. */\nexport const VARIANTS_ENDPOINT = '/api/iteration/variants';\n\n/** Where the browser keeps knob values between reloads. */\nexport const KNOBS_STORAGE_KEY = 'isotone:knobs';\n\n/** The option id that means \"no variant, the real files\". */\nexport const OVERLAY_DEFAULT_OPTION = 'default';\n\n/** The shape an explicit `group` has to take: an id, like a variant's. */\nexport const OVERLAY_GROUP_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;\n\n/** A set of alternatives to one decision, as the panel renders it. */\nexport interface OverlayGroup<T extends OverlayVariant = OverlayVariant> {\n /** The explicit `group`, else a slug of the sorted member ids. */\n id: string;\n label: string;\n /** In listing order. */\n members: T[];\n}\n\n/**\n * Partition variants into the decisions they answer. An explicit `group`\n * wins; the rest are joined by file overlap, because two copies of the same\n * file can never both be active, so variants sharing a file are alternatives\n * whether or not anyone said so. Overlap is transitive (connected\n * components): if A shares a file with B and B with C, all three are one\n * group even when A and C touch different files.\n *\n * The label is any member's `groupLabel`, else the basename of a file every\n * member overrides (the element's own file, in practice), else a lone\n * member's label, else the group id. Pure, so the panel, the CLI and the\n * tests all agree on the grouping.\n */\nexport function overlayGroups<T extends OverlayVariant>(variants: T[]): OverlayGroup<T>[] {\n const explicit = new Map<string, T[]>();\n const loose: T[] = [];\n for (const v of variants) {\n if (v.group) {\n const members = explicit.get(v.group) ?? [];\n members.push(v);\n explicit.set(v.group, members);\n } else {\n loose.push(v);\n }\n }\n\n // Union-find over the loose variants, keyed by index into `loose`.\n const parent = loose.map((_, i) => i);\n const find = (i: number): number => {\n while (parent[i] !== i) {\n parent[i] = parent[parent[i]];\n i = parent[i];\n }\n return i;\n };\n const owner = new Map<string, number>();\n loose.forEach((v, i) => {\n for (const file of Object.keys(v.files)) {\n const other = owner.get(file);\n if (other === undefined) owner.set(file, i);\n else parent[find(i)] = find(other);\n }\n });\n const inferred = new Map<number, T[]>();\n loose.forEach((v, i) => {\n const root = find(i);\n const members = inferred.get(root) ?? [];\n members.push(v);\n inferred.set(root, members);\n });\n\n // Emit in listing order of each group's first member, so a new variant\n // appends a selector instead of reshuffling the ones the reviewer knows.\n const groups: OverlayGroup<T>[] = [];\n const index = new Map(loose.map((variant, i) => [variant, i]));\n const seen = new Set<T[]>();\n for (const v of variants) {\n const members = v.group ? explicit.get(v.group) : inferred.get(find(index.get(v) ?? 0));\n if (!members) continue;\n if (seen.has(members)) continue;\n seen.add(members);\n const id = v.group ?? members.map((m) => m.id).sort().join('+');\n groups.push({ id, label: groupLabel(id, members), members });\n }\n return groups;\n}\n\nfunction groupLabel(id: string, members: OverlayVariant[]): string {\n const named = members.find((m) => m.groupLabel);\n if (named?.groupLabel) return named.groupLabel;\n const [first, ...rest] = members;\n const shared = Object.keys(first?.files ?? {}).find((file) =>\n rest.every((m) => Object.hasOwn(m.files, file)),\n );\n if (shared) return shared.slice(shared.lastIndexOf('/') + 1);\n if (members.length === 1 && first) return first.label || first.id;\n return id;\n}\n\n/**\n * The overlay adapter's axes for `route`: one select per group that has a\n * member on this route (a member made for it or for every route), with\n * Default plus those members. A selector belongs to the page its element is\n * on and nowhere else, active or not: an active variant\n * made for another route keeps applying, since the file swap is global, but\n * the place to switch it back is that route's page. Pure, so the panel and\n * the tests can build it from a listing.\n */\n/**\n * Does a variant scoped to `pattern` belong on `route`? No pattern means\n * everywhere; an exact route means that page; a pattern ending in `/*`\n * means the base and every page under it, so a variant made on one\n * detail page (`/items/42`) can be filed for `/items/*` and show on all.\n */\nexport function variantAppliesTo(pattern: string | undefined, route: string): boolean {\n if (!pattern) return true;\n if (pattern.endsWith('/*')) {\n const base = pattern.slice(0, -2);\n return route === base || route.startsWith(`${base}/`);\n }\n return pattern === route;\n}\n\nexport function overlayAxes(list: OverlayVariantsList, route: string): SelectVariantAxis[] {\n const axes: SelectVariantAxis[] = [];\n for (const group of overlayGroups(list.variants)) {\n const mine = group.members.filter((v) => variantAppliesTo(v.route, route));\n if (mine.length === 0) continue;\n axes.push({\n id: group.id,\n label: group.label,\n defaultOption: OVERLAY_DEFAULT_OPTION,\n options: [\n { id: OVERLAY_DEFAULT_OPTION, label: 'Default', hint: 'The committed source' },\n ...mine.map((v) => ({ id: v.id, label: v.label || v.id, hint: overlayHint(v) })),\n ],\n });\n }\n return axes;\n}\n\nfunction overlayHint(v: OverlayVariantStatus): string {\n const parts: string[] = [];\n if (v.description) parts.push(v.description);\n if (v.element) {\n parts.push(`↳ <${v.element.tag}>${v.element.text ? ` \"${v.element.text}\"` : ''}`);\n }\n const files = Object.keys(v.files);\n if (files.length) parts.push(files.join(', '));\n if (v.stale.length) parts.push(`stale: ${v.stale.join(', ')}`);\n if (v.missing.length) parts.push(`missing: ${v.missing.join(', ')}`);\n return parts.join('\\n');\n}\n\n/**\n * A `VariantsSource` over the overlay endpoint. One select axis per group on\n * the route; choosing an option replaces that group's active member and\n * posts the whole active set, and the server answers by hot-swapping the\n * files, so nothing here has to re-render anything. `refresh()` re-reads\n * the listing; the panel calls it when it opens.\n */\nexport function createOverlayVariantsSource(cfg: {\n /** Absolute or same-origin URL of the variants endpoint. */\n endpoint: string;\n getRoute: () => string;\n /** Where knob values live between reloads; omit for none (tests). Default: `localStorage`. */\n storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'> | null;\n /** The element knobs write their properties on. Default: `document.documentElement`. */\n knobRoot?: Parameters<typeof applyKnobValues>[2] | null;\n}): VariantsSource & {\n refresh(): Promise<void>;\n readonly list: OverlayVariantsList | null;\n /** The knobs on the current route and their values, for the save note. */\n readonly knobs: { axes: KnobAxis[]; values: VariantSelections };\n} {\n let list: OverlayVariantsList | null = null;\n let error: string | null = null;\n let intended: string[] | null = null;\n let activation: Promise<void> | null = null;\n let revision = 0;\n let refreshRevision = 0;\n const listeners = new Set<() => void>();\n const emit = () => {\n for (const l of listeners) l();\n };\n const selects = (): SelectVariantAxis[] => (list ? overlayAxes(list, cfg.getRoute()) : []);\n const knobAxes = (): KnobAxis[] => knobsFor(list?.knobs, cfg.getRoute());\n const axes = (): VariantAxis[] => [...selects(), ...knobAxes()];\n\n // Knob values are review-time state, kept in the browser rather than on\n // the server: a slider is a look, not a file swap.\n const storage = (() => {\n if (cfg.storage !== undefined) return cfg.storage;\n try { return typeof localStorage === 'undefined' ? null : localStorage; }\n catch { return null; }\n })();\n const knobRoot =\n cfg.knobRoot === undefined\n ? typeof document === 'undefined'\n ? null\n : document.documentElement\n : cfg.knobRoot;\n let knobValues: VariantSelections = (() => {\n const parse = (raw: string | null): VariantSelections => {\n try {\n const parsed = raw ? (JSON.parse(raw) as unknown) : null;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? Object.fromEntries(Object.entries(parsed).filter((entry) => typeof entry[1] === 'string')) : {};\n } catch { return {}; }\n };\n try {\n return storage ? parse(readBrowserStorage(storage, KNOBS_STORAGE_KEY, (current, previous) =>\n JSON.stringify({ ...parse(previous), ...parse(current) }))) : {};\n } catch {\n return {};\n }\n })();\n const persistKnobs = () => {\n try {\n if (!storage) return;\n if (Object.keys(knobValues).length === 0) removeBrowserStorage(storage, KNOBS_STORAGE_KEY);\n else writeBrowserStorage(storage, KNOBS_STORAGE_KEY, JSON.stringify(knobValues));\n } catch {\n // Storage full or blocked: the values still apply for this page view.\n }\n };\n const applyKnobs = () => {\n if (knobRoot) applyKnobValues(list?.knobs ?? [], knobValues, knobRoot);\n };\n async function drainActivations(): Promise<void> {\n while (intended !== null) {\n const active = intended;\n const sentRevision = revision;\n try {\n const res = await request(`${cfg.endpoint}/active`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ active }),\n });\n if (!res.ok) throw new Error(`Activating variants failed (${res.status}). Try again.`);\n if (list) list = { ...list, active };\n error = null;\n } catch (cause) {\n error = cause instanceof Error ? cause.message : 'Could not activate this variant. Try again.';\n }\n // Newer picks include the full intended set, including other groups.\n // Completing or failing an older request must not discard that intent.\n if (sentRevision === revision) intended = null;\n emit();\n }\n }\n function activate(active: string[]): void {\n intended = active;\n revision++;\n error = null;\n emit();\n scheduleActivation();\n }\n function scheduleActivation(): void {\n if (activation) return;\n activation = Promise.resolve().then(drainActivations).finally(() => {\n activation = null;\n if (intended !== null) scheduleActivation();\n });\n }\n return {\n title: 'Variants',\n getError: () => error,\n get list() {\n return list;\n },\n get knobs() {\n return { axes: knobAxes(), values: { ...knobValues } };\n },\n async refresh() {\n const refreshId = ++refreshRevision;\n const startedRevision = revision;\n const wasActivating = activation !== null;\n const next = await request(cfg.endpoint, undefined, async (res) => {\n if (!res.ok) throw new Error(`Loading variants failed (${res.status})`);\n return await res.json() as OverlayVariantsList;\n });\n if (refreshId !== refreshRevision) return;\n // An older GET must not roll back a successful POST that finished while\n // the listing was loading. Definitions can still refresh independently.\n const previousKnobs = list?.knobs ?? [];\n list = list && (wasActivating || activation !== null || startedRevision !== revision)\n ? { ...next, active: list.active } : next;\n if (knobRoot) applyKnobValues(previousKnobs, {}, knobRoot);\n const valid = new Map((list.knobs ?? []).map((knob) => [knob.id, knob]));\n knobValues = Object.fromEntries(Object.entries(knobValues).filter(([id, value]) => {\n const knob = valid.get(id);\n return knob && isValidSelection(knob, value);\n }));\n applyKnobs();\n emit();\n },\n getAxes: axes,\n async manage(action) {\n const next = await request(`${cfg.endpoint}/manage`, {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ ...action, route: cfg.getRoute() }),\n }, async (res) => {\n if (!res.ok) {\n const body = await res.json().catch(() => ({})) as { error?: string };\n throw new Error(body.error ?? 'Could not update this selector.');\n }\n return await res.json() as OverlayVariantsList;\n });\n const previousKnobs = list?.knobs ?? [];\n list = next;\n const previous = previousKnobs.find(k => k.id === action.axisId);\n if (action.action === 'remove-option' && previous?.kind === 'color') {\n const removed = previous.swatches.find(s => s.id === action.optionId);\n if (removed && knobValues[previous.id]?.toLowerCase() === removed.value.toLowerCase()) delete knobValues[previous.id];\n }\n if (knobRoot) applyKnobValues(previousKnobs, {}, knobRoot);\n const remaining = new Set(list.knobs?.map((k) => k.id));\n knobValues = Object.fromEntries(Object.entries(knobValues).filter(([id]) => remaining.has(id)));\n persistKnobs();\n applyKnobs();\n emit();\n },\n getSelections() {\n const active = new Set(intended ?? list?.active ?? []);\n const picks = Object.fromEntries(\n selects().map((axis) => [\n axis.id,\n axis.options.find((o) => active.has(o.id))?.id ?? OVERLAY_DEFAULT_OPTION,\n ]),\n );\n for (const k of knobAxes()) picks[k.id] = knobValues[k.id] ?? axisDefault(k);\n return picks;\n },\n setSelection(axisId, value) {\n const knob = knobAxes().find((k) => k.id === axisId);\n if (knob) {\n if (!isValidSelection(knob, value)) return;\n if (value === axisDefault(knob)) delete knobValues[axisId];\n else knobValues = { ...knobValues, [axisId]: value };\n persistKnobs();\n applyKnobs();\n emit();\n return;\n }\n const axis = selects().find((a) => a.id === axisId);\n if (!axis || !isValidSelection(axis, value)) return;\n // The group's members leave the set and the pick joins it; the other\n // groups' picks ride along untouched.\n const members = new Set(axis.options.map((o) => o.id));\n const next = (intended ?? list?.active ?? []).filter((id) => !members.has(id));\n if (value !== OVERLAY_DEFAULT_OPTION) next.push(value);\n activate(next);\n },\n reset() {\n knobValues = {};\n persistKnobs();\n applyKnobs();\n activate([]);\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/core/browser-storage.ts","../src/core/request.ts","../src/core/variants.ts"],"sourcesContent":["/** Browser-only migration from the former package's default namespaces. */\nexport const FEEDBACK_STORAGE_KEY = 'isotone-feedback';\ntype BrowserStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;\n\nfunction legacyKey(key: string): string | undefined {\n if (key === FEEDBACK_STORAGE_KEY || key.startsWith(`${FEEDBACK_STORAGE_KEY}::draft::`))\n return `iso-iterate-feedback${key.slice(FEEDBACK_STORAGE_KEY.length)}`;\n if (key === 'isotone:knobs') return 'iso-iterate:knobs';\n return undefined;\n}\n\n/** A failed migration remains readable under its old key for the next visit. */\nexport function readBrowserStorage(\n storage: BrowserStorage,\n key: string,\n merge: (current: string | null, previous: string) => string = (current, previous) => current ?? previous,\n): string | null {\n let current: string | null = null;\n try { current = storage.getItem(key); }\n catch { return null; }\n const previousKey = legacyKey(key);\n if (!previousKey) return current;\n let previous: string | null;\n try { previous = storage.getItem(previousKey); }\n catch { return current; }\n if (previous === null) return current;\n const next = merge(current, previous);\n try { writeBrowserStorage(storage, key, next); }\n catch { /* Quota or access restrictions must not hide the previous value. */ }\n return next;\n}\n\nexport function writeBrowserStorage(storage: BrowserStorage, key: string, value: string): void {\n storage.setItem(key, value);\n const previousKey = legacyKey(key);\n if (!previousKey) return;\n try {\n if (storage.getItem(key) === value) storage.removeItem(previousKey);\n } catch { /* Keep the old value unless its replacement was verified. */ }\n}\n\n/** Explicitly clearing a value also clears its legacy fallback. */\nexport function removeBrowserStorage(storage: BrowserStorage, key: string): void {\n storage.removeItem(key);\n const previousKey = legacyKey(key);\n if (previousKey) storage.removeItem(previousKey);\n}\n","type RequestInput = Parameters<typeof fetch>[0];\n\nexport function request(input: RequestInput, init?: RequestInit): Promise<Response>;\nexport function request<T>(input: RequestInput, init: RequestInit | undefined, consume: (response: Response) => Promise<T>): Promise<T>;\n\n/** The deadline includes body consumption, not just receipt of the headers. */\nexport async function request<T>(input: RequestInput, init?: RequestInit, consume?: (response: Response) => Promise<T>): Promise<Response | T> {\n const native = (globalThis as { __isotoneFetch?: typeof fetch }).__isotoneFetch;\n const controller = new AbortController();\n const signal = init?.signal ?? (typeof Request !== 'undefined' && input instanceof Request ? input.signal : undefined);\n const abort = () => controller.abort(signal?.reason);\n if (signal?.aborted) abort();\n else signal?.addEventListener('abort', abort, { once: true });\n const timeout = setTimeout(() => controller.abort(new DOMException('The local server did not respond in time.', 'TimeoutError')), 5_000);\n let aborted: () => void = () => {};\n const cancelled = new Promise<never>((_, reject) => {\n aborted = () => reject(controller.signal.reason);\n if (controller.signal.aborted) aborted();\n else controller.signal.addEventListener('abort', aborted, { once: true });\n });\n try {\n return await Promise.race([cancelled, (async () => {\n if (controller.signal.aborted) throw controller.signal.reason;\n const response = await (native ?? globalThis.fetch)(input, { ...init, signal: controller.signal });\n return consume ? consume(response) : response;\n })()]);\n } finally {\n clearTimeout(timeout);\n signal?.removeEventListener('abort', abort);\n controller.signal.removeEventListener('abort', aborted);\n }\n}\n","/**\n * isotone — variants: the framework-free model.\n *\n * A variant axis is one tunable decision about a page: a discrete choice\n * (select), a continuous value (range) or a colour. The panel renders axes;\n * where their state lives is the host's business. Two hosts ship today:\n *\n * - A host with its own variant system (its own axes and\n * `useVariant` hooks) hands the panel a {@link VariantsSource} over that\n * state. The panel is then a view; the host still owns `?v=` sync,\n * defaults and promotion.\n * - A host with no variant system gets the overlay adapter: alternative\n * source files under `.isotone/variants/<id>/…` that the Vite plugin\n * swaps in for the real ones while a variant is active. The browser sees\n * one select axis per group of alternatives, on the route they belong to\n * ({@link overlayGroups}, {@link createOverlayVariantsSource}).\n *\n * Everything in this module is pure or DOM-free and runs in a `node` test.\n */\n\nimport type { IterationElement } from './index';\nimport { request } from './request';\nimport { readBrowserStorage, removeBrowserStorage, writeBrowserStorage } from './browser-storage';\n\n/* ── Axes ─────────────────────────────────────────────────────────────── */\n\nexport interface VariantOption {\n /** Stable short id, e.g. \"a\", \"b\". */\n id: string;\n /** Short label (1–2 words); it renders as a segment and truncates. */\n label: string;\n /** Optional preview image URL. When every option of an axis has one, the\n * panel renders the axis as a thumbnail grid. */\n image?: string;\n /** Optional secondary text shown as a tooltip on the option. */\n hint?: string;\n}\n\n/** Show an axis only while another axis holds (`equals`) or does not hold\n * (`not`) a value. Exactly one of the two should be set. */\nexport interface VariantAxisCondition {\n axis: string;\n equals?: string;\n not?: string;\n}\n\nexport interface SelectVariantAxis {\n kind?: 'select';\n id: string;\n label: string;\n options: VariantOption[];\n /** The primary option; what a host renders when nothing is selected. */\n defaultOption: string;\n showWhen?: VariantAxisCondition;\n /** Panel grouping: a hairline separates consecutive different sections. */\n section?: string;\n}\n\nexport interface RangeVariantAxis {\n kind: 'range';\n id: string;\n label: string;\n min: number;\n max: number;\n step: number;\n defaultValue: number;\n /** How the panel prints the value (\"px\", \"°\", \"em\"). Omitted: a 0–1 axis\n * prints as a percentage, anything else as the raw number. */\n unit?: string;\n showWhen?: VariantAxisCondition;\n section?: string;\n}\n\nexport interface ColorSwatch {\n id: string;\n label: string;\n /** Hex, e.g. \"#ea382e\". */\n value: string;\n}\n\nexport interface ColorVariantAxis {\n kind: 'color';\n id: string;\n label: string;\n /** Hex. */\n defaultColor: string;\n /** Quick-pick presets shown as swatches (include the default). */\n swatches: ColorSwatch[];\n showWhen?: VariantAxisCondition;\n section?: string;\n}\n\nexport type VariantAxis = SelectVariantAxis | RangeVariantAxis | ColorVariantAxis;\n\n/** Axis id → selected value. Ranges stringify their number. */\nexport type VariantSelections = Record<string, string>;\n\nexport interface VariantAction {\n axisId: string;\n action: 'remove-option' | 'remove-axis' | 'keep-default';\n optionId?: string;\n}\n\n/* ── Pure helpers ─────────────────────────────────────────────────────── */\n\n/** The primary value of an axis, as the selection string. */\nexport function axisDefault(axis: VariantAxis): string {\n if (axis.kind === 'range') return String(axis.defaultValue);\n if (axis.kind === 'color') return axis.defaultColor;\n return axis.defaultOption;\n}\n\n/** True when `value` is legal for the axis. */\nexport function isValidSelection(axis: VariantAxis, value: string): boolean {\n if (axis.kind === 'range') {\n const n = Number(value);\n return value.trim() !== '' && Number.isFinite(n) && n >= axis.min && n <= axis.max;\n }\n if (axis.kind === 'color') return /^#[0-9a-fA-F]{6}$/.test(value);\n return axis.options.some((o) => o.id === value);\n}\n\nexport function defaultSelections(axes: VariantAxis[]): VariantSelections {\n return Object.fromEntries(axes.map((a) => [a.id, axisDefault(a)]));\n}\n\n/** `\"headline:b,haze-speed:0.7\"` → validated selections. Unknown axes and\n * illegal values are dropped rather than failing the whole string. */\nexport function parseVariantParam(axes: VariantAxis[], raw: string): VariantSelections {\n const out = new Map<string, string>();\n const byId = new Map(axes.map((axis) => [axis.id, axis]));\n for (const pair of raw.split(',')) {\n const at = pair.indexOf(':');\n if (at < 0) continue;\n const axisId = pair.slice(0, at);\n const value = pair.slice(at + 1);\n const axis = byId.get(axisId);\n if (axis && isValidSelection(axis, value)) out.set(axisId, value);\n }\n return Object.fromEntries(out);\n}\n\n/** Non-default selections only, `\"\"` when everything is at its default. */\nexport function serializeVariantParam(\n axes: VariantAxis[],\n selections: VariantSelections,\n): string {\n return axes\n .filter((a) => selections[a.id] !== undefined && selections[a.id] !== axisDefault(a))\n .map((a) => `${a.id}:${selections[a.id]}`)\n .join(',');\n}\n\n/** Whether any axis differs from its default. */\nexport function isDirty(axes: VariantAxis[], selections: VariantSelections): boolean {\n return axes.some((a) => selections[a.id] !== undefined && selections[a.id] !== axisDefault(a));\n}\n\n/**\n * Whether an axis shows, given every axis and the current selections.\n * A hidden parent hides its children. Cycles are hidden, and long chains\n * are evaluated iteratively rather than consuming the call stack.\n */\nexport function isAxisVisible(\n axis: VariantAxis,\n axes: VariantAxis[],\n selections: VariantSelections,\n _depth = 0,\n): boolean {\n return visibilityById(axes.includes(axis) ? axes : [...axes, axis], selections).get(axis.id) ?? false;\n}\n\nexport function visibleAxes(axes: VariantAxis[], selections: VariantSelections): VariantAxis[] {\n const visibility = visibilityById(axes, selections);\n return axes.filter((axis) => visibility.get(axis.id));\n}\n\nfunction visibilityById(axes: VariantAxis[], selections: VariantSelections): Map<string, boolean> {\n const byId = new Map(axes.map((axis) => [axis.id, axis]));\n const visible = new Map<string, boolean>();\n for (const axis of axes) {\n const trail = new Set<string>();\n let current: VariantAxis | undefined = axis;\n let result = true;\n while (current) {\n const known = visible.get(current.id);\n if (known !== undefined) { result = known; break; }\n if (trail.has(current.id)) { result = false; break; }\n trail.add(current.id);\n const condition: VariantAxisCondition | undefined = current.showWhen;\n if (!condition) break;\n const parent = byId.get(condition.axis);\n const value = selections[condition.axis] ?? (parent ? axisDefault(parent) : undefined);\n if ((condition.equals !== undefined && value !== condition.equals) ||\n (condition.not !== undefined && value === condition.not)) { result = false; break; }\n current = parent;\n }\n for (const id of trail) visible.set(id, result);\n }\n return visible;\n}\n\n/** How the panel prints a range value: its unit when declared, a percentage\n * for a bare 0–1 axis, else the raw number. */\nexport function formatRangeValue(axis: RangeVariantAxis, value: string): string {\n if (axis.unit !== undefined) return `${value}${axis.unit}`;\n if (axis.min === 0 && axis.max === 1) return `${Math.round(Number(value) * 100)}%`;\n return value;\n}\n\n/* ── The source contract ──────────────────────────────────────────────── */\n\n/**\n * What the panel renders variants from. The host owns the state; the panel\n * reads it through these calls and re-renders on `subscribe` notifications.\n * Every method must be safe to call at any time after mount, including\n * before the host has any axes (return `[]`).\n */\nexport interface VariantsSource {\n /** The axes for the current page. May change over time (route change). */\n getAxes(): VariantAxis[];\n /** The current selections. Missing keys mean \"at default\". */\n getSelections(): VariantSelections;\n setSelection(axisId: string, value: string): void;\n /** Return every axis to its default. */\n reset(): void;\n /** Notify on any change to axes or selections. Returns an unsubscribe. */\n subscribe(listener: () => void): () => void;\n /** A recoverable transport error; notify subscribers when it changes. */\n getError?: () => string | null;\n /**\n * When present, the panel shows \"Save as defaults\" and calls this with the\n * current selections. A host that promotes by editing source (one that\n * files a `[set-defaults]` note for the agent) implements it; a host\n * whose variants are whole alternatives (the overlay adapter) leaves it\n * out and promotes from the CLI instead.\n */\n saveDefaults?: (selections: VariantSelections) => Promise<void> | void;\n /** Panel heading; defaults to \"Variants\". */\n title?: string;\n /** Optional durable host mutation. Reject to leave the UI unchanged. */\n manage?: (action: VariantAction) => Promise<void> | void;\n}\n\n/**\n * A `VariantsSource` over plain state, for hosts that just want to hand the\n * panel an axes array and be told when a selection changes. Handles\n * subscriptions, validation and defaults. `onChange` fires after every\n * selection or reset with the full selections.\n */\nexport function createVariantsSource(init: {\n axes: VariantAxis[];\n selections?: VariantSelections;\n onChange?: (selections: VariantSelections) => void;\n saveDefaults?: VariantsSource['saveDefaults'];\n title?: string;\n /** Persist edited definitions in a host's own storage, before notifying. */\n onManage?: (axes: VariantAxis[], selections: VariantSelections, action: VariantAction) => Promise<void> | void;\n}): VariantsSource & {\n /** Replace the axes (route change); selections not valid for the new set\n * are dropped. */\n setAxes(axes: VariantAxis[]): void;\n /** Replace the selections wholesale (URL hydration). */\n setSelections(selections: VariantSelections): void;\n} {\n let axes = init.axes;\n let selections = defaultSelections(axes);\n for (const axis of axes) {\n const value = init.selections?.[axis.id];\n if (value !== undefined && isValidSelection(axis, value)) selections[axis.id] = value;\n }\n const listeners = new Set<() => void>();\n const emit = () => {\n for (const l of listeners) l();\n };\n const changed = () => {\n init.onChange?.({ ...selections });\n emit();\n };\n return {\n title: init.title,\n saveDefaults: init.saveDefaults,\n getAxes: () => axes,\n getSelections: () => selections,\n async manage(action) {\n const axis = axes.find((a) => a.id === action.axisId);\n if (!axis) throw new Error('This selector is no longer available.');\n let next = [...axes];\n const picks = { ...selections };\n if (action.action === 'remove-option') {\n if (axis.kind === 'range') throw new Error('This selector has no removable options.');\n if (axis.kind === 'color') {\n const swatch = axis.swatches.find(s => s.id === action.optionId);\n if (!swatch || swatch.value.toLowerCase() === axis.defaultColor.toLowerCase()) throw new Error('Keep the default colour, or remove the whole selector.');\n const swatches = axis.swatches.filter(s => s.id !== action.optionId);\n if (picks[axis.id]?.toLowerCase() === swatch.value.toLowerCase()) picks[axis.id] = axis.defaultColor;\n next = next.map(a => a.id === axis.id ? { ...axis, swatches } : a);\n } else {\n if (action.optionId === axis.defaultOption) throw new Error('Keep the default option, or remove the whole selector.');\n if (!axis.options.some((o) => o.id === action.optionId)) throw new Error('This option is no longer available.');\n const options = axis.options.filter((o) => o.id !== action.optionId);\n if (picks[axis.id] === action.optionId) picks[axis.id] = axis.defaultOption;\n next = next.map((a) => a.id === axis.id ? { ...axis, options } : a);\n if (options.length === 1) next = next.filter((a) => a.id !== axis.id);\n }\n } else {\n next = next.filter((a) => a.id !== axis.id);\n if (action.action === 'keep-default') picks[axis.id] = axisDefault(axis);\n else delete picks[axis.id];\n }\n // A dependent control cannot outlive the decision that reveals it.\n const kept = new Set(next.map((a) => a.id));\n const removed = new Set(axes.filter((a) => !kept.has(a.id)).map((a) => a.id));\n const children = new Map<string, string[]>();\n for (const a of next) {\n if (!a.showWhen) continue;\n const dependents = children.get(a.showWhen.axis) ?? [];\n dependents.push(a.id);\n children.set(a.showWhen.axis, dependents);\n }\n // Set iteration includes newly added descendants and terminates on cycles.\n for (const id of removed) for (const child of children.get(id) ?? []) removed.add(child);\n next = next.filter((a) => !removed.has(a.id));\n for (const id of removed) if (id !== axis.id) delete picks[id];\n await init.onManage?.(next, picks, action);\n axes = next;\n selections = picks;\n changed();\n },\n setSelection(axisId, value) {\n const axis = axes.find((a) => a.id === axisId);\n if (!axis || !isValidSelection(axis, value)) return;\n if (selections[axisId] === value) return;\n selections = { ...selections, [axisId]: value };\n changed();\n },\n reset() {\n selections = defaultSelections(axes);\n changed();\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n setAxes(next) {\n axes = next;\n const kept: VariantSelections = defaultSelections(next);\n for (const a of next) {\n const v = selections[a.id];\n if (v !== undefined && isValidSelection(a, v)) kept[a.id] = v;\n }\n selections = kept;\n emit();\n },\n setSelections(next) {\n selections = { ...defaultSelections(axes) };\n for (const a of axes) {\n const v = next[a.id];\n if (v !== undefined && isValidSelection(a, v)) selections[a.id] = v;\n }\n changed();\n },\n };\n}\n\n/* ── The overlay adapter's wire shape ─────────────────────────────── */\n\n/**\n * One overlay variant: a directory of alternative source files that the dev\n * server swaps in for the real ones while the variant is active. Stored as\n * `variant.json` in `.isotone/variants/<id>/`, with the files mirrored\n * under it at their repo-relative paths.\n */\nexport interface OverlayVariant {\n /** Directory name. Lowercase, `a-z0-9-` only. */\n id: string;\n /** Panel label. Defaults to the id. */\n label: string;\n /** The route the variant belongs to; `\"\"` (or absent) shows on every route.\n * Only a scope string, like a note's route. */\n route?: string;\n /** The element the variant was asked about — the handle, not a boundary;\n * the changed files may affect more than this element. */\n element?: IterationElement | null;\n /** The note id the variant answers, when it came from one. */\n note?: string;\n /** What the variant tries, in a sentence. */\n description?: string;\n /**\n * The decision this variant is one answer to. Variants in the same group\n * are alternatives of one element and share a selector in the panel;\n * activating one deactivates its siblings. Same shape as an id. Absent,\n * the group is inferred from file overlap (see {@link overlayGroups}).\n */\n group?: string;\n /** What the panel calls the group's selector. Any member may carry it. */\n groupLabel?: string;\n createdAt: string;\n /**\n * Repo-relative source path → what the override was derived from. `base`\n * is the git blob id of the real file when the override was made (`null`\n * when the file was not hashable), so a later change to the real file can\n * be detected and three-way merged into the override.\n */\n files: Record<string, { base: string | null }>;\n}\n\n/** An overlay variant as the endpoint lists it: the manifest plus what the\n * server computed about it. */\nexport interface OverlayVariantStatus extends OverlayVariant {\n /** Files whose real source has changed since the override was made. */\n stale: string[];\n /** Files the manifest names but the variant directory no longer holds. */\n missing: string[];\n}\n\n/**\n * A knob: a range or colour axis the host declares in `.isotone/knobs.json`\n * next to its overlay variants. The panel renders it as a slider (or a colour\n * pick) on the routes it names, and the value reaches the page as a CSS\n * custom property on `<html>`, `--knob-<id>`, with the axis unit appended\n * (`--knob-fade-from: 40%`). The host reads it with a fallback that is the\n * committed default: `var(--knob-fade-from, 0%)`. \"Save as defaults\" files\n * the moved knobs in the same `[set-defaults]` note as the variant picks, so\n * the agent writes the numbers into the source and the knob file.\n */\nexport type KnobAxis = (RangeVariantAxis | ColorVariantAxis) & {\n /** Route pattern(s) the knob belongs on, like a variant's `route`; absent means every route. */\n route?: string | string[];\n};\n\n/** `GET` body of the variants endpoint. */\nexport interface OverlayVariantsList {\n variants: OverlayVariantStatus[];\n /** The active variant ids, at most one per group; `[]` is the default. */\n active: string[];\n /** The host's knobs, every route; the client filters by route. */\n knobs?: KnobAxis[];\n}\n\n/** The custom property a knob writes: `--knob-<id>`. */\nexport function knobVar(id: string): string {\n return `--knob-${id}`;\n}\n\n/** The property's value: a range carries its unit, a colour is its hex. */\nexport function knobCssValue(axis: KnobAxis, value: string): string {\n return axis.kind === 'range' ? `${value}${axis.unit ?? ''}` : value;\n}\n\n/** The knobs that belong on `route`. */\nexport function knobsFor(knobs: KnobAxis[] | undefined, route: string): KnobAxis[] {\n return (knobs ?? []).filter((k) => {\n const patterns = k.route === undefined ? [undefined] : ([] as (string | undefined)[]).concat(k.route);\n return patterns.some((p) => variantAppliesTo(p, route));\n });\n}\n\n/** Write every knob's value onto the root element; missing values clear the property. */\nexport function applyKnobValues(\n knobs: KnobAxis[],\n values: VariantSelections,\n root: { style: { setProperty(name: string, value: string): void; removeProperty(name: string): void } },\n): void {\n for (const k of knobs) {\n const v = values[k.id];\n if (v === undefined || v === axisDefault(k)) root.style.removeProperty(knobVar(k.id));\n else root.style.setProperty(knobVar(k.id), knobCssValue(k, v));\n }\n}\n\n/** The knobs that sit off their default, with the value the panel showed. */\nexport function knobDelta(\n knobs: KnobAxis[],\n values: VariantSelections,\n): { axis: KnobAxis; value: string; shown: string }[] {\n return knobs.flatMap((k) => {\n const v = values[k.id];\n if (v === undefined || v === axisDefault(k)) return [];\n const shown = k.kind === 'range' ? formatRangeValue(k, v) : v;\n return [{ axis: k, value: v, shown }];\n });\n}\n\n/** The endpoint path the client calls. */\nexport const VARIANTS_ENDPOINT = '/api/iteration/variants';\n\n/** Where the browser keeps knob values between reloads. */\nexport const KNOBS_STORAGE_KEY = 'isotone:knobs';\n\n/** The option id that means \"no variant, the real files\". */\nexport const OVERLAY_DEFAULT_OPTION = 'default';\n\n/** The shape an explicit `group` has to take: an id, like a variant's. */\nexport const OVERLAY_GROUP_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;\n\n/** A set of alternatives to one decision, as the panel renders it. */\nexport interface OverlayGroup<T extends OverlayVariant = OverlayVariant> {\n /** The explicit `group`, else a slug of the sorted member ids. */\n id: string;\n label: string;\n /** In listing order. */\n members: T[];\n}\n\n/**\n * Partition variants into the decisions they answer. An explicit `group`\n * wins; the rest are joined by file overlap, because two copies of the same\n * file can never both be active, so variants sharing a file are alternatives\n * whether or not anyone said so. Overlap is transitive (connected\n * components): if A shares a file with B and B with C, all three are one\n * group even when A and C touch different files.\n *\n * The label is any member's `groupLabel`, else the basename of a file every\n * member overrides (the element's own file, in practice), else a lone\n * member's label, else the group id. Pure, so the panel, the CLI and the\n * tests all agree on the grouping.\n */\nexport function overlayGroups<T extends OverlayVariant>(variants: T[]): OverlayGroup<T>[] {\n const explicit = new Map<string, T[]>();\n const loose: T[] = [];\n for (const v of variants) {\n if (v.group) {\n const members = explicit.get(v.group) ?? [];\n members.push(v);\n explicit.set(v.group, members);\n } else {\n loose.push(v);\n }\n }\n\n // Union-find over the loose variants, keyed by index into `loose`.\n const parent = loose.map((_, i) => i);\n const find = (i: number): number => {\n while (parent[i] !== i) {\n parent[i] = parent[parent[i]];\n i = parent[i];\n }\n return i;\n };\n const owner = new Map<string, number>();\n loose.forEach((v, i) => {\n for (const file of Object.keys(v.files)) {\n const other = owner.get(file);\n if (other === undefined) owner.set(file, i);\n else parent[find(i)] = find(other);\n }\n });\n const inferred = new Map<number, T[]>();\n loose.forEach((v, i) => {\n const root = find(i);\n const members = inferred.get(root) ?? [];\n members.push(v);\n inferred.set(root, members);\n });\n\n // Emit in listing order of each group's first member, so a new variant\n // appends a selector instead of reshuffling the ones the reviewer knows.\n const groups: OverlayGroup<T>[] = [];\n const index = new Map(loose.map((variant, i) => [variant, i]));\n const seen = new Set<T[]>();\n for (const v of variants) {\n const members = v.group ? explicit.get(v.group) : inferred.get(find(index.get(v) ?? 0));\n if (!members) continue;\n if (seen.has(members)) continue;\n seen.add(members);\n const id = v.group ?? members.map((m) => m.id).sort().join('+');\n groups.push({ id, label: groupLabel(id, members), members });\n }\n return groups;\n}\n\nfunction groupLabel(id: string, members: OverlayVariant[]): string {\n const named = members.find((m) => m.groupLabel);\n if (named?.groupLabel) return named.groupLabel;\n const [first, ...rest] = members;\n const shared = Object.keys(first?.files ?? {}).find((file) =>\n rest.every((m) => Object.hasOwn(m.files, file)),\n );\n if (shared) return shared.slice(shared.lastIndexOf('/') + 1);\n if (members.length === 1 && first) return first.label || first.id;\n return id;\n}\n\n/**\n * The overlay adapter's axes for `route`: one select per group that has a\n * member on this route (a member made for it or for every route), with\n * Default plus those members. A selector belongs to the page its element is\n * on and nowhere else, active or not: an active variant\n * made for another route keeps applying, since the file swap is global, but\n * the place to switch it back is that route's page. Pure, so the panel and\n * the tests can build it from a listing.\n */\n/**\n * Does a variant scoped to `pattern` belong on `route`? No pattern means\n * everywhere; an exact route means that page; a pattern ending in `/*`\n * means the base and every page under it, so a variant made on one\n * detail page (`/items/42`) can be filed for `/items/*` and show on all.\n */\nexport function variantAppliesTo(pattern: string | undefined, route: string): boolean {\n if (!pattern) return true;\n if (pattern.endsWith('/*')) {\n const base = pattern.slice(0, -2);\n return route === base || route.startsWith(`${base}/`);\n }\n return pattern === route;\n}\n\nexport function overlayAxes(list: OverlayVariantsList, route: string): SelectVariantAxis[] {\n const axes: SelectVariantAxis[] = [];\n for (const group of overlayGroups(list.variants)) {\n const mine = group.members.filter((v) => variantAppliesTo(v.route, route));\n if (mine.length === 0) continue;\n axes.push({\n id: group.id,\n label: group.label,\n defaultOption: OVERLAY_DEFAULT_OPTION,\n options: [\n { id: OVERLAY_DEFAULT_OPTION, label: 'Default', hint: 'The committed source' },\n ...mine.map((v) => ({ id: v.id, label: v.label || v.id, hint: overlayHint(v) })),\n ],\n });\n }\n return axes;\n}\n\nfunction overlayHint(v: OverlayVariantStatus): string {\n const parts: string[] = [];\n if (v.description) parts.push(v.description);\n if (v.element) {\n parts.push(`↳ <${v.element.tag}>${v.element.text ? ` \"${v.element.text}\"` : ''}`);\n }\n const files = Object.keys(v.files);\n if (files.length) parts.push(files.join(', '));\n if (v.stale.length) parts.push(`stale: ${v.stale.join(', ')}`);\n if (v.missing.length) parts.push(`missing: ${v.missing.join(', ')}`);\n return parts.join('\\n');\n}\n\n/**\n * A `VariantsSource` over the overlay endpoint. One select axis per group on\n * the route; choosing an option replaces that group's active member and\n * posts the whole active set, and the server answers by hot-swapping the\n * files, so nothing here has to re-render anything. `refresh()` re-reads\n * the listing; the panel calls it when it opens.\n */\nexport function createOverlayVariantsSource(cfg: {\n /** Absolute or same-origin URL of the variants endpoint. */\n endpoint: string;\n getRoute: () => string;\n /** Where knob values live between reloads; omit for none (tests). Default: `localStorage`. */\n storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'> | null;\n /** The element knobs write their properties on. Default: `document.documentElement`. */\n knobRoot?: Parameters<typeof applyKnobValues>[2] | null;\n}): VariantsSource & {\n refresh(): Promise<void>;\n readonly list: OverlayVariantsList | null;\n /** The knobs on the current route and their values, for the save note. */\n readonly knobs: { axes: KnobAxis[]; values: VariantSelections };\n} {\n let list: OverlayVariantsList | null = null;\n let error: string | null = null;\n let intended: string[] | null = null;\n let activation: Promise<void> | null = null;\n let revision = 0;\n let refreshRevision = 0;\n const listeners = new Set<() => void>();\n const emit = () => {\n for (const l of listeners) l();\n };\n const selects = (): SelectVariantAxis[] => (list ? overlayAxes(list, cfg.getRoute()) : []);\n const knobAxes = (): KnobAxis[] => knobsFor(list?.knobs, cfg.getRoute());\n const axes = (): VariantAxis[] => [...selects(), ...knobAxes()];\n\n // Knob values are review-time state, kept in the browser rather than on\n // the server: a slider is a look, not a file swap.\n const storage = (() => {\n if (cfg.storage !== undefined) return cfg.storage;\n try { return typeof localStorage === 'undefined' ? null : localStorage; }\n catch { return null; }\n })();\n const knobRoot =\n cfg.knobRoot === undefined\n ? typeof document === 'undefined'\n ? null\n : document.documentElement\n : cfg.knobRoot;\n let knobValues: VariantSelections = (() => {\n const parse = (raw: string | null): VariantSelections => {\n try {\n const parsed = raw ? (JSON.parse(raw) as unknown) : null;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? Object.fromEntries(Object.entries(parsed).filter((entry) => typeof entry[1] === 'string')) : {};\n } catch { return {}; }\n };\n try {\n return storage ? parse(readBrowserStorage(storage, KNOBS_STORAGE_KEY, (current, previous) =>\n JSON.stringify({ ...parse(previous), ...parse(current) }))) : {};\n } catch {\n return {};\n }\n })();\n const persistKnobs = () => {\n try {\n if (!storage) return;\n if (Object.keys(knobValues).length === 0) removeBrowserStorage(storage, KNOBS_STORAGE_KEY);\n else writeBrowserStorage(storage, KNOBS_STORAGE_KEY, JSON.stringify(knobValues));\n } catch {\n // Storage full or blocked: the values still apply for this page view.\n }\n };\n const applyKnobs = () => {\n if (knobRoot) applyKnobValues(list?.knobs ?? [], knobValues, knobRoot);\n };\n async function drainActivations(): Promise<void> {\n while (intended !== null) {\n const active = intended;\n const sentRevision = revision;\n try {\n const res = await request(`${cfg.endpoint}/active`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ active }),\n });\n if (!res.ok) throw new Error(`Activating variants failed (${res.status}). Try again.`);\n if (list) list = { ...list, active };\n error = null;\n } catch (cause) {\n error = cause instanceof Error ? cause.message : 'Could not activate this variant. Try again.';\n }\n // Newer picks include the full intended set, including other groups.\n // Completing or failing an older request must not discard that intent.\n if (sentRevision === revision) intended = null;\n emit();\n }\n }\n function activate(active: string[]): void {\n intended = active;\n revision++;\n error = null;\n emit();\n scheduleActivation();\n }\n function scheduleActivation(): void {\n if (activation) return;\n activation = Promise.resolve().then(drainActivations).finally(() => {\n activation = null;\n if (intended !== null) scheduleActivation();\n });\n }\n return {\n title: 'Variants',\n getError: () => error,\n get list() {\n return list;\n },\n get knobs() {\n return { axes: knobAxes(), values: { ...knobValues } };\n },\n async refresh() {\n const refreshId = ++refreshRevision;\n const startedRevision = revision;\n const wasActivating = activation !== null;\n const next = await request(cfg.endpoint, undefined, async (res) => {\n if (!res.ok) throw new Error(`Loading variants failed (${res.status})`);\n return await res.json() as OverlayVariantsList;\n });\n if (refreshId !== refreshRevision) return;\n // An older GET must not roll back a successful POST that finished while\n // the listing was loading. Definitions can still refresh independently.\n const previousKnobs = list?.knobs ?? [];\n list = list && (wasActivating || activation !== null || startedRevision !== revision)\n ? { ...next, active: list.active } : next;\n if (knobRoot) applyKnobValues(previousKnobs, {}, knobRoot);\n const valid = new Map((list.knobs ?? []).map((knob) => [knob.id, knob]));\n knobValues = Object.fromEntries(Object.entries(knobValues).filter(([id, value]) => {\n const knob = valid.get(id);\n return knob && isValidSelection(knob, value);\n }));\n applyKnobs();\n emit();\n },\n getAxes: axes,\n getSelections() {\n const active = new Set(intended ?? list?.active ?? []);\n const picks = Object.fromEntries(\n selects().map((axis) => [\n axis.id,\n axis.options.find((o) => active.has(o.id))?.id ?? OVERLAY_DEFAULT_OPTION,\n ]),\n );\n for (const k of knobAxes()) picks[k.id] = knobValues[k.id] ?? axisDefault(k);\n return picks;\n },\n setSelection(axisId, value) {\n const knob = knobAxes().find((k) => k.id === axisId);\n if (knob) {\n if (!isValidSelection(knob, value)) return;\n if (value === axisDefault(knob)) delete knobValues[axisId];\n else knobValues = { ...knobValues, [axisId]: value };\n persistKnobs();\n applyKnobs();\n emit();\n return;\n }\n const axis = selects().find((a) => a.id === axisId);\n if (!axis || !isValidSelection(axis, value)) return;\n // The group's members leave the set and the pick joins it; the other\n // groups' picks ride along untouched.\n const members = new Set(axis.options.map((o) => o.id));\n const next = (intended ?? list?.active ?? []).filter((id) => !members.has(id));\n if (value !== OVERLAY_DEFAULT_OPTION) next.push(value);\n activate(next);\n },\n reset() {\n knobValues = {};\n persistKnobs();\n applyKnobs();\n activate([]);\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -3,7 +3,7 @@ import {
3
3
  readBrowserStorage,
4
4
  request,
5
5
  writeBrowserStorage
6
- } from "./chunk-SNGBNVH2.js";
6
+ } from "./chunk-OSETWKGE.js";
7
7
  import {
8
8
  MAX_FEEDBACK_LENGTH
9
9
  } from "./chunk-A4DX2V4C.js";
@@ -299,7 +299,7 @@ function createIterationStore(cfg = {}) {
299
299
  }
300
300
  if (!result || typeof result.id !== "string" || !result.id) return saveOffline();
301
301
  if (pendingBefore) acknowledge(pendingBefore);
302
- lastSave = localAll().length ? "local" : "server";
302
+ lastSave = "server";
303
303
  return result.id;
304
304
  }
305
305
  async function deleteNote(id) {
@@ -337,4 +337,4 @@ export {
337
337
  isRouteVisible,
338
338
  createIterationStore
339
339
  };
340
- //# sourceMappingURL=chunk-YWMOXPE7.js.map
340
+ //# sourceMappingURL=chunk-UYMMI2IH.js.map