mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-12 05:10:51 +02:00
iro file theme updates
This commit is contained in:
@@ -207,10 +207,16 @@ export class FileBackedThemeLibrary {
|
||||
|
||||
private async ensureTemplateFile(): Promise<void> {
|
||||
const targetPath = resolve(join(this.themesDir, TEMPLATE_THEME_FILE_NAME))
|
||||
const nextContent = createTemplateThemeFile()
|
||||
|
||||
if (await this.pathExists(targetPath)) {
|
||||
return
|
||||
const currentContent = await readFile(targetPath, 'utf8')
|
||||
if (currentContent === nextContent) {
|
||||
return
|
||||
}
|
||||
}
|
||||
await writeFile(targetPath, createTemplateThemeFile(), 'utf8')
|
||||
|
||||
await writeFile(targetPath, nextContent, 'utf8')
|
||||
}
|
||||
|
||||
private async readManagedEntries(): Promise<ManagedThemeEntry[]> {
|
||||
|
||||
@@ -101,6 +101,11 @@ export default function AstraScopeModule({
|
||||
'--astra-bg': theme.background,
|
||||
'--astra-surface': theme.surface,
|
||||
'--astra-border': theme.border,
|
||||
'--astra-button-bg': theme.buttonBg,
|
||||
'--astra-button-bg-hover': theme.buttonBgHover,
|
||||
'--astra-button-bg-active': theme.buttonBgActive,
|
||||
'--astra-button-border': theme.buttonBorder,
|
||||
'--astra-button-text': theme.buttonText,
|
||||
'--astra-text': theme.text,
|
||||
'--astra-subtext': theme.subtext,
|
||||
'--astra-progress-track': theme.progressTrack,
|
||||
|
||||
@@ -201,24 +201,24 @@ export default function DialogApp(): JSX.Element {
|
||||
}
|
||||
|
||||
.dialog-btn--primary {
|
||||
background: rgba(var(--accent-rgb), 0.18);
|
||||
border-color: rgba(var(--accent-rgb), 0.32);
|
||||
background: var(--control-bg-active);
|
||||
border-color: var(--control-border-active);
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
.dialog-btn--primary:hover {
|
||||
background: rgba(var(--accent-rgb), 0.26);
|
||||
border-color: rgba(var(--accent-rgb), 0.48);
|
||||
background: color-mix(in srgb, var(--control-bg-active) 72%, var(--accent) 28%);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.dialog-btn--danger {
|
||||
background: rgba(248, 113, 113, 0.12);
|
||||
border-color: rgba(248, 113, 113, 0.28);
|
||||
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
||||
border-color: color-mix(in srgb, var(--danger) 28%, transparent);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.dialog-btn--danger:hover {
|
||||
background: rgba(248, 113, 113, 0.2);
|
||||
background: color-mix(in srgb, var(--danger) 20%, transparent);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
|
||||
@@ -241,11 +241,11 @@ select {
|
||||
}
|
||||
|
||||
.app-banner--error {
|
||||
border-color: rgba(248, 113, 113, 0.32);
|
||||
border-color: color-mix(in srgb, var(--danger) 32%, transparent);
|
||||
}
|
||||
|
||||
.app-banner--info {
|
||||
border-color: rgba(var(--accent-rgb), 0.24);
|
||||
border-color: var(--control-border-active);
|
||||
}
|
||||
|
||||
.app-banner__message {
|
||||
@@ -814,9 +814,9 @@ select {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--control-border);
|
||||
background: var(--control-bg);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--astra-button-border);
|
||||
background: var(--astra-button-bg);
|
||||
color: var(--astra-button-text);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
@@ -831,14 +831,14 @@ select {
|
||||
|
||||
.astra-scope__control:hover:not(:disabled),
|
||||
.astra-scope__control:focus-visible {
|
||||
background: var(--control-bg-hover);
|
||||
border-color: var(--control-border-active);
|
||||
color: var(--text-primary);
|
||||
background: var(--astra-button-bg-hover);
|
||||
border-color: var(--astra-accent);
|
||||
color: var(--astra-button-text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.astra-scope__control:active:not(:disabled) {
|
||||
background: var(--control-bg-active);
|
||||
background: var(--astra-button-bg-active);
|
||||
}
|
||||
|
||||
.astra-scope__control:disabled {
|
||||
|
||||
+129
-68
@@ -765,6 +765,11 @@ function parseThemeContent(content: string, fallbackId: string, fallbackName: st
|
||||
const tokenKey = parseSectionValue(currentSection, key)
|
||||
if (!tokenKey) continue
|
||||
|
||||
if (PASSTHROUGH_KEYS.has(tokenKey)) {
|
||||
getSectionTokenRecord(nextTheme, currentSection)[tokenKey] = value
|
||||
continue
|
||||
}
|
||||
|
||||
const parsedColor = parseCssColor(value) ?? parseThemeChannelColor(value)
|
||||
if (!parsedColor) continue
|
||||
getSectionTokenRecord(nextTheme, currentSection)[tokenKey] = toCssColor(quantizeThemeColor(parsedColor))
|
||||
@@ -829,83 +834,139 @@ export function serializeThemeFile(theme: PrismTheme): string {
|
||||
|
||||
export function createTemplateThemeFile(): string {
|
||||
const base = createDefaultTheme()
|
||||
const resolved = resolveTheme(base)
|
||||
const themeSection = [
|
||||
'[Theme]',
|
||||
`format = ${THEME_FILE_FORMAT}`,
|
||||
`version = ${THEME_FILE_VERSION}`,
|
||||
'id = theme_template',
|
||||
'name = Template Theme',
|
||||
'credit = Your Name',
|
||||
'website = https://example.com',
|
||||
'description = Custom Prism theme',
|
||||
]
|
||||
|
||||
const template: PrismTheme = {
|
||||
id: 'theme_template',
|
||||
name: 'Template Theme',
|
||||
credit: 'Your Name',
|
||||
website: 'https://example.com',
|
||||
description: 'Custom Prism theme',
|
||||
app: {
|
||||
...base.app,
|
||||
toolbarBg: resolved.interface.toolbarBg,
|
||||
settingsBgTop: resolved.interface.settingsBgTop,
|
||||
settingsBgBottom: resolved.interface.settingsBgBottom,
|
||||
bottomBarBg: resolved.interface.bottomBarBg,
|
||||
},
|
||||
controls: { ...base.controls },
|
||||
scopes: { ...base.scopes },
|
||||
spectrum: {
|
||||
...base.spectrum,
|
||||
background: resolved.spectrum.background,
|
||||
guides: resolved.spectrum.guides,
|
||||
labels: resolved.spectrum.labels,
|
||||
heatBase: resolved.spectrum.heatBase,
|
||||
},
|
||||
oscilloscope: {
|
||||
...base.oscilloscope,
|
||||
background: resolved.oscilloscope.background,
|
||||
guides: resolved.oscilloscope.guides,
|
||||
},
|
||||
vectorscope: {
|
||||
...base.vectorscope,
|
||||
background: resolved.vectorscope.background,
|
||||
guides: resolved.vectorscope.guides,
|
||||
labels: resolved.vectorscope.labels,
|
||||
},
|
||||
spectrogram: {
|
||||
...base.spectrogram,
|
||||
background: resolved.spectrogram.background,
|
||||
},
|
||||
vumeter: {
|
||||
...base.vumeter,
|
||||
background: resolved.vumeter.background,
|
||||
scale: resolved.vumeter.scale,
|
||||
labels: resolved.vumeter.labels,
|
||||
},
|
||||
lufsmeter: {
|
||||
...base.lufsmeter,
|
||||
background: resolved.lufsmeter.background,
|
||||
scale: resolved.lufsmeter.scale,
|
||||
labels: resolved.lufsmeter.labels,
|
||||
},
|
||||
waveform: {
|
||||
...base.waveform,
|
||||
background: resolved.waveform.background,
|
||||
guides: resolved.waveform.guides,
|
||||
},
|
||||
astra: { ...base.astra },
|
||||
}
|
||||
const appSection = serializeSection('App', {
|
||||
accent: base.app.accent,
|
||||
success: base.app.success,
|
||||
warning: base.app.warning,
|
||||
danger: base.app.danger,
|
||||
background: base.app.background,
|
||||
surface: base.app.surface,
|
||||
surfaceAlt: base.app.surfaceAlt,
|
||||
border: base.app.border,
|
||||
text: base.app.text,
|
||||
textMuted: base.app.textMuted,
|
||||
}, APP_SCHEMA as SectionSchema<Record<string, string | undefined>>)
|
||||
|
||||
appSection.push(
|
||||
'# Optional shell overrides:',
|
||||
`# toolbar_bg = ${toThemeChannels(withAlpha(base.app.surfaceAlt ?? 'rgba(4, 8, 12, 0.98)', 0.78))}`,
|
||||
`# settings_bg_top = ${toThemeChannels(base.app.surface ?? 'rgba(8, 11, 16, 0.92)')}`,
|
||||
`# settings_bg_bottom = ${toThemeChannels(base.app.surfaceAlt ?? 'rgba(4, 8, 12, 0.98)')}`,
|
||||
`# bottom_bar_bg = ${toThemeChannels(withAlpha(base.app.surfaceAlt ?? 'rgba(4, 8, 12, 0.98)', 0.98))}`,
|
||||
)
|
||||
|
||||
const controlsSection = serializeSection('Controls', {
|
||||
...base.controls,
|
||||
flatControls: 'false',
|
||||
}, CONTROLS_SCHEMA as SectionSchema<Record<string, string | undefined>>)
|
||||
|
||||
const scopesSection = serializeSection('Scopes', { ...base.scopes }, SCOPES_SCHEMA as SectionSchema<Record<string, string | undefined>>)
|
||||
|
||||
const spectrumSection = serializeSection('Spectrum', {
|
||||
line: base.spectrum.line,
|
||||
sideLine: base.spectrum.sideLine,
|
||||
fill: base.spectrum.fill,
|
||||
heatLow: base.spectrum.heatLow,
|
||||
heatMid: base.spectrum.heatMid,
|
||||
heatHigh: base.spectrum.heatHigh,
|
||||
heatBase: base.scopes.background,
|
||||
}, SPECTRUM_SCHEMA as SectionSchema<Record<string, string | undefined>>)
|
||||
|
||||
const oscilloscopeSection = serializeSection('Oscilloscope', {
|
||||
line: base.oscilloscope.line,
|
||||
fill: base.oscilloscope.fill,
|
||||
}, OSCILLOSCOPE_SCHEMA as SectionSchema<Record<string, string | undefined>>)
|
||||
|
||||
const vectorscopeSection = serializeSection('Vectorscope', {
|
||||
trace: base.vectorscope.trace,
|
||||
bandLow: base.vectorscope.bandLow,
|
||||
bandMid: base.vectorscope.bandMid,
|
||||
bandHigh: base.vectorscope.bandHigh,
|
||||
labels: base.scopes.guides,
|
||||
}, VECTORSCOPE_SCHEMA as SectionSchema<Record<string, string | undefined>>)
|
||||
|
||||
const spectrogramSection = serializeSection('Spectrogram', {
|
||||
mono: base.spectrogram.mono,
|
||||
heatLow: base.spectrogram.heatLow,
|
||||
heatMid: base.spectrogram.heatMid,
|
||||
heatHigh: base.spectrogram.heatHigh,
|
||||
}, SPECTROGRAM_SCHEMA as SectionSchema<Record<string, string | undefined>>)
|
||||
|
||||
const vumeterSection = serializeSection('VUMeter', {
|
||||
level: base.vumeter.level,
|
||||
track: base.vumeter.track,
|
||||
peak: base.vumeter.peak,
|
||||
clip: base.vumeter.clip,
|
||||
scale: base.scopes.guides,
|
||||
labels: blendText(base.app.text ?? 'rgb(255, 255, 255)', base.app.textMuted ?? 'rgba(255, 255, 255, 0.42)', 0.35),
|
||||
}, VUMETER_SCHEMA as SectionSchema<Record<string, string | undefined>>)
|
||||
|
||||
const lufsmeterSection = serializeSection('LUFSMeter', {
|
||||
level: base.lufsmeter.level,
|
||||
track: base.lufsmeter.track,
|
||||
target: base.lufsmeter.target,
|
||||
scale: base.scopes.guides,
|
||||
labels: blendText(base.app.text ?? 'rgb(255, 255, 255)', base.app.textMuted ?? 'rgba(255, 255, 255, 0.42)', 0.2),
|
||||
}, LUFSMETER_SCHEMA as SectionSchema<Record<string, string | undefined>>)
|
||||
|
||||
const waveformSection = serializeSection('Waveform', {
|
||||
line: base.waveform.line,
|
||||
bandLow: base.waveform.bandLow,
|
||||
bandMid: base.waveform.bandMid,
|
||||
bandHigh: base.waveform.bandHigh,
|
||||
}, WAVEFORM_SCHEMA as SectionSchema<Record<string, string | undefined>>)
|
||||
|
||||
const astraSection = serializeSection('Astra', { ...base.astra }, ASTRA_SCHEMA as SectionSchema<Record<string, string | undefined>>)
|
||||
|
||||
return `# Prism theme template
|
||||
#
|
||||
# Colors use R, G, B or R, G, B, A (0-255)
|
||||
# CSS colors like #hex, rgb(), and rgba() also work
|
||||
#
|
||||
# ── UI ──
|
||||
# [App] Window background, text, and accent color
|
||||
# [Controls] Buttons, inputs, menus, and sliders
|
||||
# Set flat_controls = true to disable glass highlights/gradients
|
||||
# Theme metadata:
|
||||
# [Theme]
|
||||
#
|
||||
# ── Scopes ──
|
||||
# [Scopes] Shared defaults for all scopes (background, guides, overlays)
|
||||
# Core UI:
|
||||
# [App] Main palette for the window shell and text
|
||||
# [Controls] Buttons, inputs, menus, sliders, and control chrome
|
||||
#
|
||||
# Each scope section below has FULL control over its own colors.
|
||||
# Per-scope values override the shared [Scopes] defaults.
|
||||
# Every token shown below can be changed independently.
|
||||
# Shared scope defaults:
|
||||
# [Scopes] Background, guides, and overlays shared by all scopes
|
||||
#
|
||||
${serializeThemeFile(template)}`
|
||||
# Module overrides:
|
||||
# Add tokens inside each module section only when you want that module to
|
||||
# diverge from the shared defaults. Background/guides can still be added to
|
||||
# individual sections later even if they are not shown in this template.
|
||||
#
|
||||
# Set flat_controls = true to disable glass highlights/gradients.
|
||||
#
|
||||
${[
|
||||
themeSection.join('\n'),
|
||||
appSection.join('\n'),
|
||||
controlsSection.join('\n'),
|
||||
scopesSection.join('\n'),
|
||||
'# Module overrides',
|
||||
spectrumSection.join('\n'),
|
||||
oscilloscopeSection.join('\n'),
|
||||
vectorscopeSection.join('\n'),
|
||||
spectrogramSection.join('\n'),
|
||||
vumeterSection.join('\n'),
|
||||
lufsmeterSection.join('\n'),
|
||||
waveformSection.join('\n'),
|
||||
astraSection.join('\n'),
|
||||
].join('\n\n')}
|
||||
`
|
||||
}
|
||||
|
||||
interface ResolvedAppTokens {
|
||||
@@ -992,7 +1053,7 @@ function resolveControlsTokens(tokens: ThemeControlsTokens, app: ResolvedAppToke
|
||||
menuSurface: tokens.menuSurface ?? app.surface,
|
||||
menuBorder: tokens.menuBorder ?? 'rgba(255, 255, 255, 0.1)',
|
||||
slider: tokens.slider ?? withAlpha(app.accent, 0.82),
|
||||
flatControls: tokens.flatControls === 'true',
|
||||
flatControls: tokens.flatControls?.trim().toLowerCase() === 'true',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { FileBackedThemeLibrary } from '../src/main/themeLibrary'
|
||||
import {
|
||||
createBundledThemes,
|
||||
createDefaultTheme,
|
||||
createTemplateThemeFile,
|
||||
createMigratedAccentTheme,
|
||||
parseThemeFileContent,
|
||||
resolveTheme,
|
||||
@@ -42,6 +43,7 @@ test('theme files round-trip and keep grouped sections intact', () => {
|
||||
const theme = createDefaultTheme()
|
||||
theme.app.accent = '#4ade80'
|
||||
theme.controls.menuSurface = 'rgb(12, 18, 32)'
|
||||
theme.controls.flatControls = 'true'
|
||||
theme.scopes.background = '#030712'
|
||||
theme.spectrum.heatMid = 'rgb(200, 50, 120)'
|
||||
theme.vumeter.track = '#111827'
|
||||
@@ -54,17 +56,39 @@ test('theme files round-trip and keep grouped sections intact', () => {
|
||||
assert.match(serialized, /\[App\]/)
|
||||
assert.match(serialized, /\[Controls\]/)
|
||||
assert.match(serialized, /\[Scopes\]/)
|
||||
assert.match(serialized, /flat_controls = true/)
|
||||
|
||||
assert.equal(parsed.id, DEFAULT_THEME_ID)
|
||||
assert.equal(parsed.name, DEFAULT_THEME_NAME)
|
||||
assert.equal(parsed.app.accent, 'rgb(74, 222, 128)')
|
||||
assert.equal(parsed.controls.menuSurface, 'rgb(12, 18, 32)')
|
||||
assert.equal(parsed.controls.flatControls, 'true')
|
||||
assert.equal(parsed.scopes.background, 'rgb(3, 7, 18)')
|
||||
assert.equal(parsed.spectrum.heatMid, 'rgb(200, 50, 120)')
|
||||
assert.equal(parsed.vumeter.track, 'rgb(17, 24, 39)')
|
||||
assert.equal(parsed.astra.background, theme.astra.background)
|
||||
})
|
||||
|
||||
test('parseThemeFileContent preserves passthrough controls tokens from .iro files', () => {
|
||||
const parsed = parseThemeFileContent(`
|
||||
[Theme]
|
||||
format = prism-theme
|
||||
version = 2
|
||||
id = theme_flat
|
||||
name = Flat
|
||||
|
||||
[Controls]
|
||||
flat_controls = true
|
||||
`, 'theme_flat', 'Flat')
|
||||
|
||||
const resolved = resolveTheme(parsed)
|
||||
|
||||
assert.equal(parsed.controls.flatControls, 'true')
|
||||
assert.equal(resolved.interface.glassBg, 'transparent')
|
||||
assert.equal(resolved.interface.glassHighlight, 'transparent')
|
||||
assert.equal(resolved.interface.glassHighlightStrong, 'transparent')
|
||||
})
|
||||
|
||||
test('resolveTheme maps grouped app, controls, and scopes tokens into UI and scope surfaces', () => {
|
||||
const theme = createDefaultTheme()
|
||||
theme.app.accent = 'rgb(255, 159, 67)'
|
||||
@@ -134,6 +158,54 @@ test('themeToCssVariables exposes grouped UI, control, and scope variables', ()
|
||||
assert.equal(variables['--scope-resize-handle'], 'rgb(20, 21, 22)')
|
||||
})
|
||||
|
||||
test('createTemplateThemeFile presents a simplified recommended theme layout', () => {
|
||||
const template = createTemplateThemeFile()
|
||||
const parsed = parseThemeFileContent(template, 'theme_template', 'Template Theme')
|
||||
|
||||
assert.match(template, /# Core UI:/)
|
||||
assert.match(template, /# Shared scope defaults:/)
|
||||
assert.match(template, /# Module overrides:/)
|
||||
assert.match(template, /# Optional shell overrides:/)
|
||||
assert.match(template, /flat_controls = false/)
|
||||
assert.match(template, /# toolbar_bg =/)
|
||||
assert.doesNotMatch(template, /\[Spectrum\]\nbackground =/)
|
||||
assert.doesNotMatch(template, /\[Oscilloscope\]\nbackground =/)
|
||||
assert.doesNotMatch(template, /\[Waveform\]\nbackground =/)
|
||||
assert.equal(parsed.controls.flatControls, 'false')
|
||||
})
|
||||
|
||||
test('resolveTheme exposes Astra-specific button tokens and derived button states', () => {
|
||||
const theme = createDefaultTheme()
|
||||
theme.astra.accent = 'rgb(100, 150, 200)'
|
||||
theme.astra.text = 'rgb(240, 241, 242)'
|
||||
theme.astra.buttonSurface = 'rgba(20, 30, 40, 0.9)'
|
||||
theme.astra.buttonBorder = 'rgb(50, 60, 70)'
|
||||
|
||||
const resolved = resolveTheme(theme)
|
||||
|
||||
assert.equal(resolved.astra.buttonBg, 'rgba(20, 30, 40, 0.902)')
|
||||
assert.equal(resolved.astra.buttonBgHover, 'rgba(31, 47, 62, 0.94)')
|
||||
assert.equal(resolved.astra.buttonBgActive, 'rgba(100, 150, 200, 0.16)')
|
||||
assert.equal(resolved.astra.buttonBorder, 'rgb(50, 60, 70)')
|
||||
assert.equal(resolved.astra.buttonText, 'rgb(240, 241, 242)')
|
||||
})
|
||||
|
||||
test('Astra renderer consumes Astra-specific button theme vars', async () => {
|
||||
const componentSource = await readFile(join(process.cwd(), 'src', 'renderer', 'components', 'AstraScopeModule.tsx'), 'utf8')
|
||||
const stylesSource = await readFile(join(process.cwd(), 'src', 'renderer', 'styles', 'globals.css'), 'utf8')
|
||||
|
||||
assert.match(componentSource, /'--astra-button-bg': theme\.buttonBg/)
|
||||
assert.match(componentSource, /'--astra-button-bg-hover': theme\.buttonBgHover/)
|
||||
assert.match(componentSource, /'--astra-button-bg-active': theme\.buttonBgActive/)
|
||||
assert.match(componentSource, /'--astra-button-border': theme\.buttonBorder/)
|
||||
assert.match(componentSource, /'--astra-button-text': theme\.buttonText/)
|
||||
assert.match(stylesSource, /\.astra-scope__control \{[\s\S]*background: var\(--astra-button-bg\);/)
|
||||
assert.match(stylesSource, /\.astra-scope__control \{[\s\S]*border: 1px solid var\(--astra-button-border\);/)
|
||||
assert.match(stylesSource, /\.astra-scope__control \{[\s\S]*color: var\(--astra-button-text\);/)
|
||||
assert.match(stylesSource, /\.astra-scope__control:hover:not\(:disabled\),[\s\S]*background: var\(--astra-button-bg-hover\);/)
|
||||
assert.match(stylesSource, /\.astra-scope__control:active:not\(:disabled\) \{[\s\S]*background: var\(--astra-button-bg-active\);/)
|
||||
})
|
||||
|
||||
test('bundled and migrated accent themes recolor every accent-driven scope', () => {
|
||||
const purple = createBundledThemes().find((theme) => theme.id === 'theme_purple')
|
||||
assert.ok(purple)
|
||||
@@ -220,6 +292,25 @@ test('library refreshes shipped bundled themes when their definitions change', a
|
||||
}
|
||||
})
|
||||
|
||||
test('library refreshes the managed template when its generated layout changes', async () => {
|
||||
const harness = await createHarness()
|
||||
|
||||
try {
|
||||
await harness.library.getSnapshot()
|
||||
|
||||
await writeFile(join(harness.themesDir, '_TEMPLATE.iro'), '# stale template\n', 'utf8')
|
||||
|
||||
await harness.library.reloadThemes()
|
||||
|
||||
const templateContent = await readFile(join(harness.themesDir, '_TEMPLATE.iro'), 'utf8')
|
||||
assert.match(templateContent, /# Core UI:/)
|
||||
assert.match(templateContent, /flat_controls = false/)
|
||||
assert.doesNotMatch(templateContent, /\[Spectrum\]\nbackground =/)
|
||||
} finally {
|
||||
await harness.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test('legacy migration can create an accent theme and make it active', async () => {
|
||||
const harness = await createHarness()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user