@adea-ai/ui 0.147.8 → 0.147.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/layout/split-layout/model.d.ts +4 -9
- package/dist/components/layout/split-layout/model.d.ts.map +1 -1
- package/dist/components/layout/split-layout/model.js +9 -12
- package/dist/components/layout/split-layout/model.js.map +1 -1
- package/dist/components/theme/theme-persistence.js +4 -4
- package/dist/components/theme/theme-persistence.js.map +1 -1
- package/dist/r/src/components/layout/split-layout/model.ts +12 -12
- package/dist/r/src/components/theme/theme-persistence.ts +4 -4
- package/package.json +1 -1
- package/src/components/layout/split-layout/model.ts +12 -12
- package/src/components/theme/theme-persistence.ts +4 -4
|
@@ -18,15 +18,10 @@ export type BalancedSplitPaneInput<L extends SplitLayoutLeaf = SplitLayoutLeaf>
|
|
|
18
18
|
export declare function createLayoutState<L extends SplitLayoutLeaf>(center: SplitLayoutNode<L>): SplitLayoutState<L>;
|
|
19
19
|
export declare function splitPane<L extends SplitLayoutLeaf>(state: SplitLayoutState<L>, targetLeafId: string, input: SplitPaneInput<L>): SplitLayoutState<L>;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* equal height. Existing opaque leaf objects are preserved. Split IDs are
|
|
26
|
-
* recycled across rebuilt branches, whose axis and descendants can change;
|
|
27
|
-
* resize actions must resolve against the current tree. Ratios are recomputed.
|
|
28
|
-
* This is opt-in: callers
|
|
29
|
-
* that need an explicit axis and ratio should keep using `splitPane`.
|
|
21
|
+
* Reflow into a balanced row-major grid of at most two rows and four columns,
|
|
22
|
+
* preserving opaque leaves. Split IDs are recycled across rebuilt branches, so
|
|
23
|
+
* resize actions must resolve against the current tree. Call `splitPane` instead
|
|
24
|
+
* when the axis and ratio must be explicit.
|
|
30
25
|
*/
|
|
31
26
|
export declare function splitPaneBalanced<L extends SplitLayoutLeaf>(state: SplitLayoutState<L>, targetLeafId: string, input: BalancedSplitPaneInput<L>): SplitLayoutState<L>;
|
|
32
27
|
export declare function closePane<L extends SplitLayoutLeaf>(state: SplitLayoutState<L>, leafId: string, createPlaceholderLeaf: () => L): SplitLayoutState<L>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../../../src/components/layout/split-layout/model.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAA;AAEjF,cAAc,QAAQ,CAAA;AAEtB,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAAI,QAAQ,CAAC;IACnF,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAA;IAC1B,aAAa,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,SAAS,QAAQ,CAAC;QAAE,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,CAAA;CAC5E,CAAC,CAAA;AAEF,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAAI,QAAQ,CAAC;IACjF,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAA;IAC5C,SAAS,EAAE,QAAQ,GAAG,OAAO,CAAA;IAC7B,IAAI,EAAE,CAAC,CAAA;IACP,OAAO,EAAE,MAAM,CAAA;CAChB,CAAC,CAAA;AAEF,MAAM,MAAM,sBAAsB,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAAI,QAAQ,CACxF,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAAC,CAC1D,CAAA;
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../../../src/components/layout/split-layout/model.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,7 +23,8 @@ function removeLeaf(node, leafId) {
|
|
|
23
23
|
function containsLeaf(node, leafId) {
|
|
24
24
|
return node.kind === "leaf" ? node.id === leafId : containsLeaf(node.children[0], leafId) || containsLeaf(node.children[1], leafId);
|
|
25
25
|
}
|
|
26
|
-
function assertUniqueInput(state, leaf, splitId) {
|
|
26
|
+
function assertUniqueInput(state, leaf, splitId, placement) {
|
|
27
|
+
if (!leaf || leaf.kind !== "leaf" || placement !== "before" && placement !== "after") throw new Error("invalid_state: invalid split input");
|
|
27
28
|
assertLayoutTree(leaf);
|
|
28
29
|
if (typeof splitId !== "string" || splitId.length === 0) throw new Error("invalid_state: split identity must be nonempty");
|
|
29
30
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -76,8 +77,9 @@ function createLayoutState(center) {
|
|
|
76
77
|
}
|
|
77
78
|
function splitPane(state, targetLeafId, input) {
|
|
78
79
|
if (!containsLeaf(state.center, targetLeafId)) return state;
|
|
80
|
+
if (input.direction !== "row" && input.direction !== "column") throw new Error("invalid_state: invalid split input");
|
|
79
81
|
if (countLeaves(state.center) >= 8) throw new Error("limit_exceeded: center layout has eight leaves");
|
|
80
|
-
assertUniqueInput(state, input.leaf, input.splitId);
|
|
82
|
+
assertUniqueInput(state, input.leaf, input.splitId, input.placement);
|
|
81
83
|
const target = listLeaves(state.center).find((leaf) => leaf.id === targetLeafId);
|
|
82
84
|
if (!target) return state;
|
|
83
85
|
const children = input.placement === "before" ? [input.leaf, target] : [target, input.leaf];
|
|
@@ -97,22 +99,17 @@ function splitPane(state, targetLeafId, input) {
|
|
|
97
99
|
};
|
|
98
100
|
}
|
|
99
101
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* equal height. Existing opaque leaf objects are preserved. Split IDs are
|
|
105
|
-
* recycled across rebuilt branches, whose axis and descendants can change;
|
|
106
|
-
* resize actions must resolve against the current tree. Ratios are recomputed.
|
|
107
|
-
* This is opt-in: callers
|
|
108
|
-
* that need an explicit axis and ratio should keep using `splitPane`.
|
|
102
|
+
* Reflow into a balanced row-major grid of at most two rows and four columns,
|
|
103
|
+
* preserving opaque leaves. Split IDs are recycled across rebuilt branches, so
|
|
104
|
+
* resize actions must resolve against the current tree. Call `splitPane` instead
|
|
105
|
+
* when the axis and ratio must be explicit.
|
|
109
106
|
*/
|
|
110
107
|
function splitPaneBalanced(state, targetLeafId, input) {
|
|
111
108
|
const leaves = [...listLeaves(state.center)];
|
|
112
109
|
const targetIndex = leaves.findIndex((leaf) => leaf.id === targetLeafId);
|
|
113
110
|
if (targetIndex < 0) return state;
|
|
114
111
|
if (leaves.length >= 8) throw new Error("limit_exceeded: center layout has eight leaves");
|
|
115
|
-
const splitIds = assertUniqueInput(state, input.leaf, input.splitId);
|
|
112
|
+
const splitIds = assertUniqueInput(state, input.leaf, input.splitId, input.placement);
|
|
116
113
|
const insertionIndex = targetIndex + (input.placement === "before" ? 0 : 1);
|
|
117
114
|
leaves.splice(insertionIndex, 0, input.leaf);
|
|
118
115
|
const build = (start, end, vertical = false) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.js","names":[],"sources":["../../../../src/components/layout/split-layout/model.ts"],"sourcesContent":["/*\n * Copyright (c) 2026 Michael Yong\n * Copyright (c) 2026 Muxy\n * SPDX-License-Identifier: MIT\n *\n * Portions of the binary split behavior are substantially translated from:\n * - get-bb/bb apps/app/src/lib/split-layout/ops.ts (MIT), revision\n * 52a9256373d4d36f9b60e9e2a7f333464091a2ac.\n * - muxy-app/muxy Muxy/Models/Workspace/SplitNode.swift (MIT), revision\n * 5c5be8697c57a2fe70cda97fdbaf7c912e2e31b6.\n * Adapted first in Adea packages/dev-view/src/layout/operations.ts at\n * 0322ab09ff5e9775bfddc0bf2d81e1df436dbef7. Extracted here with opaque\n * host-owned leaves and an injected final-pane placeholder; immutable strict\n * binary operations, accepted limits and undoable close are preserved.\n * See NOTICE and docs/research/dev-view-donor-audit.md.\n */\n\nimport {\n MAX_LAYOUT_LEAVES,\n MAX_LAYOUT_DEPTH,\n MIN_SPLIT_RATIO,\n MAX_SPLIT_RATIO,\n listLeaves,\n countLeaves,\n layoutDepth,\n} from './tree'\nimport type { SplitLayoutLeaf, SplitLayoutBranch, SplitLayoutNode } from './tree'\n\nexport * from './tree'\n\nexport type SplitLayoutState<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readonly<{\n center: SplitLayoutNode<L>\n focusedLeafId: string\n closed: readonly Readonly<{ center: SplitLayoutNode<L>; leafId: string }>[]\n}>\n\nexport type SplitPaneInput<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readonly<{\n direction: SplitLayoutBranch<L>['direction']\n placement: 'before' | 'after'\n leaf: L\n splitId: string\n}>\n\nexport type BalancedSplitPaneInput<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readonly<\n Pick<SplitPaneInput<L>, 'placement' | 'leaf' | 'splitId'>\n>\n\nfunction replaceLeaf<L extends SplitLayoutLeaf>(\n node: SplitLayoutNode<L>,\n leafId: string,\n replacement: SplitLayoutNode<L>\n): SplitLayoutNode<L> {\n if (node.kind === 'leaf') return node.id === leafId ? replacement : node\n const first = replaceLeaf(node.children[0], leafId, replacement)\n const second = replaceLeaf(node.children[1], leafId, replacement)\n return first === node.children[0] && second === node.children[1]\n ? node\n : { ...node, children: [first, second] }\n}\n\nfunction removeLeaf<L extends SplitLayoutLeaf>(\n node: SplitLayoutNode<L>,\n leafId: string\n): SplitLayoutNode<L> | null {\n if (node.kind === 'leaf') return node.id === leafId ? null : node\n const first = removeLeaf(node.children[0], leafId)\n const second = removeLeaf(node.children[1], leafId)\n if (!first) return second\n if (!second) return first\n return first === node.children[0] && second === node.children[1]\n ? node\n : { ...node, children: [first, second] }\n}\n\nfunction containsLeaf<L extends SplitLayoutLeaf>(\n node: SplitLayoutNode<L>,\n leafId: string\n): boolean {\n return node.kind === 'leaf'\n ? node.id === leafId\n : containsLeaf(node.children[0], leafId) || containsLeaf(node.children[1], leafId)\n}\n\nfunction assertUniqueInput<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n leaf: L,\n splitId: string\n) {\n assertLayoutTree(leaf)\n if (typeof splitId !== 'string' || splitId.length === 0)\n throw new Error('invalid_state: split identity must be nonempty')\n const ids = new Set<string>()\n const splitIds: string[] = []\n const visit = (node: SplitLayoutNode<L>) => {\n if (ids.has(node.id)) throw new Error('invalid_state: duplicate pane id')\n ids.add(node.id)\n if (node.kind === 'split') {\n splitIds.push(node.id)\n node.children.forEach(visit)\n }\n }\n visit(state.center)\n if (ids.has(leaf.id) || ids.has(splitId) || leaf.id === splitId)\n throw new Error('invalid_state: duplicate pane id')\n return splitIds\n}\n\n/** Visual-tree validation only; applications still decode and scope persisted preferences. */\nfunction assertLayoutTree<L extends SplitLayoutLeaf>(center: SplitLayoutNode<L>): void {\n const ids = new Set<string>()\n const seen = new WeakSet<object>()\n let leaves = 0\n const visit = (node: SplitLayoutNode<L>, depth: number) => {\n if (depth > MAX_LAYOUT_DEPTH) throw new Error('limit_exceeded: layout depth exceeds eight')\n if (!node || typeof node !== 'object') throw new Error('invalid_state: missing layout node')\n if (seen.has(node)) throw new Error('invalid_state: cyclic or reused layout node')\n seen.add(node)\n if (typeof node.id !== 'string' || node.id.length === 0)\n throw new Error('invalid_state: layout identity must be nonempty')\n if (ids.has(node.id)) throw new Error('invalid_state: duplicate pane id')\n ids.add(node.id)\n if (node.kind === 'leaf') {\n leaves += 1\n if (leaves > MAX_LAYOUT_LEAVES)\n throw new Error('limit_exceeded: layout has more than eight leaves')\n return\n }\n if (\n node.kind !== 'split' ||\n !Array.isArray(node.children) ||\n node.children.length !== 2 ||\n (node.direction !== 'row' && node.direction !== 'column')\n )\n throw new Error('invalid_state: layout must be a strict binary row or column tree')\n visit(node.children[0], depth + 1)\n visit(node.children[1], depth + 1)\n }\n visit(center, 1)\n}\n\nexport function createLayoutState<L extends SplitLayoutLeaf>(\n center: SplitLayoutNode<L>\n): SplitLayoutState<L> {\n assertLayoutTree(center)\n const first = listLeaves(center)[0]\n if (!first) throw new Error('invalid_state: layout requires a leaf')\n return { center, focusedLeafId: first.id, closed: [] }\n}\n\nexport function splitPane<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n targetLeafId: string,\n input: SplitPaneInput<L>\n): SplitLayoutState<L> {\n if (!containsLeaf(state.center, targetLeafId)) return state\n if (countLeaves(state.center) >= MAX_LAYOUT_LEAVES)\n throw new Error('limit_exceeded: center layout has eight leaves')\n assertUniqueInput(state, input.leaf, input.splitId)\n const target = listLeaves(state.center).find((leaf) => leaf.id === targetLeafId)\n if (!target) return state\n const children =\n input.placement === 'before' ? ([input.leaf, target] as const) : ([target, input.leaf] as const)\n const center = replaceLeaf(state.center, targetLeafId, {\n kind: 'split',\n id: input.splitId,\n direction: input.direction,\n ratio: 0.5,\n children,\n })\n if (layoutDepth(center) > MAX_LAYOUT_DEPTH)\n throw new Error('limit_exceeded: center layout depth exceeds eight')\n return { ...state, center, focusedLeafId: input.leaf.id, closed: [] }\n}\n\n/**\n * Add a pane beside the selected leaf and reflow the visual tree into a\n * balanced row-major grid. One or two leaves share one row; from three leaves\n * onward, leaves fill a first row and then a second row, with no more than four\n * columns in either. Each row gives its leaves equal width and the two rows\n * equal height. Existing opaque leaf objects are preserved. Split IDs are\n * recycled across rebuilt branches, whose axis and descendants can change;\n * resize actions must resolve against the current tree. Ratios are recomputed.\n * This is opt-in: callers\n * that need an explicit axis and ratio should keep using `splitPane`.\n */\nexport function splitPaneBalanced<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n targetLeafId: string,\n input: BalancedSplitPaneInput<L>\n): SplitLayoutState<L> {\n const leaves = [...listLeaves(state.center)]\n const targetIndex = leaves.findIndex((leaf) => leaf.id === targetLeafId)\n if (targetIndex < 0) return state\n if (leaves.length >= MAX_LAYOUT_LEAVES)\n throw new Error('limit_exceeded: center layout has eight leaves')\n const splitIds = assertUniqueInput(state, input.leaf, input.splitId)\n\n const insertionIndex = targetIndex + (input.placement === 'before' ? 0 : 1)\n leaves.splice(insertionIndex, 0, input.leaf)\n\n const build = (start: number, end: number, vertical = false): SplitLayoutNode<L> => {\n if (end - start === 1) return leaves[start]!\n const middle = start + Math.ceil((end - start) / 2)\n // The new identity belongs to the lowest branch separating the selected\n // pane from its new neighbour. Other branches reuse the existing ID pool.\n const id = middle === targetIndex + 1 ? input.splitId : splitIds.shift()!\n return {\n kind: 'split',\n id,\n direction: vertical ? 'column' : 'row',\n ratio: vertical ? 0.5 : (middle - start) / (end - start),\n children: [build(start, middle), build(middle, end)],\n }\n }\n const center = build(0, leaves.length, leaves.length > 2)\n assertLayoutTree(center)\n return { ...state, center, focusedLeafId: input.leaf.id, closed: [] }\n}\n\nexport function closePane<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n leafId: string,\n createPlaceholderLeaf: () => L\n): SplitLayoutState<L> {\n if (!containsLeaf(state.center, leafId)) return state\n const before = state.center\n const readingOrder = listLeaves(before)\n const closedIndex = readingOrder.findIndex((leaf) => leaf.id === leafId)\n const removed = removeLeaf(before, leafId)\n const center: SplitLayoutNode<L> = removed ?? createPlaceholderLeaf()\n if (!removed) {\n if (center.kind !== 'leaf') throw new Error('invalid_state: placeholder must be a leaf')\n assertLayoutTree(center)\n }\n const leaves = listLeaves(center)\n const fallback = leaves[Math.min(closedIndex, leaves.length - 1)] ?? leaves[0]\n if (!fallback) throw new Error('invalid_state: layout requires a leaf')\n return {\n center,\n focusedLeafId: state.focusedLeafId === leafId ? fallback.id : state.focusedLeafId,\n closed: [...state.closed, { center: before, leafId }],\n }\n}\n\nexport function undoClosePane<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>\n): SplitLayoutState<L> {\n const previous = state.closed.at(-1)\n if (!previous) return state\n return {\n center: previous.center,\n focusedLeafId: previous.leafId,\n closed: state.closed.slice(0, -1),\n }\n}\n\nexport function focusPane<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n leafId: string\n): SplitLayoutState<L> {\n return containsLeaf(state.center, leafId) && state.focusedLeafId !== leafId\n ? { ...state, focusedLeafId: leafId }\n : state\n}\n\nexport function swapPanes<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n firstLeafId: string,\n secondLeafId: string\n): SplitLayoutState<L> {\n if (firstLeafId === secondLeafId) return state\n const leaves = listLeaves(state.center)\n const first = leaves.find((leaf) => leaf.id === firstLeafId)\n const second = leaves.find((leaf) => leaf.id === secondLeafId)\n if (!first || !second) return state\n const swap = (node: SplitLayoutNode<L>): SplitLayoutNode<L> => {\n if (node.kind === 'leaf') {\n if (node.id === firstLeafId) return second\n if (node.id === secondLeafId) return first\n return node\n }\n const left = swap(node.children[0])\n const right = swap(node.children[1])\n return left === node.children[0] && right === node.children[1]\n ? node\n : { ...node, children: [left, right] }\n }\n return { ...state, center: swap(state.center), closed: [] }\n}\n\nexport function movePane<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n leafId: string,\n targetLeafId: string,\n placement: 'before' | 'after',\n direction: SplitLayoutBranch<L>['direction'],\n splitId: string\n): SplitLayoutState<L> {\n if (leafId === targetLeafId) return state\n const moving = listLeaves(state.center).find((leaf) => leaf.id === leafId)\n if (!moving || !containsLeaf(state.center, targetLeafId)) return state\n const detached = removeLeaf(state.center, leafId)\n if (!detached) return state\n const moved = splitPane(\n { center: detached, focusedLeafId: state.focusedLeafId, closed: state.closed },\n targetLeafId,\n { direction, placement, leaf: moving, splitId }\n )\n return { ...moved, focusedLeafId: leafId }\n}\n\nexport function resizeSplit<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n splitId: string,\n ratio: number\n): SplitLayoutState<L> {\n if (!Number.isFinite(ratio)) return state\n const nextRatio = Math.min(MAX_SPLIT_RATIO, Math.max(MIN_SPLIT_RATIO, ratio))\n let found = false\n const visit = (node: SplitLayoutNode<L>): SplitLayoutNode<L> => {\n if (node.kind === 'leaf') return node\n if (node.id === splitId) {\n found = true\n return node.ratio === nextRatio ? node : { ...node, ratio: nextRatio }\n }\n const first = visit(node.children[0])\n const second = visit(node.children[1])\n return first === node.children[0] && second === node.children[1]\n ? node\n : { ...node, children: [first, second] }\n }\n const center = visit(state.center)\n return found && center !== state.center ? { ...state, center, closed: [] } : state\n}\n\n/**\n * Pure repair walk ported from bb's size normalization: every split ratio is\n * clamped into the normative range and a non-finite ratio falls back to an\n * even split. Structure, IDs, and focus are untouched.\n */\nexport function normalizeLayout<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>\n): SplitLayoutState<L> {\n const visit = (node: SplitLayoutNode<L>): SplitLayoutNode<L> => {\n if (node.kind === 'leaf') return node\n const first = visit(node.children[0])\n const second = visit(node.children[1])\n const ratio = Number.isFinite(node.ratio)\n ? Math.min(MAX_SPLIT_RATIO, Math.max(MIN_SPLIT_RATIO, node.ratio))\n : (MIN_SPLIT_RATIO + MAX_SPLIT_RATIO) / 2\n return first === node.children[0] && second === node.children[1] && ratio === node.ratio\n ? node\n : { ...node, ratio, children: [first, second] }\n }\n const center = visit(state.center)\n return center === state.center ? state : { ...state, center }\n}\n\n/** The leaf immediately after (`1`) or before (`-1`) the given leaf in reading order. */\nexport function neighborLeaf<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n leafId: string,\n step: 1 | -1\n): L | undefined {\n const leaves = listLeaves(state.center)\n const index = leaves.findIndex((leaf) => leaf.id === leafId)\n if (index < 0) return undefined\n return leaves[index + step]\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"model.js","names":[],"sources":["../../../../src/components/layout/split-layout/model.ts"],"sourcesContent":["/*\n * Copyright (c) 2026 Michael Yong\n * Copyright (c) 2026 Muxy\n * SPDX-License-Identifier: MIT\n *\n * Portions of the binary split behavior are substantially translated from:\n * - get-bb/bb apps/app/src/lib/split-layout/ops.ts (MIT), revision\n * 52a9256373d4d36f9b60e9e2a7f333464091a2ac.\n * - muxy-app/muxy Muxy/Models/Workspace/SplitNode.swift (MIT), revision\n * 5c5be8697c57a2fe70cda97fdbaf7c912e2e31b6.\n * Adapted first in Adea packages/dev-view/src/layout/operations.ts at\n * 0322ab09ff5e9775bfddc0bf2d81e1df436dbef7. Extracted here with opaque\n * host-owned leaves and an injected final-pane placeholder; immutable strict\n * binary operations, accepted limits and undoable close are preserved.\n * See NOTICE and docs/research/dev-view-donor-audit.md.\n */\n\nimport {\n MAX_LAYOUT_LEAVES,\n MAX_LAYOUT_DEPTH,\n MIN_SPLIT_RATIO,\n MAX_SPLIT_RATIO,\n listLeaves,\n countLeaves,\n layoutDepth,\n} from './tree'\nimport type { SplitLayoutLeaf, SplitLayoutBranch, SplitLayoutNode } from './tree'\n\nexport * from './tree'\n\nexport type SplitLayoutState<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readonly<{\n center: SplitLayoutNode<L>\n focusedLeafId: string\n closed: readonly Readonly<{ center: SplitLayoutNode<L>; leafId: string }>[]\n}>\n\nexport type SplitPaneInput<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readonly<{\n direction: SplitLayoutBranch<L>['direction']\n placement: 'before' | 'after'\n leaf: L\n splitId: string\n}>\n\nexport type BalancedSplitPaneInput<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readonly<\n Pick<SplitPaneInput<L>, 'placement' | 'leaf' | 'splitId'>\n>\n\nfunction replaceLeaf<L extends SplitLayoutLeaf>(\n node: SplitLayoutNode<L>,\n leafId: string,\n replacement: SplitLayoutNode<L>\n): SplitLayoutNode<L> {\n if (node.kind === 'leaf') return node.id === leafId ? replacement : node\n const first = replaceLeaf(node.children[0], leafId, replacement)\n const second = replaceLeaf(node.children[1], leafId, replacement)\n return first === node.children[0] && second === node.children[1]\n ? node\n : { ...node, children: [first, second] }\n}\n\nfunction removeLeaf<L extends SplitLayoutLeaf>(\n node: SplitLayoutNode<L>,\n leafId: string\n): SplitLayoutNode<L> | null {\n if (node.kind === 'leaf') return node.id === leafId ? null : node\n const first = removeLeaf(node.children[0], leafId)\n const second = removeLeaf(node.children[1], leafId)\n if (!first) return second\n if (!second) return first\n return first === node.children[0] && second === node.children[1]\n ? node\n : { ...node, children: [first, second] }\n}\n\nfunction containsLeaf<L extends SplitLayoutLeaf>(\n node: SplitLayoutNode<L>,\n leafId: string\n): boolean {\n return node.kind === 'leaf'\n ? node.id === leafId\n : containsLeaf(node.children[0], leafId) || containsLeaf(node.children[1], leafId)\n}\n\nfunction assertUniqueInput<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n leaf: L,\n splitId: string,\n placement: SplitPaneInput<L>['placement']\n) {\n if (!leaf || leaf.kind !== 'leaf' || (placement !== 'before' && placement !== 'after'))\n throw new Error('invalid_state: invalid split input')\n assertLayoutTree(leaf)\n if (typeof splitId !== 'string' || splitId.length === 0)\n throw new Error('invalid_state: split identity must be nonempty')\n const ids = new Set<string>()\n const splitIds: string[] = []\n const visit = (node: SplitLayoutNode<L>) => {\n if (ids.has(node.id)) throw new Error('invalid_state: duplicate pane id')\n ids.add(node.id)\n if (node.kind === 'split') {\n splitIds.push(node.id)\n node.children.forEach(visit)\n }\n }\n visit(state.center)\n if (ids.has(leaf.id) || ids.has(splitId) || leaf.id === splitId)\n throw new Error('invalid_state: duplicate pane id')\n return splitIds\n}\n\n/** Visual-tree validation only; applications still decode and scope persisted preferences. */\nfunction assertLayoutTree<L extends SplitLayoutLeaf>(center: SplitLayoutNode<L>): void {\n const ids = new Set<string>()\n const seen = new WeakSet<object>()\n let leaves = 0\n const visit = (node: SplitLayoutNode<L>, depth: number) => {\n if (depth > MAX_LAYOUT_DEPTH) throw new Error('limit_exceeded: layout depth exceeds eight')\n if (!node || typeof node !== 'object') throw new Error('invalid_state: missing layout node')\n if (seen.has(node)) throw new Error('invalid_state: cyclic or reused layout node')\n seen.add(node)\n if (typeof node.id !== 'string' || node.id.length === 0)\n throw new Error('invalid_state: layout identity must be nonempty')\n if (ids.has(node.id)) throw new Error('invalid_state: duplicate pane id')\n ids.add(node.id)\n if (node.kind === 'leaf') {\n leaves += 1\n if (leaves > MAX_LAYOUT_LEAVES)\n throw new Error('limit_exceeded: layout has more than eight leaves')\n return\n }\n if (\n node.kind !== 'split' ||\n !Array.isArray(node.children) ||\n node.children.length !== 2 ||\n (node.direction !== 'row' && node.direction !== 'column')\n )\n throw new Error('invalid_state: layout must be a strict binary row or column tree')\n visit(node.children[0], depth + 1)\n visit(node.children[1], depth + 1)\n }\n visit(center, 1)\n}\n\nexport function createLayoutState<L extends SplitLayoutLeaf>(\n center: SplitLayoutNode<L>\n): SplitLayoutState<L> {\n assertLayoutTree(center)\n const first = listLeaves(center)[0]\n if (!first) throw new Error('invalid_state: layout requires a leaf')\n return { center, focusedLeafId: first.id, closed: [] }\n}\n\nexport function splitPane<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n targetLeafId: string,\n input: SplitPaneInput<L>\n): SplitLayoutState<L> {\n if (!containsLeaf(state.center, targetLeafId)) return state\n if (input.direction !== 'row' && input.direction !== 'column')\n throw new Error('invalid_state: invalid split input')\n if (countLeaves(state.center) >= MAX_LAYOUT_LEAVES)\n throw new Error('limit_exceeded: center layout has eight leaves')\n assertUniqueInput(state, input.leaf, input.splitId, input.placement)\n const target = listLeaves(state.center).find((leaf) => leaf.id === targetLeafId)\n if (!target) return state\n const children =\n input.placement === 'before' ? ([input.leaf, target] as const) : ([target, input.leaf] as const)\n const center = replaceLeaf(state.center, targetLeafId, {\n kind: 'split',\n id: input.splitId,\n direction: input.direction,\n ratio: 0.5,\n children,\n })\n if (layoutDepth(center) > MAX_LAYOUT_DEPTH)\n throw new Error('limit_exceeded: center layout depth exceeds eight')\n return { ...state, center, focusedLeafId: input.leaf.id, closed: [] }\n}\n\n/**\n * Reflow into a balanced row-major grid of at most two rows and four columns,\n * preserving opaque leaves. Split IDs are recycled across rebuilt branches, so\n * resize actions must resolve against the current tree. Call `splitPane` instead\n * when the axis and ratio must be explicit.\n */\nexport function splitPaneBalanced<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n targetLeafId: string,\n input: BalancedSplitPaneInput<L>\n): SplitLayoutState<L> {\n const leaves = [...listLeaves(state.center)]\n const targetIndex = leaves.findIndex((leaf) => leaf.id === targetLeafId)\n if (targetIndex < 0) return state\n if (leaves.length >= MAX_LAYOUT_LEAVES)\n throw new Error('limit_exceeded: center layout has eight leaves')\n const splitIds = assertUniqueInput(state, input.leaf, input.splitId, input.placement)\n\n const insertionIndex = targetIndex + (input.placement === 'before' ? 0 : 1)\n leaves.splice(insertionIndex, 0, input.leaf)\n\n const build = (start: number, end: number, vertical = false): SplitLayoutNode<L> => {\n if (end - start === 1) return leaves[start]!\n const middle = start + Math.ceil((end - start) / 2)\n // The new identity belongs to the lowest branch separating the selected\n // pane from its new neighbour. Other branches reuse the existing ID pool.\n const id = middle === targetIndex + 1 ? input.splitId : splitIds.shift()!\n return {\n kind: 'split',\n id,\n direction: vertical ? 'column' : 'row',\n ratio: vertical ? 0.5 : (middle - start) / (end - start),\n children: [build(start, middle), build(middle, end)],\n }\n }\n const center = build(0, leaves.length, leaves.length > 2)\n assertLayoutTree(center)\n return { ...state, center, focusedLeafId: input.leaf.id, closed: [] }\n}\n\nexport function closePane<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n leafId: string,\n createPlaceholderLeaf: () => L\n): SplitLayoutState<L> {\n if (!containsLeaf(state.center, leafId)) return state\n const before = state.center\n const readingOrder = listLeaves(before)\n const closedIndex = readingOrder.findIndex((leaf) => leaf.id === leafId)\n const removed = removeLeaf(before, leafId)\n const center: SplitLayoutNode<L> = removed ?? createPlaceholderLeaf()\n if (!removed) {\n if (center.kind !== 'leaf') throw new Error('invalid_state: placeholder must be a leaf')\n assertLayoutTree(center)\n }\n const leaves = listLeaves(center)\n const fallback = leaves[Math.min(closedIndex, leaves.length - 1)] ?? leaves[0]\n if (!fallback) throw new Error('invalid_state: layout requires a leaf')\n return {\n center,\n focusedLeafId: state.focusedLeafId === leafId ? fallback.id : state.focusedLeafId,\n closed: [...state.closed, { center: before, leafId }],\n }\n}\n\nexport function undoClosePane<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>\n): SplitLayoutState<L> {\n const previous = state.closed.at(-1)\n if (!previous) return state\n return {\n center: previous.center,\n focusedLeafId: previous.leafId,\n closed: state.closed.slice(0, -1),\n }\n}\n\nexport function focusPane<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n leafId: string\n): SplitLayoutState<L> {\n return containsLeaf(state.center, leafId) && state.focusedLeafId !== leafId\n ? { ...state, focusedLeafId: leafId }\n : state\n}\n\nexport function swapPanes<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n firstLeafId: string,\n secondLeafId: string\n): SplitLayoutState<L> {\n if (firstLeafId === secondLeafId) return state\n const leaves = listLeaves(state.center)\n const first = leaves.find((leaf) => leaf.id === firstLeafId)\n const second = leaves.find((leaf) => leaf.id === secondLeafId)\n if (!first || !second) return state\n const swap = (node: SplitLayoutNode<L>): SplitLayoutNode<L> => {\n if (node.kind === 'leaf') {\n if (node.id === firstLeafId) return second\n if (node.id === secondLeafId) return first\n return node\n }\n const left = swap(node.children[0])\n const right = swap(node.children[1])\n return left === node.children[0] && right === node.children[1]\n ? node\n : { ...node, children: [left, right] }\n }\n return { ...state, center: swap(state.center), closed: [] }\n}\n\nexport function movePane<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n leafId: string,\n targetLeafId: string,\n placement: 'before' | 'after',\n direction: SplitLayoutBranch<L>['direction'],\n splitId: string\n): SplitLayoutState<L> {\n if (leafId === targetLeafId) return state\n const moving = listLeaves(state.center).find((leaf) => leaf.id === leafId)\n if (!moving || !containsLeaf(state.center, targetLeafId)) return state\n const detached = removeLeaf(state.center, leafId)\n if (!detached) return state\n const moved = splitPane(\n { center: detached, focusedLeafId: state.focusedLeafId, closed: state.closed },\n targetLeafId,\n { direction, placement, leaf: moving, splitId }\n )\n return { ...moved, focusedLeafId: leafId }\n}\n\nexport function resizeSplit<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n splitId: string,\n ratio: number\n): SplitLayoutState<L> {\n if (!Number.isFinite(ratio)) return state\n const nextRatio = Math.min(MAX_SPLIT_RATIO, Math.max(MIN_SPLIT_RATIO, ratio))\n let found = false\n const visit = (node: SplitLayoutNode<L>): SplitLayoutNode<L> => {\n if (node.kind === 'leaf') return node\n if (node.id === splitId) {\n found = true\n return node.ratio === nextRatio ? node : { ...node, ratio: nextRatio }\n }\n const first = visit(node.children[0])\n const second = visit(node.children[1])\n return first === node.children[0] && second === node.children[1]\n ? node\n : { ...node, children: [first, second] }\n }\n const center = visit(state.center)\n return found && center !== state.center ? { ...state, center, closed: [] } : state\n}\n\n/**\n * Pure repair walk ported from bb's size normalization: every split ratio is\n * clamped into the normative range and a non-finite ratio falls back to an\n * even split. Structure, IDs, and focus are untouched.\n */\nexport function normalizeLayout<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>\n): SplitLayoutState<L> {\n const visit = (node: SplitLayoutNode<L>): SplitLayoutNode<L> => {\n if (node.kind === 'leaf') return node\n const first = visit(node.children[0])\n const second = visit(node.children[1])\n const ratio = Number.isFinite(node.ratio)\n ? Math.min(MAX_SPLIT_RATIO, Math.max(MIN_SPLIT_RATIO, node.ratio))\n : (MIN_SPLIT_RATIO + MAX_SPLIT_RATIO) / 2\n return first === node.children[0] && second === node.children[1] && ratio === node.ratio\n ? node\n : { ...node, ratio, children: [first, second] }\n }\n const center = visit(state.center)\n return center === state.center ? state : { ...state, center }\n}\n\n/** The leaf immediately after (`1`) or before (`-1`) the given leaf in reading order. */\nexport function neighborLeaf<L extends SplitLayoutLeaf>(\n state: SplitLayoutState<L>,\n leafId: string,\n step: 1 | -1\n): L | undefined {\n const leaves = listLeaves(state.center)\n const index = leaves.findIndex((leaf) => leaf.id === leafId)\n if (index < 0) return undefined\n return leaves[index + step]\n}\n"],"mappings":"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"}
|
|
@@ -119,9 +119,9 @@ function createVersionedPersistence(schema, codec, options = {}) {
|
|
|
119
119
|
};
|
|
120
120
|
return {
|
|
121
121
|
read() {
|
|
122
|
-
const storage = storageOf();
|
|
123
|
-
if (!storage) return fallback();
|
|
124
122
|
try {
|
|
123
|
+
const storage = storageOf();
|
|
124
|
+
if (!storage) return fallback();
|
|
125
125
|
const found = readVersionedRecord(storage, recordSchema, options.now);
|
|
126
126
|
if (found.status === "record") {
|
|
127
127
|
previous = found.record;
|
|
@@ -136,9 +136,9 @@ function createVersionedPersistence(schema, codec, options = {}) {
|
|
|
136
136
|
return fallback();
|
|
137
137
|
},
|
|
138
138
|
write(next) {
|
|
139
|
-
const storage = storageOf();
|
|
140
|
-
if (!storage) return;
|
|
141
139
|
try {
|
|
140
|
+
const storage = storageOf();
|
|
141
|
+
if (!storage) return;
|
|
142
142
|
const document = codec.encode(next, previous);
|
|
143
143
|
if (writeVersionedRecord(storage, schema.key, document)) previous = typeof document === "object" && document !== null ? document : void 0;
|
|
144
144
|
} catch {}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme-persistence.js","names":[],"sources":["../../../src/components/theme/theme-persistence.ts"],"sourcesContent":["import { normalizeCustomAccent } from '#lib/accent-roles'\nimport { accentPresets } from '#lib/accent-presets'\nimport { normalizeAppearanceEditorFontSettings } from '#lib/appearance-font-settings'\nimport { isThemeAccentId } from '#lib/theme-accents'\nimport {\n catalogueThemeAppearance,\n defaultDarkThemeId,\n defaultLightThemeId,\n} from '#lib/theme-defaults'\nimport { normalizeSurface } from '#lib/theme-surface'\nimport {\n defaultPreferenceStorage,\n readLegacyValue,\n readVersionedRecord,\n recoveryKeyFor,\n writeVersionedRecord,\n type PreferenceStorage,\n} from '#lib/preference-store'\nimport { defaultThemeSelection, resolvedDensity, type ThemeSelection } from './theme-state-core'\n\n/**\n * Where a selection is kept between sessions.\n *\n * The provider never touches storage itself; it asks a `ThemePersistence`. The\n * default is {@link localStoragePersistence}, which is what the provider always did.\n * A host that stores the preference in a different shape, under a versioned\n * envelope, or somewhere that is not `localStorage` supplies its own and the\n * resolution and application rules stay the shared ones.\n *\n * The contract is deliberately narrow, so that an implementation cannot make the\n * provider less safe than it was:\n *\n * - `read` is synchronous and runs once, when the provider is created. It may\n * repair or quarantine what it finds, and it must not throw: the provider\n * treats a throw as \"nothing stored\" and starts from the defaults.\n * - `write` runs only from `setSelection`. Never on mount, never from `preview`,\n * never as a side effect of reading, so the first write to a store is always a\n * user's decision. A throw is swallowed, because a full or blocked store must\n * not break the switch.\n */\nexport type ThemePersistence = {\n read(): Partial<ThemeSelection> | undefined\n write(next: ThemeSelection): void\n}\n\n/**\n * The preference store, or nothing.\n *\n * Only ever called inside a `try`: where storage is blocked — a sandboxed iframe, a\n * privacy mode, a denied site-data permission — merely *reading* the `localStorage`\n * global throws (\"Access is denied\", `SecurityError`), so even the `typeof` guard\n * has to sit inside the `try` rather than in front of it. Without a store the\n * preference lives only in the provider's signal for the session.\n */\nfunction preferenceStore(): Storage | undefined {\n return typeof localStorage === 'undefined' ? undefined : localStorage\n}\n\n/**\n * The stored preference, validated against the catalogue's ids (`theme-defaults`), so\n * reading it never loads the catalogue.\n */\nfunction readSelection(storageKey: string): ThemeSelection {\n try {\n const raw = preferenceStore()?.getItem(storageKey)\n if (!raw) return defaultThemeSelection\n const parsed = JSON.parse(raw) as Partial<ThemeSelection>\n const fonts = normalizeAppearanceEditorFontSettings(parsed.fonts, parsed.font).settings\n return {\n appearance:\n parsed.appearance === 'light' ||\n parsed.appearance === 'dark' ||\n parsed.appearance === 'system'\n ? parsed.appearance\n : defaultThemeSelection.appearance,\n // An unknown id would leave the document with no theme at all, so a stale\n // preference from an older catalogue falls back rather than applying nothing.\n lightThemeId:\n catalogueThemeAppearance(parsed.lightThemeId ?? '') === 'light'\n ? (parsed.lightThemeId as string)\n : defaultLightThemeId,\n darkThemeId:\n catalogueThemeAppearance(parsed.darkThemeId ?? '') === 'dark'\n ? (parsed.darkThemeId as string)\n : defaultDarkThemeId,\n // A theme accent is kept whenever it names a slot, even one the stored pair\n // cannot offer: the id is role-shaped so it survives a theme switch, and an\n // unoffered slot already resolves to the theme's own primary when applied. A\n // custom `#rrggbb` is kept in its one stored spelling; its readability is\n // settled when it is painted, not when it is read.\n accent:\n typeof parsed.accent === 'string' &&\n (accentPresets.some((preset) => preset.id === parsed.accent) ||\n isThemeAccentId(parsed.accent))\n ? parsed.accent\n : (normalizeCustomAccent(parsed.accent) ?? defaultThemeSelection.accent),\n font: fonts.ui.family,\n fonts,\n // Every axis has to be read back here, or it is lost twice: the provider's\n // document effect removes the `data-density` that `themeScript` set for the first paint, and\n // the next `setSelection` rewrites the store without it.\n density: resolvedDensity(parsed as ThemeSelection),\n // The newer axes are read back only when the store holds a usable value and are\n // otherwise left `undefined`, never defaulted: `JSON.stringify` drops them, so a\n // consumer that never sets one writes the bytes it always did. An id is kept as\n // stored, known or not, for the same reason a theme id the registry lacks is.\n terminalThemeId:\n typeof parsed.terminalThemeId === 'string' && parsed.terminalThemeId.trim() !== ''\n ? parsed.terminalThemeId\n : undefined,\n surface: normalizeSurface(parsed.surface),\n reduceTransparency:\n typeof parsed.reduceTransparency === 'boolean' ? parsed.reduceTransparency : undefined,\n }\n } catch {\n return defaultThemeSelection\n }\n}\n\n/**\n * The default persistence: one JSON document in `localStorage` under `key`.\n *\n * `read` is the provider's original `readSelection`, moved here unchanged — every\n * stored id is validated against the catalogue, so a stale preference falls back to\n * a default rather than reaching the document. `key` may be a function so a\n * provider whose `storageKey` prop changes writes to the key it is now given.\n *\n * Blocked, full or absent storage is survivable in both directions: a read answers\n * with the defaults and a write is dropped, so the preference simply lasts the\n * session.\n */\nexport function localStoragePersistence(key: string | (() => string)): ThemePersistence {\n const keyOf = () => (typeof key === 'function' ? key() : key)\n return {\n read: () => readSelection(keyOf()),\n write(next) {\n try {\n preferenceStore()?.setItem(keyOf(), JSON.stringify(next))\n } catch {\n // A full, blocked or unavailable store must not break the switch: the\n // preference simply does not survive the reload.\n }\n },\n }\n}\n\n/**\n * What a persistence knows, or nothing.\n *\n * The seam's `read` must not throw, and the provider does not rely on that: a host\n * persistence is code the design system has not seen, and a theme provider that\n * fails to mount because a store misbehaved has turned a lost preference into a\n * blank application.\n */\nexport function readPersisted(persistence: ThemePersistence): Partial<ThemeSelection> | undefined {\n try {\n return persistence.read()\n } catch {\n return undefined\n }\n}\n\n/** Hand a selection to the store; a store that throws loses the preference, not the switch. */\nexport function writePersisted(persistence: ThemePersistence, next: ThemeSelection): void {\n try {\n persistence.write(next)\n } catch {\n // Same contract as a full or blocked `localStorage`: the choice holds for the\n // session and does not survive the reload.\n }\n}\n\n/**\n * Where a host's versioned preference document lives. One schema describes the\n * document to both halves of the system: {@link createVersionedPersistence} reads and\n * writes it, and `themeScript(options)` reads it before first paint, so the two\n * cannot disagree about a key or a version.\n */\nexport type ThemeStorageSchema = {\n /** The storage key of the document. */\n key: string\n /**\n * The only top-level `version` read. Absent, the document is unversioned. A record\n * of any other version is quarantined (see `recoveryKey`), not interpreted.\n */\n version?: number\n /**\n * The document field that holds the appearance preference (`system`, `light`,\n * `dark`). Read by `themeScript`; the persistence leaves field names to the codec.\n * Default `appearance`.\n */\n appearanceField?: 'appearance' | 'mode'\n /**\n * An older single-value key (`light` | `dark` | `system`) read only while `key` is\n * absent, and never deleted. The persistence hands it to the codec's `fromLegacy`.\n */\n legacyAppearanceKey?: string\n /**\n * The stored library of imported themes, for `themeScript` only: an imported\n * theme's id and verbatim tokens, so the first paint can use the right variant. The\n * persistence neither reads nor writes it.\n */\n libraryKey?: string\n /** Where an unreadable document is kept. Default `${key}.recovery`. */\n recoveryKey?: string\n}\n\n/**\n * How a host's document maps to and from the shared {@link ThemeSelection}.\n *\n * The codec is the product schema: it decides which fields a document has, how an\n * unknown value is corrected, and what a fresh install looks like. The engine decides\n * everything about *storage*: what is quarantined, when the legacy key is read, and\n * that nothing is written until `setSelection` asks.\n */\nexport type VersionedCodec<Doc> = {\n /**\n * A readable record (the right JSON shape and version) as a selection patch. Fields\n * it leaves `undefined` fall back to the provider's defaults. It may keep an id the\n * catalogue lacks; the provider resolves it, it does not rewrite it.\n */\n decode(record: Readonly<Record<string, unknown>>): Partial<ThemeSelection>\n /**\n * The legacy key's value (never `null`) as a selection patch, or `undefined` for a\n * value it does not recognise. Called only when the main key is absent.\n */\n fromLegacy?(value: string): Partial<ThemeSelection> | undefined\n /**\n * What a store with nothing readable answers: a fresh install, an unreadable\n * document, a blocked store. Absent, the provider's own defaults apply.\n */\n defaults?(): Partial<ThemeSelection> | undefined\n /**\n * The document to store for a selection. `previous` is the record last read or\n * written, so a codec can carry a field the selection does not model.\n */\n encode(next: ThemeSelection, previous?: Readonly<Record<string, unknown>>): Doc\n}\n\n/** Seams for the storage and the clock; both default to the real ones. */\nexport type VersionedPersistenceOptions = {\n /** The store, or nothing. Called per access; default is `localStorage` (blocked-safe). */\n storage?: () => PreferenceStorage | undefined\n /** The clock for the recovery envelope's `capturedAt`. */\n now?: () => Date\n}\n\n/**\n * A {@link ThemePersistence} over a versioned document and a host codec.\n *\n * `read` follows the engine's rules (`lib/preference-store.ts`): the main key first;\n * an unreadable or unsupported document is quarantined byte for byte into the\n * recovery key and the codec's `defaults` answer; the legacy key only when the main\n * key is absent. It never writes the main key, so the first write is a user's\n * decision. `write` stores `codec.encode(next, previous)` as JSON and swallows a\n * failure.\n *\n * Neither method throws. The stored bytes are exactly what the codec encodes, so a\n * host keeps its keys, field names and bytes by keeping its codec.\n */\nexport function createVersionedPersistence<Doc>(\n schema: ThemeStorageSchema,\n codec: VersionedCodec<Doc>,\n options: VersionedPersistenceOptions = {}\n): ThemePersistence {\n const storageOf = options.storage ?? defaultPreferenceStorage\n const recordSchema = {\n key: schema.key,\n version: schema.version,\n recoveryKey: recoveryKeyFor(schema),\n }\n let previous: Readonly<Record<string, unknown>> | undefined\n\n const fallback = () => {\n try {\n return codec.defaults?.()\n } catch {\n return undefined\n }\n }\n\n return {\n read() {\n const storage = storageOf()\n if (!storage) return fallback()\n try {\n const found = readVersionedRecord(storage, recordSchema, options.now)\n if (found.status === 'record') {\n previous = found.record\n return codec.decode(found.record)\n }\n if (found.status === 'absent' && schema.legacyAppearanceKey && codec.fromLegacy) {\n const legacy = readLegacyValue(storage, schema.legacyAppearanceKey)\n const migrated = legacy === null ? undefined : codec.fromLegacy(legacy)\n if (migrated) return migrated\n }\n } catch {\n // A codec that throws on a record it cannot read is an unreadable store.\n }\n return fallback()\n },\n write(next) {\n const storage = storageOf()\n if (!storage) return\n try {\n const document = codec.encode(next, previous)\n if (writeVersionedRecord(storage, schema.key, document)) {\n previous =\n typeof document === 'object' && document !== null\n ? (document as Readonly<Record<string, unknown>>)\n : undefined\n }\n } catch {\n // An encoder that throws loses the preference, not the switch.\n }\n },\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"theme-persistence.js","names":[],"sources":["../../../src/components/theme/theme-persistence.ts"],"sourcesContent":["import { normalizeCustomAccent } from '#lib/accent-roles'\nimport { accentPresets } from '#lib/accent-presets'\nimport { normalizeAppearanceEditorFontSettings } from '#lib/appearance-font-settings'\nimport { isThemeAccentId } from '#lib/theme-accents'\nimport {\n catalogueThemeAppearance,\n defaultDarkThemeId,\n defaultLightThemeId,\n} from '#lib/theme-defaults'\nimport { normalizeSurface } from '#lib/theme-surface'\nimport {\n defaultPreferenceStorage,\n readLegacyValue,\n readVersionedRecord,\n recoveryKeyFor,\n writeVersionedRecord,\n type PreferenceStorage,\n} from '#lib/preference-store'\nimport { defaultThemeSelection, resolvedDensity, type ThemeSelection } from './theme-state-core'\n\n/**\n * Where a selection is kept between sessions.\n *\n * The provider never touches storage itself; it asks a `ThemePersistence`. The\n * default is {@link localStoragePersistence}, which is what the provider always did.\n * A host that stores the preference in a different shape, under a versioned\n * envelope, or somewhere that is not `localStorage` supplies its own and the\n * resolution and application rules stay the shared ones.\n *\n * The contract is deliberately narrow, so that an implementation cannot make the\n * provider less safe than it was:\n *\n * - `read` is synchronous and runs once, when the provider is created. It may\n * repair or quarantine what it finds, and it must not throw: the provider\n * treats a throw as \"nothing stored\" and starts from the defaults.\n * - `write` runs only from `setSelection`. Never on mount, never from `preview`,\n * never as a side effect of reading, so the first write to a store is always a\n * user's decision. A throw is swallowed, because a full or blocked store must\n * not break the switch.\n */\nexport type ThemePersistence = {\n read(): Partial<ThemeSelection> | undefined\n write(next: ThemeSelection): void\n}\n\n/**\n * The preference store, or nothing.\n *\n * Only ever called inside a `try`: where storage is blocked — a sandboxed iframe, a\n * privacy mode, a denied site-data permission — merely *reading* the `localStorage`\n * global throws (\"Access is denied\", `SecurityError`), so even the `typeof` guard\n * has to sit inside the `try` rather than in front of it. Without a store the\n * preference lives only in the provider's signal for the session.\n */\nfunction preferenceStore(): Storage | undefined {\n return typeof localStorage === 'undefined' ? undefined : localStorage\n}\n\n/**\n * The stored preference, validated against the catalogue's ids (`theme-defaults`), so\n * reading it never loads the catalogue.\n */\nfunction readSelection(storageKey: string): ThemeSelection {\n try {\n const raw = preferenceStore()?.getItem(storageKey)\n if (!raw) return defaultThemeSelection\n const parsed = JSON.parse(raw) as Partial<ThemeSelection>\n const fonts = normalizeAppearanceEditorFontSettings(parsed.fonts, parsed.font).settings\n return {\n appearance:\n parsed.appearance === 'light' ||\n parsed.appearance === 'dark' ||\n parsed.appearance === 'system'\n ? parsed.appearance\n : defaultThemeSelection.appearance,\n // An unknown id would leave the document with no theme at all, so a stale\n // preference from an older catalogue falls back rather than applying nothing.\n lightThemeId:\n catalogueThemeAppearance(parsed.lightThemeId ?? '') === 'light'\n ? (parsed.lightThemeId as string)\n : defaultLightThemeId,\n darkThemeId:\n catalogueThemeAppearance(parsed.darkThemeId ?? '') === 'dark'\n ? (parsed.darkThemeId as string)\n : defaultDarkThemeId,\n // A theme accent is kept whenever it names a slot, even one the stored pair\n // cannot offer: the id is role-shaped so it survives a theme switch, and an\n // unoffered slot already resolves to the theme's own primary when applied. A\n // custom `#rrggbb` is kept in its one stored spelling; its readability is\n // settled when it is painted, not when it is read.\n accent:\n typeof parsed.accent === 'string' &&\n (accentPresets.some((preset) => preset.id === parsed.accent) ||\n isThemeAccentId(parsed.accent))\n ? parsed.accent\n : (normalizeCustomAccent(parsed.accent) ?? defaultThemeSelection.accent),\n font: fonts.ui.family,\n fonts,\n // Every axis has to be read back here, or it is lost twice: the provider's\n // document effect removes the `data-density` that `themeScript` set for the first paint, and\n // the next `setSelection` rewrites the store without it.\n density: resolvedDensity(parsed as ThemeSelection),\n // The newer axes are read back only when the store holds a usable value and are\n // otherwise left `undefined`, never defaulted: `JSON.stringify` drops them, so a\n // consumer that never sets one writes the bytes it always did. An id is kept as\n // stored, known or not, for the same reason a theme id the registry lacks is.\n terminalThemeId:\n typeof parsed.terminalThemeId === 'string' && parsed.terminalThemeId.trim() !== ''\n ? parsed.terminalThemeId\n : undefined,\n surface: normalizeSurface(parsed.surface),\n reduceTransparency:\n typeof parsed.reduceTransparency === 'boolean' ? parsed.reduceTransparency : undefined,\n }\n } catch {\n return defaultThemeSelection\n }\n}\n\n/**\n * The default persistence: one JSON document in `localStorage` under `key`.\n *\n * `read` is the provider's original `readSelection`, moved here unchanged — every\n * stored id is validated against the catalogue, so a stale preference falls back to\n * a default rather than reaching the document. `key` may be a function so a\n * provider whose `storageKey` prop changes writes to the key it is now given.\n *\n * Blocked, full or absent storage is survivable in both directions: a read answers\n * with the defaults and a write is dropped, so the preference simply lasts the\n * session.\n */\nexport function localStoragePersistence(key: string | (() => string)): ThemePersistence {\n const keyOf = () => (typeof key === 'function' ? key() : key)\n return {\n read: () => readSelection(keyOf()),\n write(next) {\n try {\n preferenceStore()?.setItem(keyOf(), JSON.stringify(next))\n } catch {\n // A full, blocked or unavailable store must not break the switch: the\n // preference simply does not survive the reload.\n }\n },\n }\n}\n\n/**\n * What a persistence knows, or nothing.\n *\n * The seam's `read` must not throw, and the provider does not rely on that: a host\n * persistence is code the design system has not seen, and a theme provider that\n * fails to mount because a store misbehaved has turned a lost preference into a\n * blank application.\n */\nexport function readPersisted(persistence: ThemePersistence): Partial<ThemeSelection> | undefined {\n try {\n return persistence.read()\n } catch {\n return undefined\n }\n}\n\n/** Hand a selection to the store; a store that throws loses the preference, not the switch. */\nexport function writePersisted(persistence: ThemePersistence, next: ThemeSelection): void {\n try {\n persistence.write(next)\n } catch {\n // Same contract as a full or blocked `localStorage`: the choice holds for the\n // session and does not survive the reload.\n }\n}\n\n/**\n * Where a host's versioned preference document lives. One schema describes the\n * document to both halves of the system: {@link createVersionedPersistence} reads and\n * writes it, and `themeScript(options)` reads it before first paint, so the two\n * cannot disagree about a key or a version.\n */\nexport type ThemeStorageSchema = {\n /** The storage key of the document. */\n key: string\n /**\n * The only top-level `version` read. Absent, the document is unversioned. A record\n * of any other version is quarantined (see `recoveryKey`), not interpreted.\n */\n version?: number\n /**\n * The document field that holds the appearance preference (`system`, `light`,\n * `dark`). Read by `themeScript`; the persistence leaves field names to the codec.\n * Default `appearance`.\n */\n appearanceField?: 'appearance' | 'mode'\n /**\n * An older single-value key (`light` | `dark` | `system`) read only while `key` is\n * absent, and never deleted. The persistence hands it to the codec's `fromLegacy`.\n */\n legacyAppearanceKey?: string\n /**\n * The stored library of imported themes, for `themeScript` only: an imported\n * theme's id and verbatim tokens, so the first paint can use the right variant. The\n * persistence neither reads nor writes it.\n */\n libraryKey?: string\n /** Where an unreadable document is kept. Default `${key}.recovery`. */\n recoveryKey?: string\n}\n\n/**\n * How a host's document maps to and from the shared {@link ThemeSelection}.\n *\n * The codec is the product schema: it decides which fields a document has, how an\n * unknown value is corrected, and what a fresh install looks like. The engine decides\n * everything about *storage*: what is quarantined, when the legacy key is read, and\n * that nothing is written until `setSelection` asks.\n */\nexport type VersionedCodec<Doc> = {\n /**\n * A readable record (the right JSON shape and version) as a selection patch. Fields\n * it leaves `undefined` fall back to the provider's defaults. It may keep an id the\n * catalogue lacks; the provider resolves it, it does not rewrite it.\n */\n decode(record: Readonly<Record<string, unknown>>): Partial<ThemeSelection>\n /**\n * The legacy key's value (never `null`) as a selection patch, or `undefined` for a\n * value it does not recognise. Called only when the main key is absent.\n */\n fromLegacy?(value: string): Partial<ThemeSelection> | undefined\n /**\n * What a store with nothing readable answers: a fresh install, an unreadable\n * document, a blocked store. Absent, the provider's own defaults apply.\n */\n defaults?(): Partial<ThemeSelection> | undefined\n /**\n * The document to store for a selection. `previous` is the record last read or\n * written, so a codec can carry a field the selection does not model.\n */\n encode(next: ThemeSelection, previous?: Readonly<Record<string, unknown>>): Doc\n}\n\n/** Seams for the storage and the clock; both default to the real ones. */\nexport type VersionedPersistenceOptions = {\n /** The store, or nothing. Called per access; default is `localStorage` (blocked-safe). */\n storage?: () => PreferenceStorage | undefined\n /** The clock for the recovery envelope's `capturedAt`. */\n now?: () => Date\n}\n\n/**\n * A {@link ThemePersistence} over a versioned document and a host codec.\n *\n * `read` follows the engine's rules (`lib/preference-store.ts`): the main key first;\n * an unreadable or unsupported document is quarantined byte for byte into the\n * recovery key and the codec's `defaults` answer; the legacy key only when the main\n * key is absent. It never writes the main key, so the first write is a user's\n * decision. `write` stores `codec.encode(next, previous)` as JSON and swallows a\n * failure.\n *\n * Neither method throws. The stored bytes are exactly what the codec encodes, so a\n * host keeps its keys, field names and bytes by keeping its codec.\n */\nexport function createVersionedPersistence<Doc>(\n schema: ThemeStorageSchema,\n codec: VersionedCodec<Doc>,\n options: VersionedPersistenceOptions = {}\n): ThemePersistence {\n const storageOf = options.storage ?? defaultPreferenceStorage\n const recordSchema = {\n key: schema.key,\n version: schema.version,\n recoveryKey: recoveryKeyFor(schema),\n }\n let previous: Readonly<Record<string, unknown>> | undefined\n\n const fallback = () => {\n try {\n return codec.defaults?.()\n } catch {\n return undefined\n }\n }\n\n return {\n read() {\n try {\n const storage = storageOf()\n if (!storage) return fallback()\n const found = readVersionedRecord(storage, recordSchema, options.now)\n if (found.status === 'record') {\n previous = found.record\n return codec.decode(found.record)\n }\n if (found.status === 'absent' && schema.legacyAppearanceKey && codec.fromLegacy) {\n const legacy = readLegacyValue(storage, schema.legacyAppearanceKey)\n const migrated = legacy === null ? undefined : codec.fromLegacy(legacy)\n if (migrated) return migrated\n }\n } catch {\n // A codec that throws on a record it cannot read is an unreadable store.\n }\n return fallback()\n },\n write(next) {\n try {\n const storage = storageOf()\n if (!storage) return\n const document = codec.encode(next, previous)\n if (writeVersionedRecord(storage, schema.key, document)) {\n previous =\n typeof document === 'object' && document !== null\n ? (document as Readonly<Record<string, unknown>>)\n : undefined\n }\n } catch {\n // An encoder that throws loses the preference, not the switch.\n }\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -84,8 +84,11 @@ function containsLeaf<L extends SplitLayoutLeaf>(
|
|
|
84
84
|
function assertUniqueInput<L extends SplitLayoutLeaf>(
|
|
85
85
|
state: SplitLayoutState<L>,
|
|
86
86
|
leaf: L,
|
|
87
|
-
splitId: string
|
|
87
|
+
splitId: string,
|
|
88
|
+
placement: SplitPaneInput<L>['placement']
|
|
88
89
|
) {
|
|
90
|
+
if (!leaf || leaf.kind !== 'leaf' || (placement !== 'before' && placement !== 'after'))
|
|
91
|
+
throw new Error('invalid_state: invalid split input')
|
|
89
92
|
assertLayoutTree(leaf)
|
|
90
93
|
if (typeof splitId !== 'string' || splitId.length === 0)
|
|
91
94
|
throw new Error('invalid_state: split identity must be nonempty')
|
|
@@ -153,9 +156,11 @@ export function splitPane<L extends SplitLayoutLeaf>(
|
|
|
153
156
|
input: SplitPaneInput<L>
|
|
154
157
|
): SplitLayoutState<L> {
|
|
155
158
|
if (!containsLeaf(state.center, targetLeafId)) return state
|
|
159
|
+
if (input.direction !== 'row' && input.direction !== 'column')
|
|
160
|
+
throw new Error('invalid_state: invalid split input')
|
|
156
161
|
if (countLeaves(state.center) >= MAX_LAYOUT_LEAVES)
|
|
157
162
|
throw new Error('limit_exceeded: center layout has eight leaves')
|
|
158
|
-
assertUniqueInput(state, input.leaf, input.splitId)
|
|
163
|
+
assertUniqueInput(state, input.leaf, input.splitId, input.placement)
|
|
159
164
|
const target = listLeaves(state.center).find((leaf) => leaf.id === targetLeafId)
|
|
160
165
|
if (!target) return state
|
|
161
166
|
const children =
|
|
@@ -173,15 +178,10 @@ export function splitPane<L extends SplitLayoutLeaf>(
|
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* equal height. Existing opaque leaf objects are preserved. Split IDs are
|
|
181
|
-
* recycled across rebuilt branches, whose axis and descendants can change;
|
|
182
|
-
* resize actions must resolve against the current tree. Ratios are recomputed.
|
|
183
|
-
* This is opt-in: callers
|
|
184
|
-
* that need an explicit axis and ratio should keep using `splitPane`.
|
|
181
|
+
* Reflow into a balanced row-major grid of at most two rows and four columns,
|
|
182
|
+
* preserving opaque leaves. Split IDs are recycled across rebuilt branches, so
|
|
183
|
+
* resize actions must resolve against the current tree. Call `splitPane` instead
|
|
184
|
+
* when the axis and ratio must be explicit.
|
|
185
185
|
*/
|
|
186
186
|
export function splitPaneBalanced<L extends SplitLayoutLeaf>(
|
|
187
187
|
state: SplitLayoutState<L>,
|
|
@@ -193,7 +193,7 @@ export function splitPaneBalanced<L extends SplitLayoutLeaf>(
|
|
|
193
193
|
if (targetIndex < 0) return state
|
|
194
194
|
if (leaves.length >= MAX_LAYOUT_LEAVES)
|
|
195
195
|
throw new Error('limit_exceeded: center layout has eight leaves')
|
|
196
|
-
const splitIds = assertUniqueInput(state, input.leaf, input.splitId)
|
|
196
|
+
const splitIds = assertUniqueInput(state, input.leaf, input.splitId, input.placement)
|
|
197
197
|
|
|
198
198
|
const insertionIndex = targetIndex + (input.placement === 'before' ? 0 : 1)
|
|
199
199
|
leaves.splice(insertionIndex, 0, input.leaf)
|
|
@@ -281,9 +281,9 @@ export function createVersionedPersistence<Doc>(
|
|
|
281
281
|
|
|
282
282
|
return {
|
|
283
283
|
read() {
|
|
284
|
-
const storage = storageOf()
|
|
285
|
-
if (!storage) return fallback()
|
|
286
284
|
try {
|
|
285
|
+
const storage = storageOf()
|
|
286
|
+
if (!storage) return fallback()
|
|
287
287
|
const found = readVersionedRecord(storage, recordSchema, options.now)
|
|
288
288
|
if (found.status === 'record') {
|
|
289
289
|
previous = found.record
|
|
@@ -300,9 +300,9 @@ export function createVersionedPersistence<Doc>(
|
|
|
300
300
|
return fallback()
|
|
301
301
|
},
|
|
302
302
|
write(next) {
|
|
303
|
-
const storage = storageOf()
|
|
304
|
-
if (!storage) return
|
|
305
303
|
try {
|
|
304
|
+
const storage = storageOf()
|
|
305
|
+
if (!storage) return
|
|
306
306
|
const document = codec.encode(next, previous)
|
|
307
307
|
if (writeVersionedRecord(storage, schema.key, document)) {
|
|
308
308
|
previous =
|
package/package.json
CHANGED
|
@@ -84,8 +84,11 @@ function containsLeaf<L extends SplitLayoutLeaf>(
|
|
|
84
84
|
function assertUniqueInput<L extends SplitLayoutLeaf>(
|
|
85
85
|
state: SplitLayoutState<L>,
|
|
86
86
|
leaf: L,
|
|
87
|
-
splitId: string
|
|
87
|
+
splitId: string,
|
|
88
|
+
placement: SplitPaneInput<L>['placement']
|
|
88
89
|
) {
|
|
90
|
+
if (!leaf || leaf.kind !== 'leaf' || (placement !== 'before' && placement !== 'after'))
|
|
91
|
+
throw new Error('invalid_state: invalid split input')
|
|
89
92
|
assertLayoutTree(leaf)
|
|
90
93
|
if (typeof splitId !== 'string' || splitId.length === 0)
|
|
91
94
|
throw new Error('invalid_state: split identity must be nonempty')
|
|
@@ -153,9 +156,11 @@ export function splitPane<L extends SplitLayoutLeaf>(
|
|
|
153
156
|
input: SplitPaneInput<L>
|
|
154
157
|
): SplitLayoutState<L> {
|
|
155
158
|
if (!containsLeaf(state.center, targetLeafId)) return state
|
|
159
|
+
if (input.direction !== 'row' && input.direction !== 'column')
|
|
160
|
+
throw new Error('invalid_state: invalid split input')
|
|
156
161
|
if (countLeaves(state.center) >= MAX_LAYOUT_LEAVES)
|
|
157
162
|
throw new Error('limit_exceeded: center layout has eight leaves')
|
|
158
|
-
assertUniqueInput(state, input.leaf, input.splitId)
|
|
163
|
+
assertUniqueInput(state, input.leaf, input.splitId, input.placement)
|
|
159
164
|
const target = listLeaves(state.center).find((leaf) => leaf.id === targetLeafId)
|
|
160
165
|
if (!target) return state
|
|
161
166
|
const children =
|
|
@@ -173,15 +178,10 @@ export function splitPane<L extends SplitLayoutLeaf>(
|
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* equal height. Existing opaque leaf objects are preserved. Split IDs are
|
|
181
|
-
* recycled across rebuilt branches, whose axis and descendants can change;
|
|
182
|
-
* resize actions must resolve against the current tree. Ratios are recomputed.
|
|
183
|
-
* This is opt-in: callers
|
|
184
|
-
* that need an explicit axis and ratio should keep using `splitPane`.
|
|
181
|
+
* Reflow into a balanced row-major grid of at most two rows and four columns,
|
|
182
|
+
* preserving opaque leaves. Split IDs are recycled across rebuilt branches, so
|
|
183
|
+
* resize actions must resolve against the current tree. Call `splitPane` instead
|
|
184
|
+
* when the axis and ratio must be explicit.
|
|
185
185
|
*/
|
|
186
186
|
export function splitPaneBalanced<L extends SplitLayoutLeaf>(
|
|
187
187
|
state: SplitLayoutState<L>,
|
|
@@ -193,7 +193,7 @@ export function splitPaneBalanced<L extends SplitLayoutLeaf>(
|
|
|
193
193
|
if (targetIndex < 0) return state
|
|
194
194
|
if (leaves.length >= MAX_LAYOUT_LEAVES)
|
|
195
195
|
throw new Error('limit_exceeded: center layout has eight leaves')
|
|
196
|
-
const splitIds = assertUniqueInput(state, input.leaf, input.splitId)
|
|
196
|
+
const splitIds = assertUniqueInput(state, input.leaf, input.splitId, input.placement)
|
|
197
197
|
|
|
198
198
|
const insertionIndex = targetIndex + (input.placement === 'before' ? 0 : 1)
|
|
199
199
|
leaves.splice(insertionIndex, 0, input.leaf)
|
|
@@ -281,9 +281,9 @@ export function createVersionedPersistence<Doc>(
|
|
|
281
281
|
|
|
282
282
|
return {
|
|
283
283
|
read() {
|
|
284
|
-
const storage = storageOf()
|
|
285
|
-
if (!storage) return fallback()
|
|
286
284
|
try {
|
|
285
|
+
const storage = storageOf()
|
|
286
|
+
if (!storage) return fallback()
|
|
287
287
|
const found = readVersionedRecord(storage, recordSchema, options.now)
|
|
288
288
|
if (found.status === 'record') {
|
|
289
289
|
previous = found.record
|
|
@@ -300,9 +300,9 @@ export function createVersionedPersistence<Doc>(
|
|
|
300
300
|
return fallback()
|
|
301
301
|
},
|
|
302
302
|
write(next) {
|
|
303
|
-
const storage = storageOf()
|
|
304
|
-
if (!storage) return
|
|
305
303
|
try {
|
|
304
|
+
const storage = storageOf()
|
|
305
|
+
if (!storage) return
|
|
306
306
|
const document = codec.encode(next, previous)
|
|
307
307
|
if (writeVersionedRecord(storage, schema.key, document)) {
|
|
308
308
|
previous =
|