feat: add lightweight Guanghu companion themes

This commit is contained in:
冰朔 2026-07-15 14:32:29 +08:00
parent 508acd0f12
commit 5699882938
8 changed files with 200 additions and 120 deletions

View File

@ -468,7 +468,12 @@ function summarizeScenario(scenarioName, scenario, runs) {
}
async function runBenchmarks(baseUrl) {
const browser = await chromium.launch({ headless: !options.headful })
const browser = await chromium.launch({
headless: !options.headful,
...(process.env.PLAYWRIGHT_EXECUTABLE_PATH
? { executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH }
: {}),
})
const summaries = {}
try {
for (const scenarioName of options.scenarioNames) {

View File

@ -1,73 +1,42 @@
/* App layout - minimal CSS, most styling via Tailwind */
.app-shell {
/* HoloLake Era visual system: the inherited Tolaria dark palette is warm
charcoal; override semantic tokens here so every existing panel speaks
the same deep-lake blue language as the world entrance. */
--surface-app: #071126;
--surface-sidebar: #0a1730;
--surface-panel: #0d1c38;
--surface-card: #102343;
--surface-popover: #12284c;
--surface-input: #0a1730;
--surface-button: #172e55;
--surface-dialog: #0d1d3a;
--surface-editor: #09162d;
--surface-overlay: rgba(2, 7, 18, .68);
--text-primary: #edf4ff;
--text-secondary: #b9c9e2;
--text-tertiary: #8ea4c6;
--text-muted: #7188ac;
--text-heading: #f3f7ff;
--border-default: #1d3a67;
--border-subtle: #142b4d;
--border-strong: #315588;
--border-input: #274a78;
--sidebar-border: #17365f;
--state-hover: #142b4e;
--state-hover-subtle: #0e213f;
--state-selected: #173d70;
--state-selected-strong: #24538d;
--state-active: #1d477e;
--accent-blue: #8dbbff;
--accent-blue-bg: rgba(105, 165, 255, .23);
--accent-blue-light: rgba(105, 165, 255, .16);
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
overflow: hidden;
background:
radial-gradient(58rem 35rem at 82% -16%, var(--guanghu-aurora), transparent 62%),
radial-gradient(42rem 30rem at -12% 112%, var(--guanghu-glow), transparent 65%),
var(--surface-app);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border-primary) 82%, var(--accent-blue));
background: var(--surface-app);
box-shadow: inset 0 0 0 1px var(--border-primary);
}
.hololake-bar { height: 52px; flex: 0 0 52px; display: flex; align-items: center; gap: 14px; padding: 0 20px; color: #eef4ff; background: linear-gradient(100deg, #08122b, #172d58 52%, #0b1731); border-bottom: 1px solid rgba(153, 189, 255, .28); letter-spacing: .02em; }
.hololake-bar { height: 44px; flex: 0 0 44px; display: flex; align-items: center; gap: 12px; padding: 0 16px; color: var(--text-primary); background: var(--surface-panel); border-bottom: 1px solid var(--border-default); letter-spacing: .02em; }
.hololake-bar__brand, .hololake-bar__library { border: 0; background: transparent; color: inherit; cursor: pointer; }
.hololake-bar__brand { display: inline-flex; align-items: center; gap: 9px; font-size: 16px; font-weight: 650; letter-spacing: .04em; }
.hololake-bar__brand b { color: #9cc3ff; font-weight: 500; }
.hololake-bar__mark { display: grid; place-items: center; width: 24px; height: 24px; color: #dceaff; border: 1px solid rgba(205, 225, 255, .48); border-radius: 50%; box-shadow: 0 0 22px rgba(96, 165, 250, .65); }
.hololake-bar__line { width: 1px; height: 17px; background: rgba(196, 218, 255, .35); }
.hololake-bar__context { color: #b7c8e7; font-size: 12px; }
.hololake-bar__library { margin-left: auto; padding: 6px 10px; border: 1px solid rgba(185, 211, 255, .36); border-radius: 999px; color: #dceaff; font-size: 12px; }
.hololake-home { position: relative; z-index: 2; width: 100%; overflow: auto; padding: clamp(32px, 6vw, 84px); color: #ecf4ff; background: radial-gradient(circle at 75% 10%, rgba(75, 135, 255, .25), transparent 28%), radial-gradient(circle at 12% 92%, rgba(83, 220, 255, .15), transparent 34%), #071126; }
.hololake-bar__brand { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 650; letter-spacing: .02em; }
.hololake-bar__brand b { color: var(--accent-blue); font-weight: 600; }
.hololake-bar__mark { display: grid; place-items: center; width: 20px; height: 20px; color: var(--accent-blue); border: 1px solid var(--border-strong); border-radius: 50%; }
.hololake-bar__line { width: 1px; height: 16px; background: var(--border-default); }
.hololake-bar__context { color: var(--text-secondary); font-size: 12px; }
.hololake-bar__library { margin-left: auto; padding: 5px 9px; border: 1px solid var(--border-default); border-radius: 6px; color: var(--text-secondary); font-size: 12px; }
.hololake-bar__library:hover { background: var(--state-hover-subtle); color: var(--text-primary); }
.hololake-bar__theme { padding: 5px 9px; border: 1px solid var(--border-default); border-radius: 6px; background: transparent; color: var(--text-secondary); font-size: 12px; cursor: pointer; }
.hololake-bar__theme:hover { background: var(--state-hover-subtle); color: var(--text-primary); }
.hololake-home { position: relative; z-index: 2; width: 100%; overflow: auto; padding: clamp(28px, 5vw, 64px); color: var(--text-primary); background: var(--surface-app); }
.hololake-home__hero { position: relative; max-width: 740px; padding: 24px 0 56px; overflow: hidden; }
.hololake-home__halo { position: absolute; right: -110px; top: -180px; width: 390px; aspect-ratio: 1; border: 1px solid rgba(166, 205, 255, .28); border-radius: 50%; box-shadow: 0 0 0 45px rgba(103, 165, 255, .05), 0 0 0 105px rgba(103, 165, 255, .035), 0 0 100px rgba(77, 143, 255, .4); }
.hololake-home__eyebrow, .hololake-home__section-head span { color: #8db5f7; font-size: 12px; letter-spacing: .16em; text-transform: uppercase; }
.hololake-home__halo { position: absolute; right: -80px; top: -140px; width: 300px; aspect-ratio: 1; border: 1px solid var(--border-default); border-radius: 50%; opacity: .45; }
.hololake-home__eyebrow, .hololake-home__section-head span { color: var(--accent-blue); font-size: 12px; letter-spacing: .12em; text-transform: uppercase; }
.hololake-home h1 { position: relative; margin: 18px 0 0; font-family: Georgia, 'Times New Roman', serif; font-size: clamp(52px, 7vw, 92px); font-weight: 500; line-height: .95; letter-spacing: -.06em; }
.hololake-home h1 em { color: #9cc7ff; font-style: italic; font-weight: 400; }
.hololake-home__lead { margin: 20px 0 0; font-size: 18px; letter-spacing: .16em; }
.hololake-home__copy { max-width: 560px; margin: 16px 0 0; color: #b9c8df; font-size: 15px; line-height: 1.9; }
.hololake-home__copy { max-width: 560px; margin: 16px 0 0; color: var(--text-secondary); font-size: 15px; line-height: 1.8; }
.hololake-home__actions { display: flex; align-items: center; gap: 18px; margin-top: 30px; }
.hololake-home__primary { padding: 12px 18px; border: 1px solid #b9d5ff; border-radius: 8px; background: linear-gradient(135deg, #c8dcff, #6093e8); color: #061126; font-weight: 700; cursor: pointer; }
.hololake-home__primary { padding: 10px 16px; border: 1px solid var(--border-strong); border-radius: 6px; background: var(--accent-blue); color: var(--text-inverse); font-weight: 650; cursor: pointer; }
.hololake-home__primary span, .hololake-module button span { margin-left: 12px; }
.hololake-home__status { color: #9eb3d4; font-size: 12px; }.hololake-home__status i { display: inline-block; width: 7px; height: 7px; margin-right: 7px; border-radius: 50%; background: #67e8b6; box-shadow: 0 0 12px #67e8b6; }
.hololake-home__status { color: var(--text-secondary); font-size: 12px; }.hololake-home__status i { display: inline-block; width: 7px; height: 7px; margin-right: 7px; border-radius: 50%; background: var(--accent-green); }
.hololake-home__signal-grid, .hololake-home__module-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.hololake-home__signal-grid article, .hololake-module { border: 1px solid rgba(157, 192, 244, .2); border-radius: 14px; background: linear-gradient(145deg, rgba(30, 57, 105, .55), rgba(12, 25, 51, .78)); box-shadow: inset 0 1px rgba(255,255,255,.06); }
.hololake-home__signal-grid article { padding: 18px; }.hololake-home__signal-grid span, .hololake-home__signal-grid small { display: block; color: #91a9ce; font-size: 11px; }.hololake-home__signal-grid strong { display: block; margin: 10px 0 6px; font-size: 15px; }
.hololake-home__modules { margin-top: 52px; }.hololake-home__section-head { display: flex; align-items: end; justify-content: space-between; margin-bottom: 14px; }.hololake-home__section-head p { margin: 0; color: #91a9ce; font-size: 13px; }
.hololake-module { min-height: 215px; padding: 22px; display: flex; flex-direction: column; }.hololake-module__number { color: #78aaf7; font-family: monospace; font-size: 12px; }.hololake-module h2 { margin: auto 0 8px; font-size: 22px; font-weight: 600; }.hololake-module p { margin: 0; color: #aebed7; font-size: 13px; line-height: 1.7; }.hololake-module button { margin-top: 19px; padding: 0; border: 0; color: #c8dcff; background: transparent; text-align: left; font: inherit; cursor: pointer; }.hololake-module__soon { margin-top: 19px; color: #7185a8; font-size: 12px; }
.hololake-home__signal-grid article, .hololake-module { border: 1px solid var(--border-default); border-radius: 8px; background: var(--surface-card); }
.hololake-home__signal-grid article { padding: 18px; }.hololake-home__signal-grid span, .hololake-home__signal-grid small { display: block; color: var(--text-secondary); font-size: 11px; }.hololake-home__signal-grid strong { display: block; margin: 10px 0 6px; font-size: 15px; }
.hololake-home__modules { margin-top: 44px; }.hololake-home__section-head { display: flex; align-items: end; justify-content: space-between; margin-bottom: 14px; }.hololake-home__section-head p { margin: 0; color: var(--text-secondary); font-size: 13px; }
.hololake-module { min-height: 185px; padding: 20px; display: flex; flex-direction: column; }.hololake-module__number { color: var(--accent-blue); font-family: monospace; font-size: 12px; }.hololake-module h2 { margin: auto 0 8px; font-size: 20px; font-weight: 600; }.hololake-module p { margin: 0; color: var(--text-secondary); font-size: 13px; line-height: 1.7; }.hololake-module button { margin-top: 19px; padding: 0; border: 0; color: var(--accent-blue); background: transparent; text-align: left; font: inherit; cursor: pointer; }.hololake-module__soon { margin-top: 19px; color: var(--text-muted); font-size: 12px; }
@media (max-width: 780px) { .hololake-home__signal-grid, .hololake-home__module-grid { grid-template-columns: 1fr; }.hololake-bar__context { display: none; }.hololake-home { padding: 28px; } }
.app {
@ -82,7 +51,7 @@
min-width: 220px;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, #10254a, var(--surface-sidebar) 40%, #08152c);
background: var(--surface-sidebar);
}
.app__sidebar > * {
@ -100,8 +69,7 @@
flex-direction: column;
position: relative;
z-index: 20;
background: linear-gradient(180deg, #0d2040, #091730 48%, #08152c);
box-shadow: 14px 0 30px rgba(1, 7, 20, .36);
background: var(--surface-sidebar);
}
.app__note-list > * {
@ -118,26 +86,7 @@
flex-direction: column;
container-type: inline-size;
container-name: editor;
position: relative;
background:
linear-gradient(var(--guanghu-grid) 1px, transparent 1px),
linear-gradient(90deg, var(--guanghu-grid) 1px, transparent 1px),
color-mix(in srgb, var(--surface-editor) 94%, var(--guanghu-glow));
background-size: 32px 32px, 32px 32px, auto;
}
.app__editor::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(180deg, color-mix(in srgb, var(--surface-editor) 28%, transparent), transparent 13rem);
z-index: 0;
}
.app__editor > * {
position: relative;
z-index: 1;
background: var(--surface-editor);
}
.app__editor > * {

View File

@ -22,6 +22,7 @@ import { McpSetupDialog } from './components/McpSetupDialog'
import { NoteRetargetingDialogs } from './components/note-retargeting/NoteRetargetingDialogs'
import { StartupScreen } from './components/StartupScreen'
import { HoloLakeHome } from './components/HoloLakeHome'
import { GUANGHU_THEME_LABELS } from './lib/guanghuTheme'
import { useAiAgentsOnboarding } from './hooks/useAiAgentsOnboarding'
import { useAiAgentsStatus } from './hooks/useAiAgentsStatus'
import { useVaultAiGuidanceStatus } from './hooks/useVaultAiGuidanceStatus'
@ -42,6 +43,7 @@ import { useVaultSwitcher } from './hooks/useVaultSwitcher'
import { useGitHistory } from './hooks/useGitHistory'
import { useUpdater, restartApp } from './hooks/useUpdater'
import { useAutoSync } from './hooks/useAutoSync'
import { useGuanghuTheme } from './hooks/useGuanghuTheme'
import { useConflictResolver } from './hooks/useConflictResolver'
import { useVaultConfig } from './hooks/useVaultConfig'
import { useOnboarding } from './hooks/useOnboarding'
@ -166,7 +168,8 @@ function App() {
function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | null }) {
const aiWorkspaceWindow = false
const [showHoloLakeHome, setShowHoloLakeHome] = useState(true)
const [showHoloLakeHome, setShowHoloLakeHome] = useState(false)
const { theme: guanghuTheme, cycleTheme } = useGuanghuTheme()
const [selection, setSelection] = useState<SidebarSelection>(DEFAULT_SELECTION)
const [noteListFilter, setNoteListFilter] = useState<NoteListFilter>('open')
const [pendingNoteListPdfExportPath, setPendingNoteListPdfExportPath] = useState<string | null>(null)
@ -1628,6 +1631,7 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
<span className="hololake-bar__line" />
<span className="hololake-bar__context"></span>
<button className="hololake-bar__library" onClick={() => setShowHoloLakeHome(false)}></button>
<button className="hololake-bar__theme" onClick={cycleTheme} title="切换光湖配套主题">{GUANGHU_THEME_LABELS[guanghuTheme]}</button>
</header>
<div className="app">
{sidebarVisible && (

View File

@ -24,36 +24,23 @@
.guanghu-portal-card {
width: 100%;
margin: 1.1rem 0;
padding: 1.15rem 1.25rem 1.2rem;
border: 1px solid color-mix(in srgb, var(--colors-accent) 46%, var(--colors-border));
border-radius: 1rem;
background:
linear-gradient(135deg, color-mix(in srgb, var(--colors-accent) 16%, transparent), transparent 42%),
radial-gradient(circle at 92% 16%, color-mix(in srgb, var(--colors-accent) 25%, transparent), transparent 36%),
var(--guanghu-card-highlight, var(--colors-surface));
box-shadow:
0 0.8rem 2.2rem color-mix(in srgb, black 14%, transparent),
inset 0 1px 0 color-mix(in srgb, white 28%, transparent);
overflow: hidden;
position: relative;
transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
margin: .75rem 0;
padding: 1rem 1.1rem;
border: 1px solid color-mix(in srgb, var(--colors-accent) 32%, var(--colors-border));
border-radius: .65rem;
background: var(--colors-surface);
}
.guanghu-portal-card::before {
content: '';
position: absolute;
inset: 0 auto 0 0;
width: 3px;
background: linear-gradient(var(--colors-accent), color-mix(in srgb, var(--colors-accent) 12%, transparent));
width: 2px;
background: var(--colors-accent);
}
.guanghu-portal-card:hover {
border-color: color-mix(in srgb, var(--colors-accent) 72%, var(--colors-border));
box-shadow:
0 1rem 2.6rem color-mix(in srgb, var(--colors-accent) 18%, transparent),
inset 0 1px 0 color-mix(in srgb, white 32%, transparent);
transform: translateY(-1px);
border-color: color-mix(in srgb, var(--colors-accent) 58%, var(--colors-border));
}
.guanghu-portal-card__eyebrow {
@ -215,23 +202,19 @@ body.mac-chrome .app:not(:has(.app__sidebar)):not(:has(.app__note-list)) .breadc
.editor__blocknote-container .bn-editor {
width: 100%;
padding: 36px 0 72px;
padding: 20px 0 48px;
}
.editor__blocknote-container .bn-editor > .bn-block-group {
padding: 1.25rem clamp(1rem, 3vw, 2.25rem) 3rem;
border: 1px solid color-mix(in srgb, var(--border-primary) 82%, transparent);
border-radius: 1.25rem;
background: color-mix(in srgb, var(--surface-editor) 86%, transparent);
box-shadow: 0 1.3rem 3.5rem color-mix(in srgb, black 8%, transparent);
backdrop-filter: blur(16px);
padding: 0 clamp(1rem, 3vw, 2.25rem) 2rem;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
backdrop-filter: none;
}
@media (max-width: 720px) {
.app__editor {
background-size: 22px 22px, 22px 22px, auto;
}
.editor__blocknote-container .bn-editor > .bn-block-group {
border-inline: 0;
border-radius: 0;

View File

@ -0,0 +1,30 @@
import { useCallback, useEffect, useState } from 'react'
import {
GUANGHU_THEMES,
GUANGHU_THEME_STORAGE_KEY,
readGuanghuTheme,
writeGuanghuTheme,
type GuanghuTheme,
} from '../lib/guanghuTheme'
export function useGuanghuTheme(): { theme: GuanghuTheme; cycleTheme: () => void } {
const [theme, setTheme] = useState<GuanghuTheme>(() => (
typeof window === 'undefined' ? 'native' : readGuanghuTheme(window.localStorage)
))
useEffect(() => {
document.documentElement.setAttribute('data-guanghu-theme', theme)
writeGuanghuTheme(window.localStorage, theme)
}, [theme])
const cycleTheme = useCallback(() => {
setTheme(current => {
const index = GUANGHU_THEMES.indexOf(current)
return GUANGHU_THEMES[(index + 1) % GUANGHU_THEMES.length]
})
}, [])
return { theme, cycleTheme }
}
export { GUANGHU_THEME_STORAGE_KEY }

View File

@ -180,11 +180,10 @@
--link-color: var(--accent-blue);
--link-hover: var(--accent-blue-hover);
/* Guanghu visual layer — an app-owned, ice-lake palette. */
--guanghu-aurora: rgba(91, 181, 255, 0.2);
--guanghu-glow: rgba(38, 131, 235, 0.15);
--guanghu-grid: rgba(42, 113, 188, 0.06);
--guanghu-card-highlight: rgba(255, 255, 255, 0.76);
/* Guanghu theme extension: palettes only; layout stays Tolaria-native. */
--guanghu-aurora: transparent;
--guanghu-glow: transparent;
--guanghu-grid: transparent;
}
:root.dark,
@ -337,11 +336,68 @@
--link-color: var(--accent-blue);
--link-hover: var(--accent-blue-hover);
/* Guanghu visual layer — deep lake, not a flat black canvas. */
--guanghu-aurora: rgba(76, 174, 255, 0.2);
--guanghu-glow: rgba(20, 118, 214, 0.24);
--guanghu-grid: rgba(126, 202, 255, 0.055);
--guanghu-card-highlight: rgba(28, 54, 80, 0.72);
/* Guanghu theme extension: palettes only; layout stays Tolaria-native. */
--guanghu-aurora: transparent;
--guanghu-glow: transparent;
--guanghu-grid: transparent;
}
/* Guanghu companion presets. They change semantic colors only; native
Tolaria layout, editor surfaces and interaction density remain intact. */
[data-guanghu-theme="lake-light"][data-theme="light"] {
--surface-app: #f6fbff;
--surface-sidebar: #f0f6fa;
--surface-panel: #ffffff;
--surface-card: #ffffff;
--surface-popover: #ffffff;
--surface-editor: #ffffff;
--accent-blue: #2b6ea6;
--accent-blue-hover: #21577f;
--accent-blue-bg: rgba(43, 110, 166, .12);
--accent-blue-light: rgba(43, 110, 166, .08);
--state-selected: #e4f1f8;
--state-selected-strong: #d7eaf4;
--state-active: #e4f1f8;
--border-focus: #2b6ea6;
}
[data-guanghu-theme="lake-dark"][data-theme="dark"] {
--surface-app: #15191d;
--surface-sidebar: #1b2025;
--surface-panel: #20262c;
--surface-card: #242c33;
--surface-popover: #292f35;
--surface-editor: #15191d;
--text-primary: #e6edf2;
--text-secondary: #aab8c2;
--text-heading: #f1f6f8;
--accent-blue: #72b7d6;
--accent-blue-hover: #98cee3;
--accent-blue-bg: rgba(114, 183, 214, .16);
--accent-blue-light: rgba(114, 183, 214, .1);
--state-hover: #2a333b;
--state-hover-subtle: #252d34;
--state-selected: #29414d;
--state-selected-strong: #315565;
--state-active: #29414d;
--border-focus: #72b7d6;
}
[data-guanghu-theme="lake-light"][data-theme="dark"] {
--surface-app: #17212a;
--surface-sidebar: #1b2833;
--surface-panel: #21313d;
--surface-card: #263a48;
--surface-popover: #2c4352;
--surface-editor: #17212a;
--accent-blue: #8bc9e5;
--accent-blue-hover: #b0def0;
--accent-blue-bg: rgba(139, 201, 229, .16);
--accent-blue-light: rgba(139, 201, 229, .1);
--state-selected: #294858;
--state-selected-strong: #31586b;
--state-active: #294858;
--border-focus: #8bc9e5;
}
/* --- Tailwind v4 theme inline: register colors + radii --- */

View File

@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest'
import {
GUANGHU_THEMES,
normalizeGuanghuTheme,
} from './guanghuTheme'
describe('guanghuTheme', () => {
it('keeps the companion theme choices explicit and stable', () => {
expect(GUANGHU_THEMES).toEqual(['native', 'lake-light', 'lake-dark'])
})
it('falls back to the native Tolaria theme for unknown values', () => {
expect(normalizeGuanghuTheme('old-blue-skin')).toBe('native')
expect(normalizeGuanghuTheme(null)).toBe('native')
})
it('accepts every supported companion theme', () => {
expect(normalizeGuanghuTheme('lake-light')).toBe('lake-light')
expect(normalizeGuanghuTheme('lake-dark')).toBe('lake-dark')
})
})

32
src/lib/guanghuTheme.ts Normal file
View File

@ -0,0 +1,32 @@
export const GUANGHU_THEME_STORAGE_KEY = 'guanghu-theme'
export const GUANGHU_THEMES = ['native', 'lake-light', 'lake-dark'] as const
export type GuanghuTheme = typeof GUANGHU_THEMES[number]
export const GUANGHU_THEME_LABELS: Record<GuanghuTheme, string> = {
native: '原生',
'lake-light': '湖光浅色',
'lake-dark': '湖心深色',
}
export function normalizeGuanghuTheme(value: unknown): GuanghuTheme {
return typeof value === 'string' && (GUANGHU_THEMES as readonly string[]).includes(value)
? value as GuanghuTheme
: 'native'
}
export function readGuanghuTheme(storage: Pick<Storage, 'getItem'>): GuanghuTheme {
try {
return normalizeGuanghuTheme(storage.getItem(GUANGHU_THEME_STORAGE_KEY))
} catch {
return 'native'
}
}
export function writeGuanghuTheme(storage: Pick<Storage, 'setItem'>, theme: GuanghuTheme): void {
try {
storage.setItem(GUANGHU_THEME_STORAGE_KEY, theme)
} catch {
// Storage may be unavailable in restricted browser contexts.
}
}