Skip to content

Commit bbe8620

Browse files
nmsncursoragent
andcommitted
fix(web): centralize Design Browser localStorage keys
Extract key builders, load/save, and remove helper into a shared non-component module so delete, panel, and tests cannot drift apart. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e2266fa commit bbe8620

5 files changed

Lines changed: 128 additions & 94 deletions

File tree

apps/web/src/components/DesignBrowserPanel.tsx

Lines changed: 20 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -61,18 +61,30 @@ import {
6161
projectRelativePathFromBrowserUrl,
6262
type BrowserViewportId,
6363
} from './design-browser-tools';
64+
import {
65+
DESIGN_BROWSER_HISTORY_LIMIT,
66+
loadBrowserViewport,
67+
loadHistory,
68+
saveBrowserViewport,
69+
saveHistory,
70+
type BrowserHistoryEntry,
71+
} from './design-browser-storage';
6472
import { Icon } from './Icon';
6573
import { BoardComposerPopover } from './BoardComposerPopover';
6674
import { PreviewDrawOverlay } from './PreviewDrawOverlay';
6775
import { RemixIcon } from './RemixIcon';
6876

69-
type BrowserHistoryEntry = {
70-
iconUrl?: string;
71-
title: string;
72-
url: string;
73-
lastVisitedAt: number;
74-
visitCount: number;
75-
};
77+
export {
78+
removeDesignBrowserProjectCache,
79+
designBrowserHistoryStorageKey,
80+
designBrowserViewportStorageKey,
81+
isHistoryEntry,
82+
loadBrowserViewport,
83+
loadHistory,
84+
saveBrowserViewport,
85+
saveHistory,
86+
type BrowserHistoryEntry,
87+
} from './design-browser-storage';
7688

7789
type BrowserNavigationEntry = {
7890
title: string;
@@ -283,7 +295,7 @@ export interface BrowserPageInfo {
283295

284296
const EMPTY_URL = 'about:blank';
285297
const DESIGN_BROWSER_PARTITION = 'persist:open-design-design-browser';
286-
const HISTORY_LIMIT = 80;
298+
const HISTORY_LIMIT = DESIGN_BROWSER_HISTORY_LIMIT;
287299
const HISTORY_SUGGESTION_LIMIT = 20;
288300
const EMPTY_PREVIEW_COMMENTS: PreviewComment[] = [];
289301
// Cap the resource-hint (`dns-prefetch`/`preconnect`) links we leave in <head>.
@@ -3300,73 +3312,6 @@ function BrowserSiteIcon({
33003312
);
33013313
}
33023314

3303-
export function loadHistory(projectId: string): BrowserHistoryEntry[] {
3304-
if (typeof window === 'undefined') return [];
3305-
try {
3306-
const raw = window.localStorage.getItem(historyStorageKey(projectId));
3307-
const parsed = raw ? JSON.parse(raw) : [];
3308-
if (!Array.isArray(parsed)) return [];
3309-
return parsed
3310-
.filter(isHistoryEntry)
3311-
.sort((left, right) => right.lastVisitedAt - left.lastVisitedAt)
3312-
.slice(0, HISTORY_LIMIT);
3313-
} catch {
3314-
return [];
3315-
}
3316-
}
3317-
3318-
export function saveHistory(projectId: string, history: BrowserHistoryEntry[]) {
3319-
if (typeof window === 'undefined') return;
3320-
try {
3321-
window.localStorage.setItem(historyStorageKey(projectId), JSON.stringify(history.slice(0, HISTORY_LIMIT)));
3322-
} catch {
3323-
// Ignore storage quota and private-mode failures.
3324-
}
3325-
}
3326-
3327-
function historyStorageKey(projectId: string): string {
3328-
return `od:design-browser:${projectId}:history:v1`;
3329-
}
3330-
3331-
function viewportStorageKey(projectId: string): string {
3332-
return `od:design-browser:${projectId}:viewport:v1`;
3333-
}
3334-
3335-
function isBrowserViewportId(value: unknown): value is BrowserViewportId {
3336-
return value === 'desktop' || value === 'tablet' || value === 'mobile';
3337-
}
3338-
3339-
export function loadBrowserViewport(projectId: string): BrowserViewportId {
3340-
if (typeof window === 'undefined') return 'desktop';
3341-
try {
3342-
const stored = window.localStorage.getItem(viewportStorageKey(projectId));
3343-
return isBrowserViewportId(stored) ? stored : 'desktop';
3344-
} catch {
3345-
return 'desktop';
3346-
}
3347-
}
3348-
3349-
export function saveBrowserViewport(projectId: string, viewport: BrowserViewportId) {
3350-
if (typeof window === 'undefined') return;
3351-
try {
3352-
window.localStorage.setItem(viewportStorageKey(projectId), viewport);
3353-
} catch {
3354-
// Ignore storage quota and private-mode failures.
3355-
}
3356-
}
3357-
3358-
export function isHistoryEntry(value: unknown): value is BrowserHistoryEntry {
3359-
if (typeof value !== 'object' || value == null || Array.isArray(value)) return false;
3360-
const record = value as Record<string, unknown>;
3361-
return (
3362-
typeof record.url === 'string' &&
3363-
typeof record.title === 'string' &&
3364-
typeof record.lastVisitedAt === 'number' &&
3365-
typeof record.visitCount === 'number' &&
3366-
(record.iconUrl === undefined || typeof record.iconUrl === 'string')
3367-
);
3368-
}
3369-
33703315
export function normalizeBrowserAddress(rawAddress: string): string {
33713316
const value = rawAddress.trim();
33723317
if (!value) return EMPTY_URL;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { BrowserViewportId } from './design-browser-tools';
2+
3+
/** Persisted visit list for Design Browser, scoped per project. */
4+
export type BrowserHistoryEntry = {
5+
iconUrl?: string;
6+
title: string;
7+
url: string;
8+
lastVisitedAt: number;
9+
visitCount: number;
10+
};
11+
12+
export const DESIGN_BROWSER_HISTORY_LIMIT = 80;
13+
14+
export function designBrowserHistoryStorageKey(projectId: string): string {
15+
return `od:design-browser:${projectId}:history:v1`;
16+
}
17+
18+
export function designBrowserViewportStorageKey(projectId: string): string {
19+
return `od:design-browser:${projectId}:viewport:v1`;
20+
}
21+
22+
/** Drop per-project Design Browser localStorage after a successful project delete. */
23+
export function removeDesignBrowserProjectCache(projectId: string): void {
24+
if (typeof window === 'undefined') return;
25+
try {
26+
window.localStorage.removeItem(designBrowserHistoryStorageKey(projectId));
27+
window.localStorage.removeItem(designBrowserViewportStorageKey(projectId));
28+
} catch {
29+
// Ignore private-mode/quota errors; the cache entry is best-effort.
30+
}
31+
}
32+
33+
export function isHistoryEntry(value: unknown): value is BrowserHistoryEntry {
34+
if (typeof value !== 'object' || value == null || Array.isArray(value)) return false;
35+
const record = value as Record<string, unknown>;
36+
return (
37+
typeof record.url === 'string' &&
38+
typeof record.title === 'string' &&
39+
typeof record.lastVisitedAt === 'number' &&
40+
typeof record.visitCount === 'number' &&
41+
(record.iconUrl === undefined || typeof record.iconUrl === 'string')
42+
);
43+
}
44+
45+
export function loadHistory(projectId: string): BrowserHistoryEntry[] {
46+
if (typeof window === 'undefined') return [];
47+
try {
48+
const raw = window.localStorage.getItem(designBrowserHistoryStorageKey(projectId));
49+
const parsed = raw ? JSON.parse(raw) : [];
50+
if (!Array.isArray(parsed)) return [];
51+
return parsed
52+
.filter(isHistoryEntry)
53+
.sort((left, right) => right.lastVisitedAt - left.lastVisitedAt)
54+
.slice(0, DESIGN_BROWSER_HISTORY_LIMIT);
55+
} catch {
56+
return [];
57+
}
58+
}
59+
60+
export function saveHistory(projectId: string, history: BrowserHistoryEntry[]) {
61+
if (typeof window === 'undefined') return;
62+
try {
63+
window.localStorage.setItem(
64+
designBrowserHistoryStorageKey(projectId),
65+
JSON.stringify(history.slice(0, DESIGN_BROWSER_HISTORY_LIMIT)),
66+
);
67+
} catch {
68+
// Ignore storage quota and private-mode failures.
69+
}
70+
}
71+
72+
function isBrowserViewportId(value: unknown): value is BrowserViewportId {
73+
return value === 'desktop' || value === 'tablet' || value === 'mobile';
74+
}
75+
76+
export function loadBrowserViewport(projectId: string): BrowserViewportId {
77+
if (typeof window === 'undefined') return 'desktop';
78+
try {
79+
const stored = window.localStorage.getItem(designBrowserViewportStorageKey(projectId));
80+
return isBrowserViewportId(stored) ? stored : 'desktop';
81+
} catch {
82+
return 'desktop';
83+
}
84+
}
85+
86+
export function saveBrowserViewport(projectId: string, viewport: BrowserViewportId) {
87+
if (typeof window === 'undefined') return;
88+
try {
89+
window.localStorage.setItem(designBrowserViewportStorageKey(projectId), viewport);
90+
} catch {
91+
// Ignore storage quota and private-mode failures.
92+
}
93+
}

apps/web/src/state/projects.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import type {
3232
ProjectMetadata,
3333
ProjectTemplate,
3434
} from '../types';
35+
import { removeDesignBrowserProjectCache } from '../components/design-browser-storage';
3536

3637
export type { PluginInstallOutcome } from '@open-design/contracts';
3738
export type { PluginShareAction } from '@open-design/contracts';
@@ -684,18 +685,6 @@ function removeCachedTabs(projectId: string): void {
684685
}
685686
}
686687

687-
// Keep key shapes in sync with DesignBrowserPanel historyStorageKey /
688-
// viewportStorageKey. Cleared here so deleteProject does not import the panel.
689-
function removeDesignBrowserProjectCache(projectId: string): void {
690-
if (typeof window === 'undefined') return;
691-
try {
692-
window.localStorage.removeItem(`od:design-browser:${projectId}:history:v1`);
693-
window.localStorage.removeItem(`od:design-browser:${projectId}:viewport:v1`);
694-
} catch {
695-
// Ignore private-mode/quota errors; the cache entry is best-effort.
696-
}
697-
}
698-
699688
function writeCachedTabs(projectId: string, state: OpenTabsState): OpenTabsState {
700689
const next: OpenTabsState = {
701690
...state,

apps/web/tests/components/DesignBrowserPanel.test.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ import {
2828
saveBrowserViewport,
2929
saveHistory,
3030
} from '../../src/components/DesignBrowserPanel';
31+
import {
32+
designBrowserHistoryStorageKey,
33+
designBrowserViewportStorageKey,
34+
} from '../../src/components/design-browser-storage';
3135
import {
3236
browserCommentFilePath,
3337
isProjectHtmlBrowserUrl,
@@ -409,7 +413,7 @@ describe('loadHistory / saveHistory round-trip', () => {
409413

410414
it('drops malformed entries on load', () => {
411415
window.localStorage.setItem(
412-
`od:design-browser:${projectId}:history:v1`,
416+
designBrowserHistoryStorageKey(projectId),
413417
JSON.stringify([
414418
{ url: 'https://ok.com', title: 'OK', lastVisitedAt: 1, visitCount: 1 },
415419
{ url: 123, title: 'bad', lastVisitedAt: 1, visitCount: 1 },
@@ -421,7 +425,7 @@ describe('loadHistory / saveHistory round-trip', () => {
421425
});
422426

423427
it('returns an empty array for corrupt or non-array JSON', () => {
424-
const key = `od:design-browser:${projectId}:history:v1`;
428+
const key = designBrowserHistoryStorageKey(projectId);
425429
window.localStorage.setItem(key, 'not json');
426430
expect(loadHistory(projectId)).toEqual([]);
427431
window.localStorage.setItem(key, JSON.stringify({ not: 'an array' }));
@@ -457,12 +461,12 @@ describe('loadBrowserViewport / saveBrowserViewport round-trip', () => {
457461

458462
it('round-trips the selected browser viewport', () => {
459463
saveBrowserViewport(projectId, 'mobile');
460-
expect(window.localStorage.getItem(`od:design-browser:${projectId}:viewport:v1`)).toBe('mobile');
464+
expect(window.localStorage.getItem(designBrowserViewportStorageKey(projectId))).toBe('mobile');
461465
expect(loadBrowserViewport(projectId)).toBe('mobile');
462466
});
463467

464468
it('ignores malformed stored browser viewport values', () => {
465-
window.localStorage.setItem(`od:design-browser:${projectId}:viewport:v1`, 'watch');
469+
window.localStorage.setItem(designBrowserViewportStorageKey(projectId), 'watch');
466470
expect(loadBrowserViewport(projectId)).toBe('desktop');
467471
});
468472
});

apps/web/tests/state/projects.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import {
1313
pickLocalFolderPath,
1414
publishGeneratedPluginToGitHub,
1515
} from '../../src/state/projects';
16+
import {
17+
designBrowserHistoryStorageKey,
18+
designBrowserViewportStorageKey,
19+
} from '../../src/components/design-browser-storage';
1620

1721
describe('applyPlugin', () => {
1822
afterEach(() => {
@@ -468,9 +472,8 @@ describe('deleteProject local caches', () => {
468472
});
469473

470474
const tabsKey = 'open-design:project-tabs:v1:p1';
471-
// Keep in sync with DesignBrowserPanel historyStorageKey / viewportStorageKey.
472-
const historyKey = 'od:design-browser:p1:history:v1';
473-
const viewportKey = 'od:design-browser:p1:viewport:v1';
475+
const historyKey = designBrowserHistoryStorageKey('p1');
476+
const viewportKey = designBrowserViewportStorageKey('p1');
474477

475478
function stubWindowStore(): Map<string, string> {
476479
const store = new Map<string, string>([

0 commit comments

Comments
 (0)