From 4738045c9c64d77a0385fddc166e183d0813ae6d Mon Sep 17 00:00:00 2001 From: Boof2015 <75185879+Boof2015@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:37:07 -0400 Subject: [PATCH] iro file theme updates --- src/main/themeLibrary.ts | 10 +- src/renderer/components/AstraScopeModule.tsx | 5 + src/renderer/components/DialogApp.tsx | 14 +- src/renderer/styles/globals.css | 18 +- src/shared/themeState.ts | 197 ++++++++++++------- test/theme-library.test.ts | 91 +++++++++ 6 files changed, 249 insertions(+), 86 deletions(-) diff --git a/src/main/themeLibrary.ts b/src/main/themeLibrary.ts index d3c56ab..ff8e221 100644 --- a/src/main/themeLibrary.ts +++ b/src/main/themeLibrary.ts @@ -207,10 +207,16 @@ export class FileBackedThemeLibrary { private async ensureTemplateFile(): Promise { 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 { diff --git a/src/renderer/components/AstraScopeModule.tsx b/src/renderer/components/AstraScopeModule.tsx index a8f5774..68d6f02 100644 --- a/src/renderer/components/AstraScopeModule.tsx +++ b/src/renderer/components/AstraScopeModule.tsx @@ -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, diff --git a/src/renderer/components/DialogApp.tsx b/src/renderer/components/DialogApp.tsx index 51a930f..bed3c3a 100644 --- a/src/renderer/components/DialogApp.tsx +++ b/src/renderer/components/DialogApp.tsx @@ -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); } `} diff --git a/src/renderer/styles/globals.css b/src/renderer/styles/globals.css index 6692a15..ecdbc19 100644 --- a/src/renderer/styles/globals.css +++ b/src/renderer/styles/globals.css @@ -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 { diff --git a/src/shared/themeState.ts b/src/shared/themeState.ts index 4f3273c..29a523e 100644 --- a/src/shared/themeState.ts +++ b/src/shared/themeState.ts @@ -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>) + + 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>) + + const scopesSection = serializeSection('Scopes', { ...base.scopes }, SCOPES_SCHEMA as SectionSchema>) + + 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>) + + const oscilloscopeSection = serializeSection('Oscilloscope', { + line: base.oscilloscope.line, + fill: base.oscilloscope.fill, + }, OSCILLOSCOPE_SCHEMA as SectionSchema>) + + 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>) + + const spectrogramSection = serializeSection('Spectrogram', { + mono: base.spectrogram.mono, + heatLow: base.spectrogram.heatLow, + heatMid: base.spectrogram.heatMid, + heatHigh: base.spectrogram.heatHigh, + }, SPECTROGRAM_SCHEMA as SectionSchema>) + + 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>) + + 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>) + + const waveformSection = serializeSection('Waveform', { + line: base.waveform.line, + bandLow: base.waveform.bandLow, + bandMid: base.waveform.bandMid, + bandHigh: base.waveform.bandHigh, + }, WAVEFORM_SCHEMA as SectionSchema>) + + const astraSection = serializeSection('Astra', { ...base.astra }, ASTRA_SCHEMA as SectionSchema>) 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', } } diff --git a/test/theme-library.test.ts b/test/theme-library.test.ts index da82f6b..12971e3 100644 --- a/test/theme-library.test.ts +++ b/test/theme-library.test.ts @@ -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()