9d816f0c86
L49: Remove newline from missing-binary error message in download.ts; inline
error spans render it as literal newline, now one sentence with period.
L53: backup.ts exportBackup/importBackup/showMessageBox no longer use win!
non-null assertions -- each uses a guarded ternary (same pattern index.ts
already used for chooseFolder).
L59: Extract PAGE_BACKGROUND to src/shared/theme.ts; main/index.ts and
renderer/src/theme.ts both import from it -- no more "keep in sync" comment.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
188 lines
5.3 KiB
TypeScript
188 lines
5.3 KiB
TypeScript
import { PAGE_BACKGROUND } from '@shared/theme'
|
|
import {
|
|
createLightTheme,
|
|
createDarkTheme,
|
|
type BrandVariants,
|
|
type Theme
|
|
} from '@fluentui/react-components'
|
|
import type { AccentColor } from '@shared/ipc'
|
|
|
|
// "Sunset-to-sea" accent family. All four ramps are built the same way — one
|
|
// shared lightness/saturation curve mapped onto Fluent's 16 BrandVariants stops:
|
|
// a deep near-black floor (idx 10), a white-text-safe LIGHT primary at idx 80, a
|
|
// bright DARK-mode lift at idx 110 (applied via buildDarkOverrides), and a near-
|
|
// white top (idx 160). The idx-80 primaries are tuned to equal luminance so none
|
|
// reads louder than the rest. Hues are three analogous warms — rose → coral →
|
|
// amber — plus teal as the cool complement that anchors the set.
|
|
const rose: BrandVariants = {
|
|
10: '#20090f',
|
|
20: '#3b0e1a',
|
|
30: '#581124',
|
|
40: '#75132d',
|
|
50: '#921637',
|
|
60: '#ae1941',
|
|
70: '#c91e4c',
|
|
80: '#dc2a5a',
|
|
90: '#e34670',
|
|
100: '#eb6186',
|
|
110: '#f17e9d',
|
|
120: '#f197af',
|
|
130: '#f0b0c1',
|
|
140: '#f0c9d3',
|
|
150: '#f2dfe4',
|
|
160: '#f8f2f3'
|
|
}
|
|
|
|
const coral: BrandVariants = {
|
|
10: '#210e08',
|
|
20: '#39150b',
|
|
30: '#541c0d',
|
|
40: '#6f230e',
|
|
50: '#8a2a0f',
|
|
60: '#a43111',
|
|
70: '#bc3915',
|
|
80: '#d3421a',
|
|
90: '#e85831',
|
|
100: '#f07554',
|
|
110: '#f5957a',
|
|
120: '#f4a893',
|
|
130: '#f3bdae',
|
|
140: '#f1d0c7',
|
|
150: '#f3e3de',
|
|
160: '#f8f3f1'
|
|
}
|
|
|
|
const amber: BrandVariants = {
|
|
10: '#221907',
|
|
20: '#342507',
|
|
30: '#463107',
|
|
40: '#593d06',
|
|
50: '#6b4905',
|
|
60: '#7d5506',
|
|
70: '#8d6007',
|
|
80: '#9c6b0a',
|
|
90: '#c8890a',
|
|
100: '#f4a60a',
|
|
110: '#f7b634',
|
|
120: '#f8c359',
|
|
130: '#f3ce83',
|
|
140: '#f0daad',
|
|
150: '#f1e7d3',
|
|
160: '#f9f6f1'
|
|
}
|
|
|
|
const teal: BrandVariants = {
|
|
10: '#091f20',
|
|
20: '#0a2d2e',
|
|
30: '#0b3c3e',
|
|
40: '#0c4b4d',
|
|
50: '#0d595c',
|
|
60: '#0e686b',
|
|
70: '#117578',
|
|
80: '#148185',
|
|
90: '#18a8ad',
|
|
100: '#19d2d9',
|
|
110: '#33e4ea',
|
|
120: '#5ce6ea',
|
|
130: '#86e6e9',
|
|
140: '#afe7e9',
|
|
150: '#d3edee',
|
|
160: '#f2f8f8'
|
|
}
|
|
|
|
// Rounder corners — buttons/inputs ~10px, cards ~16px. NOT pills.
|
|
const friendlyRadii = {
|
|
borderRadiusSmall: '5px',
|
|
borderRadiusMedium: '10px',
|
|
borderRadiusLarge: '12px',
|
|
borderRadiusXLarge: '16px'
|
|
} as const
|
|
|
|
// Dark mode keeps Fluent's NEUTRAL (charcoal) surfaces and uses the accent
|
|
// ramp only for buttons/links. The default dark brand is too low-contrast on
|
|
// near-black, so lift the brand to ramp-110 and flip on-brand text to dark for
|
|
// legible buttons. buildDarkOverrides derives that whole shape — hover/pressed/
|
|
// stroke shades and all — directly from each ramp's own stops.
|
|
function buildDarkOverrides(ramp: BrandVariants): Record<string, string> {
|
|
return {
|
|
colorBrandBackground: ramp[110],
|
|
colorBrandBackgroundHover: ramp[120],
|
|
colorBrandBackgroundPressed: ramp[100],
|
|
colorBrandBackgroundSelected: ramp[110],
|
|
colorNeutralForegroundOnBrand: ramp[10],
|
|
colorCompoundBrandBackground: ramp[110],
|
|
colorCompoundBrandBackgroundHover: ramp[120],
|
|
colorCompoundBrandBackgroundPressed: ramp[100],
|
|
colorCompoundBrandForeground1: ramp[120],
|
|
colorCompoundBrandForeground1Hover: ramp[130],
|
|
colorCompoundBrandForeground1Pressed: ramp[110],
|
|
colorCompoundBrandStroke: ramp[110],
|
|
colorCompoundBrandStrokeHover: ramp[120],
|
|
colorBrandForeground1: ramp[120],
|
|
colorBrandForeground2: ramp[130],
|
|
colorBrandForegroundLink: ramp[120],
|
|
colorBrandForegroundLinkHover: ramp[130],
|
|
colorBrandForegroundLinkPressed: ramp[110],
|
|
colorBrandStroke1: ramp[110],
|
|
colorBrandStroke2: ramp[60]
|
|
}
|
|
}
|
|
|
|
interface AccentPreset {
|
|
label: string
|
|
light: Theme
|
|
dark: Theme
|
|
/** ramp-80 — the light-mode primary, used as a swatch dot in the accent picker */
|
|
swatch: string
|
|
}
|
|
|
|
function buildPreset(
|
|
label: string,
|
|
ramp: BrandVariants,
|
|
darkOverrides: Record<string, string>
|
|
): AccentPreset {
|
|
return {
|
|
label,
|
|
light: { ...createLightTheme(ramp), ...friendlyRadii },
|
|
dark: { ...createDarkTheme(ramp), ...friendlyRadii, ...darkOverrides },
|
|
swatch: ramp[80]
|
|
}
|
|
}
|
|
|
|
export const ACCENT_PRESETS: Record<AccentColor, AccentPreset> = {
|
|
rose: buildPreset('Rose', rose, buildDarkOverrides(rose)),
|
|
coral: buildPreset('Coral', coral, buildDarkOverrides(coral)),
|
|
amber: buildPreset('Amber', amber, buildDarkOverrides(amber)),
|
|
teal: buildPreset('Teal', teal, buildDarkOverrides(teal))
|
|
}
|
|
|
|
export const ACCENT_OPTIONS: { value: AccentColor; label: string; swatch: string }[] = (
|
|
Object.keys(ACCENT_PRESETS) as AccentColor[]
|
|
).map((value) => ({
|
|
value,
|
|
label: ACCENT_PRESETS[value].label,
|
|
swatch: ACCENT_PRESETS[value].swatch
|
|
}))
|
|
|
|
export function getTheme(accent: AccentColor, mode: 'light' | 'dark'): Theme {
|
|
const preset = ACCENT_PRESETS[accent] ?? ACCENT_PRESETS.teal
|
|
return mode === 'dark' ? preset.dark : preset.light
|
|
}
|
|
|
|
export const pageBackground = PAGE_BACKGROUND
|
|
|
|
// Per-kind thumbnail tints. Video vs audio differ by ramp DEPTH rather than a
|
|
// competing hue, keeping a monochrome look. Theme-aware (light/dark) but
|
|
// intentionally NOT accent-aware — a fixed neutral grey that sits quietly on the
|
|
// card surface under any of the accent presets, warm or cool.
|
|
export const thumbColors = {
|
|
light: {
|
|
video: { bg: '#eeeef0', fg: '#5e616a' },
|
|
audio: { bg: '#e2e2e6', fg: '#4a4d55' }
|
|
},
|
|
dark: {
|
|
video: { bg: '#26262a', fg: '#b5b6bd' },
|
|
audio: { bg: '#2c2c31', fg: '#a0a1aa' }
|
|
}
|
|
} as const
|