fmt(js): npm run fix on merge (#119573)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
8adb99fabd
commit
ec21bd7674
@@ -42,10 +42,15 @@ export function bindLayoutSides() {
|
||||
const $leftEdgeOpen = computed([$panesFlipped, $sidebarOpen, $fileBrowserOpen], (flipped, sidebar, files) =>
|
||||
flipped ? files : sidebar
|
||||
)
|
||||
|
||||
const $rightEdgeOpen = computed([$panesFlipped, $sidebarOpen, $fileBrowserOpen], (flipped, sidebar, files) =>
|
||||
flipped ? sidebar : files
|
||||
)
|
||||
|
||||
bindTreeSideVisibility('left', $leftEdgeOpen, open => ($panesFlipped.get() ? setFileBrowserOpen : setSidebarOpen)(open))
|
||||
bindTreeSideVisibility('right', $rightEdgeOpen, open => ($panesFlipped.get() ? setSidebarOpen : setFileBrowserOpen)(open))
|
||||
bindTreeSideVisibility('left', $leftEdgeOpen, open =>
|
||||
($panesFlipped.get() ? setFileBrowserOpen : setSidebarOpen)(open)
|
||||
)
|
||||
bindTreeSideVisibility('right', $rightEdgeOpen, open =>
|
||||
($panesFlipped.get() ? setSidebarOpen : setFileBrowserOpen)(open)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ async function boot() {
|
||||
] as const) {
|
||||
registry.register({ id, area: 'panes', data: { placement }, render: () => null })
|
||||
}
|
||||
|
||||
registry.register({
|
||||
id: 'bots',
|
||||
area: 'panes',
|
||||
@@ -56,6 +57,7 @@ async function boot() {
|
||||
const preset = registry.getArea('layouts').find(p => p.id === id)!
|
||||
presets.applyLayoutPreset(id, preset.data as LayoutNode)
|
||||
}
|
||||
|
||||
const snapshot = () => ({
|
||||
tree: structuredClone(tree.$layoutTree.get()),
|
||||
panes: structuredClone(panes.$paneStates.get()),
|
||||
@@ -114,7 +116,10 @@ it('restores independently customized modes through real pane bindings and reloa
|
||||
// A moved/customized tree, dismissals and an unfinished resize survive too.
|
||||
mode.setInterfaceMode('advanced')
|
||||
app.apply('default')
|
||||
tree.moveTreePane('files', { groupId: model.findGroupOfPane(tree.$layoutTree.get()!, 'workspace')!.id, pos: 'center' })
|
||||
tree.moveTreePane('files', {
|
||||
groupId: model.findGroupOfPane(tree.$layoutTree.get()!, 'workspace')!.id,
|
||||
pos: 'center'
|
||||
})
|
||||
tree.dismissTreePane('review')
|
||||
const root = tree.$layoutTree.get()!
|
||||
|
||||
@@ -122,7 +127,10 @@ it('restores independently customized modes through real pane bindings and reloa
|
||||
throw new Error('fixture must have a split')
|
||||
}
|
||||
|
||||
tree.setTreeSplitWeights(root.id, root.weights.map((weight, i) => weight + i))
|
||||
tree.setTreeSplitWeights(
|
||||
root.id,
|
||||
root.weights.map((weight, i) => weight + i)
|
||||
)
|
||||
const advanced = app.snapshot()
|
||||
mode.setInterfaceMode('simple')
|
||||
const simple = app.snapshot()
|
||||
@@ -139,47 +147,53 @@ it('restores independently customized modes through real pane bindings and reloa
|
||||
expect(app.model.allPaneIds(app.tree.$layoutTree.get()!)).toContain('new-shared-pane')
|
||||
})
|
||||
|
||||
it.each(['advanced', 'simple'] as const)('keeps legacy %s data and never re-inherits it after migration', async initialMode => {
|
||||
const { group, split, normalize } = await import('./model')
|
||||
const legacyTree = split(
|
||||
'row',
|
||||
[group(['sessions', 'bots']), group(['workspace']), group(['files', 'review', 'terminal'])],
|
||||
[2, 8, 3]
|
||||
)
|
||||
const legacyPanes = { 'chat-sidebar': { open: false, widthOverride: 301 }, 'file-browser': { open: true } }
|
||||
const raw = JSON.stringify(legacyTree)
|
||||
window.localStorage.setItem('hermes.desktop.layoutTree.v2', raw)
|
||||
window.localStorage.setItem('hermes.desktop.paneStates.v1', JSON.stringify(legacyPanes))
|
||||
window.localStorage.setItem('hermes.desktop.layoutPreset.active', 'custom')
|
||||
window.localStorage.setItem('hermes.desktop.hiddenStripTabs.v1', '["bots"]')
|
||||
if (initialMode === 'simple') {
|
||||
window.localStorage.setItem('hermes.desktop.interfaceMode.v1', 'simple')
|
||||
it.each(['advanced', 'simple'] as const)(
|
||||
'keeps legacy %s data and never re-inherits it after migration',
|
||||
async initialMode => {
|
||||
const { group, split, normalize } = await import('./model')
|
||||
|
||||
const legacyTree = split(
|
||||
'row',
|
||||
[group(['sessions', 'bots']), group(['workspace']), group(['files', 'review', 'terminal'])],
|
||||
[2, 8, 3]
|
||||
)
|
||||
|
||||
const legacyPanes = { 'chat-sidebar': { open: false, widthOverride: 301 }, 'file-browser': { open: true } }
|
||||
const raw = JSON.stringify(legacyTree)
|
||||
window.localStorage.setItem('hermes.desktop.layoutTree.v2', raw)
|
||||
window.localStorage.setItem('hermes.desktop.paneStates.v1', JSON.stringify(legacyPanes))
|
||||
window.localStorage.setItem('hermes.desktop.layoutPreset.active', 'custom')
|
||||
window.localStorage.setItem('hermes.desktop.hiddenStripTabs.v1', '["bots"]')
|
||||
|
||||
if (initialMode === 'simple') {
|
||||
window.localStorage.setItem('hermes.desktop.interfaceMode.v1', 'simple')
|
||||
}
|
||||
|
||||
const { mode, tree, panes, layout } = await boot()
|
||||
expect(mode.$interfaceMode.get()).toBe(initialMode)
|
||||
expect(panes.$paneStates.get()).toEqual(legacyPanes)
|
||||
expect(tree.$hiddenStripTabs.get().has('bots')).toBe(true)
|
||||
// Boot collapse may update the tree, so preserve the settled arrangement.
|
||||
const settled = structuredClone(tree.$layoutTree.get())
|
||||
mode.setInterfaceMode(initialMode === 'simple' ? 'advanced' : 'simple')
|
||||
expect(tree.$collapsedTreeSides.get().has(layout.sidebarSide())).toBe(!layout.$sidebarOpen.get())
|
||||
expect(tree.$collapsedTreeSides.get().has(layout.fileBrowserSide())).toBe(!layout.$fileBrowserOpen.get())
|
||||
mode.setInterfaceMode(initialMode)
|
||||
expect(tree.$layoutTree.get()).toEqual(settled)
|
||||
expect(panes.$paneStates.get()).toEqual(legacyPanes)
|
||||
|
||||
mode.setInterfaceMode('simple')
|
||||
tree.setStripTabHidden('bots', false)
|
||||
panes.setPaneWidthOverride('chat-sidebar', 210)
|
||||
vi.resetModules()
|
||||
const reloaded = await boot()
|
||||
expect(reloaded.tree.$hiddenStripTabs.get().size).toBe(0)
|
||||
expect(reloaded.panes.$paneStates.get()['chat-sidebar'].widthOverride).toBe(210)
|
||||
reloaded.mode.setInterfaceMode('advanced')
|
||||
expect(reloaded.panes.$paneStates.get()).toEqual(legacyPanes)
|
||||
const restoredTree = reloaded.tree.$layoutTree.get()
|
||||
const expectedTree = normalize(legacyTree)
|
||||
assert(restoredTree && expectedTree)
|
||||
expect(reloaded.model.allPaneIds(restoredTree)).toEqual(reloaded.model.allPaneIds(expectedTree))
|
||||
}
|
||||
|
||||
const { mode, tree, panes, layout } = await boot()
|
||||
expect(mode.$interfaceMode.get()).toBe(initialMode)
|
||||
expect(panes.$paneStates.get()).toEqual(legacyPanes)
|
||||
expect(tree.$hiddenStripTabs.get().has('bots')).toBe(true)
|
||||
// Boot collapse may update the tree, so preserve the settled arrangement.
|
||||
const settled = structuredClone(tree.$layoutTree.get())
|
||||
mode.setInterfaceMode(initialMode === 'simple' ? 'advanced' : 'simple')
|
||||
expect(tree.$collapsedTreeSides.get().has(layout.sidebarSide())).toBe(!layout.$sidebarOpen.get())
|
||||
expect(tree.$collapsedTreeSides.get().has(layout.fileBrowserSide())).toBe(!layout.$fileBrowserOpen.get())
|
||||
mode.setInterfaceMode(initialMode)
|
||||
expect(tree.$layoutTree.get()).toEqual(settled)
|
||||
expect(panes.$paneStates.get()).toEqual(legacyPanes)
|
||||
|
||||
mode.setInterfaceMode('simple')
|
||||
tree.setStripTabHidden('bots', false)
|
||||
panes.setPaneWidthOverride('chat-sidebar', 210)
|
||||
vi.resetModules()
|
||||
const reloaded = await boot()
|
||||
expect(reloaded.tree.$hiddenStripTabs.get().size).toBe(0)
|
||||
expect(reloaded.panes.$paneStates.get()['chat-sidebar'].widthOverride).toBe(210)
|
||||
reloaded.mode.setInterfaceMode('advanced')
|
||||
expect(reloaded.panes.$paneStates.get()).toEqual(legacyPanes)
|
||||
const restoredTree = reloaded.tree.$layoutTree.get()
|
||||
const expectedTree = normalize(legacyTree)
|
||||
assert(restoredTree && expectedTree)
|
||||
expect(reloaded.model.allPaneIds(restoredTree)).toEqual(reloaded.model.allPaneIds(expectedTree))
|
||||
})
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ it.each([false, true])('restores floating geometry and collapse with keep-alive=
|
||||
const { registry } = await import('@/contrib/registry')
|
||||
const { setInterfaceMode } = await import('@/store/interface-mode')
|
||||
const { FloatingPanes } = await import('./floating-panes')
|
||||
|
||||
const dispose = registry.register({
|
||||
area: 'panes',
|
||||
id: 'card',
|
||||
@@ -17,6 +18,7 @@ it.each([false, true])('restores floating geometry and collapse with keep-alive=
|
||||
data: { placement: 'floating', width: 240, height: 180, lifecycleKeepAlive: keepAlive },
|
||||
render: () => <input aria-label="Floating draft" defaultValue="original" />
|
||||
})
|
||||
|
||||
try {
|
||||
const view = render(<FloatingPanes />)
|
||||
const card = () => view.container.querySelector<HTMLElement>('[data-floating-pane="card"]')!
|
||||
@@ -33,10 +35,12 @@ it.each([false, true])('restores floating geometry and collapse with keep-alive=
|
||||
expect(card().style.left).toBe('37px')
|
||||
expect(card().style.top).toBe('71px')
|
||||
expect(view.getByRole('textbox')).toBeDefined()
|
||||
|
||||
if (keepAlive) {
|
||||
expect(view.getByRole('textbox')).toBe(input)
|
||||
expect(input.value).toBe('unsaved')
|
||||
}
|
||||
|
||||
act(() => setInterfaceMode('simple'))
|
||||
expect(card().style.left).toBe(simplePosition)
|
||||
expect(view.queryByRole('textbox')).toBeNull()
|
||||
|
||||
@@ -80,10 +80,12 @@ function FloatingPane({ pane }: { pane: Contribution }) {
|
||||
(update: (current: FloatingRect) => FloatingRect) => {
|
||||
const positions = $positions.get()
|
||||
const current = positions[pane.id]
|
||||
|
||||
const next = update({
|
||||
...anchoredRect(anchor, { width: size.width, height: size.height }, viewport.current),
|
||||
...current
|
||||
})
|
||||
|
||||
$positions.set({ ...positions, [pane.id]: { x: next.x, y: next.y, collapsed: current?.collapsed } })
|
||||
},
|
||||
[pane.id, anchor, size.width, size.height]
|
||||
@@ -124,22 +126,25 @@ function FloatingPane({ pane }: { pane: Contribution }) {
|
||||
event.preventDefault()
|
||||
}, [])
|
||||
|
||||
const onPointerMove = useCallback((event: ReactPointerEvent<HTMLElement>) => {
|
||||
const from = drag.current
|
||||
const onPointerMove = useCallback(
|
||||
(event: ReactPointerEvent<HTMLElement>) => {
|
||||
const from = drag.current
|
||||
|
||||
if (!from) {
|
||||
return
|
||||
}
|
||||
if (!from) {
|
||||
return
|
||||
}
|
||||
|
||||
drag.current = { x: event.clientX, y: event.clientY }
|
||||
drag.current = { x: event.clientX, y: event.clientY }
|
||||
|
||||
setRect(current =>
|
||||
clampFloatingRect(
|
||||
{ ...current, x: current.x + event.clientX - from.x, y: current.y + event.clientY - from.y },
|
||||
viewport.current
|
||||
setRect(current =>
|
||||
clampFloatingRect(
|
||||
{ ...current, x: current.x + event.clientX - from.x, y: current.y + event.clientY - from.y },
|
||||
viewport.current
|
||||
)
|
||||
)
|
||||
)
|
||||
}, [setRect])
|
||||
},
|
||||
[setRect]
|
||||
)
|
||||
|
||||
const onPointerUp = useCallback(
|
||||
(event: ReactPointerEvent<HTMLElement>) => {
|
||||
|
||||
@@ -27,6 +27,7 @@ import { LayoutTreeRoot } from '.'
|
||||
// A native guest has a lifetime beyond React. DOM identity alone misses a
|
||||
// detach/reparent (even moveBefore destroys Electron's webview guest).
|
||||
const disconnected: HTMLElement[] = []
|
||||
|
||||
class LiveGuest extends HTMLElement {
|
||||
disconnectedCallback() {
|
||||
disconnected.push(this)
|
||||
@@ -37,6 +38,7 @@ customElements.define('pane-live-guest', LiveGuest)
|
||||
// jsdom has no layout. Deliver real observer notifications to the production
|
||||
// shared observer; the browser harness separately checks CSS anchor geometry.
|
||||
const observers = new Set<ResizeObserverProbe>()
|
||||
|
||||
class ResizeObserverProbe {
|
||||
targets = new Set<Element>()
|
||||
constructor(readonly callback: ResizeObserverCallback) {
|
||||
@@ -55,6 +57,7 @@ class ResizeObserverProbe {
|
||||
|
||||
function resize(target: HTMLElement, width: number, height: number) {
|
||||
const size = { inlineSize: width, blockSize: height }
|
||||
|
||||
const entry: ResizeObserverEntry = {
|
||||
target,
|
||||
contentRect: new DOMRectReadOnly(0, 0, width, height),
|
||||
@@ -108,6 +111,7 @@ function registerPane(id: string, keepAlive = true, data: Record<string, unknown
|
||||
data: { ...data, lifecycleKeepAlive: keepAlive },
|
||||
render: Probe
|
||||
})
|
||||
|
||||
disposers.push(dispose)
|
||||
|
||||
return dispose
|
||||
@@ -150,10 +154,12 @@ it('keeps a live body continuously connected across replacement IDs, ancestry an
|
||||
const input = page.querySelector('input')!
|
||||
input.value = 'unsaved page state'
|
||||
|
||||
setTree(split('row', [
|
||||
group(['plain'], { id: 'chat' }),
|
||||
split('column', [group(['live'], { id: 'replacement' }), group([], { id: 'empty' })])
|
||||
]))
|
||||
setTree(
|
||||
split('row', [
|
||||
group(['plain'], { id: 'chat' }),
|
||||
split('column', [group(['live'], { id: 'replacement' }), group([], { id: 'empty' })])
|
||||
])
|
||||
)
|
||||
expect(guest()).toBe(page)
|
||||
expect(page.dataset.group).toBe('replacement')
|
||||
expect(page.dataset.visible).toBe('true')
|
||||
@@ -206,9 +212,14 @@ it('destroys removed contributions and does not revive their old activation on r
|
||||
it('gates hidden side guests without mounting never-activated tabs or disconnecting live ones', () => {
|
||||
registerPane('live')
|
||||
registerPane('background')
|
||||
disposers.push(registry.register({
|
||||
area: 'panes', id: 'workspace', data: { placement: 'main' }, render: () => <div />
|
||||
}))
|
||||
disposers.push(
|
||||
registry.register({
|
||||
area: 'panes',
|
||||
id: 'workspace',
|
||||
data: { placement: 'main' },
|
||||
render: () => <div />
|
||||
})
|
||||
)
|
||||
setTree(split('row', [group(['workspace']), group(['live', 'background'], { id: 'side' })]))
|
||||
$collapsedTreeSides.set(new Set(['right']))
|
||||
render(<LayoutTreeRoot />)
|
||||
@@ -240,7 +251,11 @@ it('routes DOM group targeting to the new zone without adding duplicate drop zon
|
||||
expect($activeTreeGroup.get()).toBe('new-zone')
|
||||
fireEvent.pointerOver(page)
|
||||
expect($hoveredTreeGroup.get()).toBe('new-zone')
|
||||
expect(snapshotZones().map(zone => zone.id).sort()).toEqual(['new-zone', 'other'])
|
||||
expect(
|
||||
snapshotZones()
|
||||
.map(zone => zone.id)
|
||||
.sort()
|
||||
).toEqual(['new-zone', 'other'])
|
||||
})
|
||||
|
||||
it('reloads only the explicit epoch target, even while it is hidden', () => {
|
||||
@@ -275,6 +290,7 @@ it('keeps live tabs outside the bounded cache while ordinary tabs still park', (
|
||||
setTree(group(panes, { id: 'zone' }))
|
||||
render(<LayoutTreeRoot />)
|
||||
const page = guest()!
|
||||
|
||||
for (const active of ['a', 'b', 'c', 'd']) {
|
||||
setTree(group(panes, { id: 'zone', active }))
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ export function KeepAlivePanes({ children }: { children: ReactNode }) {
|
||||
const panes = useContributions('panes')
|
||||
const epochs = useStore($treePaneEpochs)
|
||||
const [placements, setPlacements] = useState<Placements>(() => new Map())
|
||||
|
||||
const place = useCallback<PlacePane>((id, placement) => {
|
||||
setPlacements(previous => {
|
||||
// Restored background tabs are lazy until first activation.
|
||||
@@ -73,6 +74,7 @@ export function KeepAlivePanes({ children }: { children: ReactNode }) {
|
||||
const present = new Set(
|
||||
panes.filter(pane => pane.render && paneChrome(pane).lifecycleKeepAlive).map(pane => pane.id)
|
||||
)
|
||||
|
||||
const retained = [...previous].filter(([id]) => present.has(id))
|
||||
|
||||
return retained.length === previous.size ? previous : new Map(retained)
|
||||
@@ -105,6 +107,7 @@ interface KeepAlivePaneHostProps {
|
||||
|
||||
const KeepAlivePaneHost = memo(function KeepAlivePaneHost({ pane, placement, epoch }: KeepAlivePaneHostProps) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Retain native guest coordinates while its placement is absent/minimized.
|
||||
// CSS follows the slot during resize; recording dimensions never re-renders
|
||||
// a guest or moves its connected host.
|
||||
@@ -119,6 +122,7 @@ const KeepAlivePaneHost = memo(function KeepAlivePaneHost({ pane, placement, epo
|
||||
},
|
||||
[placement.visible]
|
||||
)
|
||||
|
||||
useResizeObserver(rememberSize, ref)
|
||||
|
||||
return (
|
||||
@@ -190,10 +194,6 @@ export function KeepAlivePaneSlot({
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{ anchorName: anchor } as CSSProperties}
|
||||
/>
|
||||
<div aria-hidden className="pointer-events-none absolute inset-0" style={{ anchorName: anchor } as CSSProperties} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export function NarrowOverlays() {
|
||||
const stableHosts = useStablePaneHosts()
|
||||
const hiddenPanes = useStore($hiddenTreePanes)
|
||||
const [reveal, setReveal] = useState<{ id: string; pinned: boolean } | null>(null)
|
||||
|
||||
const onMouseLeave = useCallback<MouseEventHandler<HTMLDivElement>>(event => {
|
||||
// The overlay's chrome and its stable guest are DOM siblings, but one
|
||||
// hover boundary. Crossing between them must not dismiss an unpinned pane.
|
||||
|
||||
@@ -368,6 +368,7 @@ export function TreeGroup({
|
||||
const mountedPanes = node.minimized
|
||||
? keptPanes.filter(id => Boolean(paneChrome(paneFor(id)).lifecycleKeepAlive))
|
||||
: keptPanes
|
||||
|
||||
const hostedPanes = stableHosts ? node.panes.filter(id => paneChrome(paneFor(id)).lifecycleKeepAlive) : []
|
||||
const inlinePanes = mountedPanes.filter(id => !stableHosts || !paneChrome(paneFor(id)).lifecycleKeepAlive)
|
||||
|
||||
|
||||
@@ -70,7 +70,9 @@ function persist(tree: LayoutNode | null) {
|
||||
export const $layoutTree = modeLayout.atom<LayoutNode | null>(
|
||||
LAYOUT_KEYS.tree,
|
||||
() => defaultTrees[modeLayout.mode],
|
||||
Codecs.json(parsed => (isLayoutNode(parsed) ? normalize(migratePersistedTree(parsed)) : defaultTrees[modeLayout.mode])),
|
||||
Codecs.json(parsed =>
|
||||
isLayoutNode(parsed) ? normalize(migratePersistedTree(parsed)) : defaultTrees[modeLayout.mode]
|
||||
),
|
||||
true
|
||||
)
|
||||
|
||||
@@ -196,7 +198,11 @@ const paneSetCodec = {
|
||||
encode: (value: ReadonlySet<string>) => Codecs.stringArray.encode([...value])
|
||||
}
|
||||
|
||||
export const $dismissedPanes = modeLayout.atom<ReadonlySet<string>>(LAYOUT_KEYS.dismissed, () => new Set(), paneSetCodec)
|
||||
export const $dismissedPanes = modeLayout.atom<ReadonlySet<string>>(
|
||||
LAYOUT_KEYS.dismissed,
|
||||
() => new Set(),
|
||||
paneSetCodec
|
||||
)
|
||||
|
||||
function setDismissed(paneId: string, dismissed: boolean) {
|
||||
const next = toggledSet($dismissedPanes.get(), paneId, dismissed)
|
||||
@@ -288,7 +294,11 @@ function recalledEdgeWeights(paneId: string): [number, number] | undefined {
|
||||
// Persisted separately from `$hiddenTreePanes` (whose persistence each side
|
||||
// binding owns) so a hidden Bots tab stays hidden across launches even though
|
||||
// dock enforcement re-adopts the pane into the sessions zone every boot.
|
||||
export const $hiddenStripTabs = modeLayout.atom<ReadonlySet<string>>(LAYOUT_KEYS.hiddenTabs, () => new Set(), paneSetCodec)
|
||||
export const $hiddenStripTabs = modeLayout.atom<ReadonlySet<string>>(
|
||||
LAYOUT_KEYS.hiddenTabs,
|
||||
() => new Set(),
|
||||
paneSetCodec
|
||||
)
|
||||
|
||||
export function isStripTabHidden(paneId: string): boolean {
|
||||
return $hiddenStripTabs.get().has(paneId)
|
||||
@@ -1137,6 +1147,7 @@ export function bindTreeSideVisibility(
|
||||
if (!hasPersistedSides) {
|
||||
setTreeSideCollapsed(side, !$open.get())
|
||||
}
|
||||
|
||||
$open.listen(open => setTreeSideCollapsed(side, !open))
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ it('retains legacy data in memory when migration writes fail and retries on the
|
||||
if (key === 'hermes.desktop.layoutModeScopes.v1') {
|
||||
throw new DOMException('Full', 'QuotaExceededError')
|
||||
}
|
||||
|
||||
storage.setItem(key, value)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -77,7 +77,7 @@ export function createLayoutPersistence(initialMode: InterfaceMode, persistent:
|
||||
|
||||
if (persistent) {
|
||||
// Explicit empties keep migration retries from reviving cleared state.
|
||||
writeKey(keyFor(key), mode === 'simple' ? raw ?? 'null' : raw)
|
||||
writeKey(keyFor(key), mode === 'simple' ? (raw ?? 'null') : raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,12 +89,13 @@ export function createLayoutPersistence(initialMode: InterfaceMode, persistent:
|
||||
manual = false
|
||||
): WritableAtom<T> {
|
||||
const memory = new Map<InterfaceMode, T>()
|
||||
|
||||
const load = (): T => {
|
||||
if (memory.has(mode)) {
|
||||
return memory.get(mode)!
|
||||
}
|
||||
|
||||
const raw = persistent ? readKey(keyFor(key)) ?? (mode === 'simple' ? legacy.get(key) ?? null : null) : null
|
||||
const raw = persistent ? (readKey(keyFor(key)) ?? (mode === 'simple' ? (legacy.get(key) ?? null) : null)) : null
|
||||
|
||||
if (raw !== null) {
|
||||
try {
|
||||
@@ -143,6 +144,7 @@ export function createLayoutPersistence(initialMode: InterfaceMode, persistent:
|
||||
},
|
||||
onRestore(listener: () => void) {
|
||||
listeners.add(listener)
|
||||
|
||||
return () => void listeners.delete(listener)
|
||||
},
|
||||
write,
|
||||
|
||||
Reference in New Issue
Block a user