managarten/packages/shared-theme/src/constants.ts
Till JS 68c2442419
Some checks are pending
CD Mac Mini / Detect Changes (push) Waiting to run
CD Mac Mini / Deploy (push) Blocked by required conditions
CI / Detect Changes (push) Waiting to run
CI / Validate (push) Waiting to run
CI / Auth flow integration test (push) Waiting to run
CI / Build mana-auth (push) Blocked by required conditions
CI / Build mana-search (push) Blocked by required conditions
CI / Build mana-sync (push) Blocked by required conditions
CI / Build mana-notify (push) Blocked by required conditions
CI / Build mana-api-gateway (push) Blocked by required conditions
CI / Build mana-crawler (push) Blocked by required conditions
CI / Build mana-media (push) Blocked by required conditions
CI / Build mana-credits (push) Blocked by required conditions
CI / Build mana-web (push) Blocked by required conditions
CI / Build chat-backend (push) Blocked by required conditions
CI / Build chat-web (push) Blocked by required conditions
CI / Build todo-backend (push) Blocked by required conditions
CI / Build todo-web (push) Blocked by required conditions
CI / Build calendar-backend (push) Blocked by required conditions
CI / Build calendar-web (push) Blocked by required conditions
CI / Build clock-web (push) Blocked by required conditions
CI / Build contacts-backend (push) Blocked by required conditions
CI / Build contacts-web (push) Blocked by required conditions
CI / Build presi-web (push) Blocked by required conditions
CI / Build storage-backend (push) Blocked by required conditions
CI / Build storage-web (push) Blocked by required conditions
CI / Build telegram-stats-bot (push) Blocked by required conditions
CI / Build nutriphi-backend (push) Blocked by required conditions
CI / Build nutriphi-web (push) Blocked by required conditions
CI / Build skilltree-web (push) Blocked by required conditions
Docker Validate / Validate Dockerfiles (push) Waiting to run
Docker Validate / Build calendar-web (push) Blocked by required conditions
Docker Validate / Build todo-backend (push) Blocked by required conditions
Docker Validate / Build todo-web (push) Blocked by required conditions
Docker Validate / Build zitare-web (push) Blocked by required conditions
Docker Validate / Build mana-auth (push) Blocked by required conditions
Docker Validate / Build mana-sync (push) Blocked by required conditions
Docker Validate / Build mana-media (push) Blocked by required conditions
Mirror to Forgejo / Push to Forgejo (push) Waiting to run
feat(workbench): paper-grain polish — blend-mode, border, stone palette
Switch PageShell's per-theme paper overlay from a ::before +
mix-blend-mode + opacity stack to direct background-blend-mode on the
element itself. The old approach had invisibility issues in dark mode
and stacking-context quirks that made the grain disappear entirely.
background-blend-mode against background-color is the simpler, more
reliable primitive.

utils.ts auto-switches multiply → overlay in dark mode (dark × dark is
essentially invisible) while leaving other blend modes as-is. The
opacityLight/opacityDark knobs are gone from the paper config since
background-blend-mode has no opacity slot — tune via blendMode choice
instead.

Visual tuning pass:
- Card border bumped from 1px box-shadow ring to a real 2px border
  with background-clip: border-box so the paper texture reads
  continuously across the edge. Alpha 0.12 light / 0.28 dark (black).
- Drop shadow deepened (0 8px 24px + 0 3px 8px) for more card lift.
- Stone theme cooled toward real slate-blue: hue 200 → 212, saturation
  bumped ~10pts across the palette. Stone was reading as warm-neutral
  grey, now it's a proper cold blue.
- Texture remap: Lume → paper-004 (strongest grain, 480px tile for
  coarser fiber), Stone → cardboard-002 (linen), Lavender → paper-001
  (freed up after Stone claimed cardboard-002).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:38:30 +02:00

516 lines
14 KiB
TypeScript

import type { ThemeVariant, ThemeVariantDefinition, ThemeColors } from './types';
/**
* All available theme variants
*/
export const THEME_VARIANTS: readonly ThemeVariant[] = [
'lume',
'nature',
'stone',
'ocean',
'sunset',
'midnight',
'rose',
'lavender',
] as const;
/**
* HSL Color Definitions for all theme variants
*
* Format: "H S% L%" (without hsl() wrapper for CSS variable compatibility)
*
* Color tokens:
* - primary: Main brand/accent color
* - secondary: Secondary accent
* - background: Page background
* - foreground: Main text color
* - surface: Card/content background
* - muted: Disabled/subtle elements
* - border: Border colors
* - error/success/warning: Semantic colors
*/
const lumeLight: ThemeColors = {
primary: '47 95% 58%', // #f8d62b - Gold
primaryForeground: '0 0% 0%', // Black text on gold
secondary: '47 100% 41%', // #D4B200 - Darker gold
secondaryForeground: '0 0% 0%',
background: '45 30% 96%', // Warm cream with gold tint
foreground: '0 0% 17%', // #2c2c2c - Dark text
surface: '0 0% 100%', // #ffffff - White
surfaceHover: '0 0% 96%', // #f5f5f5
surfaceElevated: '0 0% 100%', // #ffffff
muted: '0 0% 90%', // #e6e6e6
mutedForeground: '0 0% 40%', // #666666
border: '0 0% 90%', // #e6e6e6
borderStrong: '0 0% 80%', // #cccccc
error: '6 78% 57%', // #e74c3c
success: '145 63% 42%', // #27ae60
warning: '36 100% 50%', // #f39c12
input: '0 0% 100%', // #ffffff
ring: '47 95% 58%', // Same as primary
};
const lumeDark: ThemeColors = {
primary: '47 95% 58%', // #f8d62b - Gold (same in dark)
primaryForeground: '0 0% 0%', // Black text on gold
secondary: '47 70% 29%', // #7C6B16 - Muted gold
secondaryForeground: '0 0% 100%',
background: '40 10% 7%', // Very dark with warm tint
foreground: '0 0% 100%', // #ffffff - White text
surface: '40 8% 12%', // Dark surface with warm tint
surfaceHover: '40 8% 16%',
surfaceElevated: '40 8% 14%',
muted: '40 6% 20%',
mutedForeground: '40 5% 60%',
border: '40 6% 26%',
borderStrong: '40 5% 35%',
error: '6 78% 57%', // #e74c3c
success: '145 63% 49%', // #2ecc71
warning: '48 100% 50%', // #f1c40f
input: '0 0% 14%', // #242424
ring: '47 95% 58%',
};
const natureLight: ThemeColors = {
primary: '122 39% 49%', // #4CAF50 - Green
primaryForeground: '0 0% 100%', // White text on green
secondary: '122 38% 63%', // #81C784 - Light green
secondaryForeground: '0 0% 0%',
background: '80 33% 97%', // #FBFDF8 - Very light green tint
foreground: '122 56% 24%', // #1B5E20 - Dark green text
surface: '0 0% 100%', // #ffffff
surfaceHover: '120 25% 95%', // #F1F8E9
surfaceElevated: '0 0% 100%',
muted: '120 25% 95%', // #F1F8E9
mutedForeground: '122 20% 40%',
border: '120 25% 91%', // #E8F5E9
borderStrong: '120 26% 79%', // #C8E6C9
error: '0 65% 67%', // #E57373
success: '122 39% 49%', // Same as primary
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '122 39% 49%',
};
const natureDark: ThemeColors = {
primary: '122 39% 49%', // #4CAF50
primaryForeground: '0 0% 100%',
secondary: '122 30% 35%', // Muted green
secondaryForeground: '0 0% 100%',
background: '120 12% 6%', // Very dark with green tint
foreground: '0 0% 100%', // White
surface: '120 10% 11%', // Slight green tint
surfaceHover: '120 10% 15%',
surfaceElevated: '120 10% 13%',
muted: '120 10% 19%',
mutedForeground: '120 10% 60%',
border: '120 10% 24%',
borderStrong: '120 10% 34%',
error: '0 65% 57%',
success: '122 50% 55%',
warning: '48 100% 50%',
input: '120 10% 14%',
ring: '122 39% 49%',
};
// Stone: cooled toward a proper blue-slate. Hue shifted 200 → 212
// (more blue, less cyan) and saturation bumped ~10pts across the
// palette so the chill actually reads instead of looking like warm grey.
const stoneLight: ThemeColors = {
primary: '212 32% 44%', // #4A6B8C - Deeper slate-blue
primaryForeground: '0 0% 100%',
secondary: '212 26% 60%', // #7E9CB8 - Cool slate
secondaryForeground: '0 0% 0%',
background: '214 30% 96%', // #EEF2F7 - Icy blue-white
foreground: '215 32% 16%', // #1B2835 - Deep slate
surface: '210 40% 99%',
surfaceHover: '212 24% 92%', // #E0E7EF
surfaceElevated: '210 40% 99%',
muted: '212 22% 92%', // #DFE6EE
mutedForeground: '212 18% 42%',
border: '212 22% 84%', // #C4D0DE
borderStrong: '212 24% 70%', // #9FB2C6
error: '4 90% 63%',
success: '145 63% 42%',
warning: '36 100% 50%',
input: '210 40% 99%',
ring: '212 32% 44%',
};
const stoneDark: ThemeColors = {
primary: '212 28% 56%', // #6F8DAC - Brighter slate-blue for dark
primaryForeground: '0 0% 0%',
secondary: '212 20% 38%',
secondaryForeground: '0 0% 100%',
background: '215 30% 8%', // Very dark with clear blue cast
foreground: '210 20% 96%',
surface: '214 24% 12%',
surfaceHover: '214 22% 17%',
surfaceElevated: '214 22% 15%',
muted: '212 18% 22%',
mutedForeground: '212 18% 62%',
border: '212 18% 28%',
borderStrong: '212 18% 40%',
error: '4 90% 58%',
success: '145 63% 49%',
warning: '48 100% 50%',
input: '214 22% 15%',
ring: '212 28% 56%',
};
const oceanLight: ThemeColors = {
primary: '199 98% 45%', // #039BE5 - Bright blue
primaryForeground: '0 0% 100%',
secondary: '199 92% 64%', // #4FC3F7 - Light blue
secondaryForeground: '0 0% 0%',
background: '199 100% 97%', // #F5FCFF - Very light blue
foreground: '199 100% 18%', // #01579B - Dark blue
surface: '0 0% 100%',
surfaceHover: '199 100% 94%', // #E1F5FE
surfaceElevated: '0 0% 100%',
muted: '199 100% 94%', // #E1F5FE
mutedForeground: '199 50% 40%',
border: '199 71% 87%', // #B3E5FC
borderStrong: '199 79% 76%', // #81D4FA
error: '4 90% 63%', // #EF5350
success: '145 63% 42%',
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '199 98% 45%',
};
const oceanDark: ThemeColors = {
primary: '199 98% 48%', // Slightly brighter in dark
primaryForeground: '0 0% 0%',
secondary: '199 60% 35%',
secondaryForeground: '0 0% 100%',
background: '200 25% 7%', // Very dark with blue tint
foreground: '0 0% 100%',
surface: '199 20% 11%', // Slight blue tint
surfaceHover: '199 20% 15%',
surfaceElevated: '199 20% 13%',
muted: '199 15% 19%',
mutedForeground: '199 15% 60%',
border: '199 15% 24%',
borderStrong: '199 15% 34%',
error: '4 90% 58%',
success: '145 63% 49%',
warning: '48 100% 50%',
input: '199 30% 14%',
ring: '199 98% 48%',
};
// ============================================================================
// Extended Themes: Sunset, Midnight, Rose, Lavender
// ============================================================================
const sunsetLight: ThemeColors = {
primary: '15 90% 55%', // Coral/Orange
primaryForeground: '0 0% 100%',
secondary: '25 100% 60%', // Warm orange
secondaryForeground: '0 0% 0%',
background: '30 50% 97%', // Warm cream
foreground: '15 50% 20%', // Dark warm brown
surface: '0 0% 100%',
surfaceHover: '30 40% 95%',
surfaceElevated: '0 0% 100%',
muted: '30 30% 93%',
mutedForeground: '15 20% 45%',
border: '30 25% 88%',
borderStrong: '30 30% 75%',
error: '0 72% 55%',
success: '145 63% 42%',
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '15 90% 55%',
};
const sunsetDark: ThemeColors = {
primary: '15 85% 58%', // Brighter coral in dark
primaryForeground: '0 0% 0%',
secondary: '25 60% 35%',
secondaryForeground: '0 0% 100%',
background: '15 20% 8%', // Dark with warm tint
foreground: '0 0% 100%',
surface: '15 15% 12%',
surfaceHover: '15 15% 16%',
surfaceElevated: '15 15% 14%',
muted: '15 12% 20%',
mutedForeground: '15 10% 60%',
border: '15 12% 25%',
borderStrong: '15 12% 35%',
error: '0 72% 55%',
success: '145 63% 49%',
warning: '48 100% 50%',
input: '15 20% 14%',
ring: '15 85% 58%',
};
const midnightLight: ThemeColors = {
primary: '260 70% 55%', // Deep purple/violet
primaryForeground: '0 0% 100%',
secondary: '270 60% 70%', // Lighter purple
secondaryForeground: '0 0% 0%',
background: '260 30% 97%', // Very light purple tint
foreground: '260 50% 20%', // Dark purple text
surface: '0 0% 100%',
surfaceHover: '260 25% 95%',
surfaceElevated: '0 0% 100%',
muted: '260 20% 93%',
mutedForeground: '260 20% 45%',
border: '260 20% 88%',
borderStrong: '260 25% 75%',
error: '0 72% 55%',
success: '145 63% 42%',
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '260 70% 55%',
};
const midnightDark: ThemeColors = {
primary: '260 65% 60%', // Brighter purple in dark
primaryForeground: '0 0% 100%',
secondary: '270 40% 35%',
secondaryForeground: '0 0% 100%',
background: '260 25% 7%', // Deep dark purple
foreground: '0 0% 100%',
surface: '260 20% 11%',
surfaceHover: '260 20% 15%',
surfaceElevated: '260 20% 13%',
muted: '260 15% 19%',
mutedForeground: '260 12% 60%',
border: '260 15% 24%',
borderStrong: '260 15% 34%',
error: '0 72% 55%',
success: '145 63% 49%',
warning: '48 100% 50%',
input: '260 25% 14%',
ring: '260 65% 60%',
};
const roseLight: ThemeColors = {
primary: '340 80% 55%', // Pink/Magenta
primaryForeground: '0 0% 100%',
secondary: '350 70% 70%', // Lighter pink
secondaryForeground: '0 0% 0%',
background: '340 40% 97%', // Very light pink tint
foreground: '340 50% 20%', // Dark rose text
surface: '0 0% 100%',
surfaceHover: '340 30% 95%',
surfaceElevated: '0 0% 100%',
muted: '340 25% 93%',
mutedForeground: '340 20% 45%',
border: '340 25% 88%',
borderStrong: '340 30% 75%',
error: '0 72% 55%',
success: '145 63% 42%',
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '340 80% 55%',
};
const roseDark: ThemeColors = {
primary: '340 75% 60%', // Brighter pink in dark
primaryForeground: '0 0% 100%',
secondary: '350 45% 35%',
secondaryForeground: '0 0% 100%',
background: '340 20% 8%', // Dark with pink tint
foreground: '0 0% 100%',
surface: '340 15% 12%',
surfaceHover: '340 15% 16%',
surfaceElevated: '340 15% 14%',
muted: '340 12% 20%',
mutedForeground: '340 10% 60%',
border: '340 12% 25%',
borderStrong: '340 12% 35%',
error: '0 72% 55%',
success: '145 63% 49%',
warning: '48 100% 50%',
input: '340 20% 14%',
ring: '340 75% 60%',
};
const lavenderLight: ThemeColors = {
primary: '270 60% 60%', // Lavender/Light purple
primaryForeground: '0 0% 100%',
secondary: '280 50% 75%', // Softer purple
secondaryForeground: '0 0% 0%',
background: '270 35% 97%', // Very light lavender
foreground: '270 40% 22%', // Dark lavender text
surface: '0 0% 100%',
surfaceHover: '270 25% 95%',
surfaceElevated: '0 0% 100%',
muted: '270 20% 93%',
mutedForeground: '270 18% 45%',
border: '270 20% 88%',
borderStrong: '270 25% 78%',
error: '0 72% 55%',
success: '145 63% 42%',
warning: '36 100% 50%',
input: '0 0% 100%',
ring: '270 60% 60%',
};
const lavenderDark: ThemeColors = {
primary: '270 55% 65%', // Brighter lavender in dark
primaryForeground: '0 0% 0%',
secondary: '280 35% 38%',
secondaryForeground: '0 0% 100%',
background: '270 20% 8%', // Dark with lavender tint
foreground: '0 0% 100%',
surface: '270 15% 12%',
surfaceHover: '270 15% 16%',
surfaceElevated: '270 15% 14%',
muted: '270 12% 20%',
mutedForeground: '270 10% 60%',
border: '270 12% 25%',
borderStrong: '270 12% 35%',
error: '0 72% 55%',
success: '145 63% 49%',
warning: '48 100% 50%',
input: '270 20% 14%',
ring: '270 55% 65%',
};
/**
* Complete theme variant definitions
*
* Each theme can also carry a `paper` descriptor — a tileable texture
* the workbench page-shell uses as a grain overlay. Swap the filename
* here to change the texture for a whole theme in one place. Assets
* live under `apps/<app>/static/textures/paper/` and are CC0-licensed
* (see that directory's LICENSE.txt for provenance).
*/
export const THEME_DEFINITIONS: Record<ThemeVariant, ThemeVariantDefinition> = {
lume: {
name: 'lume',
label: 'Lume',
emoji: '✨',
icon: 'sparkle',
hue: 47,
light: lumeLight,
dark: lumeDark,
// Lume wants a stronger, more contrasty grain — paper-004 has
// the heaviest fiber detail in the set, multiply makes it bite.
// Larger tile so the grain pattern reads as big paper fibers
// instead of fine noise.
paper: {
url: '/textures/paper/paper-004.jpg',
blendMode: 'multiply',
size: '480px 480px',
},
},
nature: {
name: 'nature',
label: 'Nature',
emoji: '🌿',
icon: 'leaf',
hue: 122,
light: natureLight,
dark: natureDark,
paper: {
url: '/textures/paper/cardboard-001.jpg',
blendMode: 'multiply',
},
},
stone: {
name: 'stone',
label: 'Stone',
emoji: '🪨',
icon: 'hexagon',
hue: 212,
light: stoneLight,
dark: stoneDark,
// Stone gets the linen-like cardboard texture — more tactile
// fiber pattern than the old paper-005 (which felt too smooth).
paper: {
url: '/textures/paper/cardboard-002.jpg',
blendMode: 'multiply',
},
},
ocean: {
name: 'ocean',
label: 'Ocean',
emoji: '🌊',
icon: 'waves',
hue: 199,
light: oceanLight,
dark: oceanDark,
paper: {
url: '/textures/paper/paper-003.jpg',
blendMode: 'multiply',
},
},
// Extended themes (not in PillNav by default, can be pinned)
sunset: {
name: 'sunset',
label: 'Sunset',
emoji: '🌅',
icon: 'sun',
hue: 15,
light: sunsetLight,
dark: sunsetDark,
paper: {
url: '/textures/paper/paper-006.jpg',
blendMode: 'multiply',
},
},
midnight: {
name: 'midnight',
label: 'Midnight',
emoji: '🌙',
icon: 'moon',
hue: 260,
light: midnightLight,
dark: midnightDark,
paper: {
url: '/textures/paper/paper-004.jpg',
blendMode: 'overlay',
},
},
rose: {
name: 'rose',
label: 'Rose',
emoji: '🌹',
icon: 'flower',
hue: 340,
light: roseLight,
dark: roseDark,
paper: {
url: '/textures/paper/paper-002.jpg',
blendMode: 'multiply',
},
},
lavender: {
name: 'lavender',
label: 'Lavender',
emoji: '💜',
icon: 'sparkle',
hue: 270,
light: lavenderLight,
dark: lavenderDark,
// Freed up after Stone claimed cardboard-002 — paper-001 gives
// lavender a softer, smoother fiber than the rough cardboard.
paper: {
url: '/textures/paper/paper-001.jpg',
blendMode: 'soft-light',
},
},
};
/**
* Default theme configuration
*/
export const DEFAULT_MODE = 'system' as const;
export const DEFAULT_VARIANT = 'lume' as const;
/**
* CSS variable prefix
*/
export const CSS_VAR_PREFIX = '--color' as const;
/**
* LocalStorage key suffix
*/
export const STORAGE_KEY_SUFFIX = '-theme' as const;