iro file theme updates

This commit is contained in:
Boof2015
2026-04-05 22:37:07 -04:00
parent da1218de38
commit 4738045c9c
6 changed files with 249 additions and 86 deletions
+8 -2
View File
@@ -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,
+7 -7
View File
@@ -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>
+9 -9
View File
@@ -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
View File
@@ -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',
}
}
+91
View File
@@ -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()