code: nextjs,frontends,theme (7 files)

This commit is contained in:
Richard Ward
2025-12-30 18:54:42 +00:00
parent 456b126beb
commit dbb97f2501
7 changed files with 434 additions and 325 deletions
+114 -56
View File
@@ -1,62 +1,120 @@
'use client'
export * from './modes/dark-theme'
import { alpha, createTheme, type Shadows } from '@mui/material/styles'
/**
* Dark Theme - CSS Custom Properties
*
* This theme exports CSS variable definitions for the dark color scheme.
* Apply these variables to your root element or use with data-theme="dark".
*/
import { colors } from './colors'
import { getComponentOverrides } from './components'
import { fonts } from './fonts'
import { layout } from './layout'
import { typography } from './typography'
const custom = {
fonts,
borderRadius: layout.borderRadius,
contentWidth: layout.contentWidth,
sidebar: layout.sidebar,
header: layout.header,
}
const shadows = (o: number): Shadows =>
[
'none',
`0 1px 2px rgba(0,0,0,${o / 2})`,
`0 1px 3px rgba(0,0,0,${o})`,
`0 4px 6px rgba(0,0,0,${o})`,
`0 10px 15px rgba(0,0,0,${o})`,
`0 20px 25px rgba(0,0,0,${o})`,
`0 25px 50px rgba(0,0,0,${o * 2.5})`,
...Array(18).fill(`0 25px 50px rgba(0,0,0,${o * 2.5})`),
] as Shadows
/** CSS custom property definitions for dark theme */
export const darkTheme = {
// Mode
'--theme-mode': 'dark',
export const darkTheme = createTheme({
palette: {
mode: 'dark',
primary: colors.primary.dark,
secondary: colors.secondary.dark,
error: colors.error.dark,
warning: colors.warning.dark,
info: colors.info.dark,
success: colors.success.dark,
neutral: colors.neutral,
background: { default: colors.neutral[950], paper: colors.neutral[900] },
text: {
primary: colors.neutral[100],
secondary: colors.neutral[400],
disabled: colors.neutral[600],
},
divider: colors.neutral[800],
action: {
active: colors.neutral[300],
hover: alpha(colors.neutral[400], 0.12),
selected: alpha(colors.primary.dark.main, 0.2),
disabled: colors.neutral[600],
disabledBackground: colors.neutral[800],
},
},
typography,
spacing: layout.spacing,
shape: { borderRadius: 8 },
shadows: shadows(0.4),
components: getComponentOverrides('dark'),
custom,
})
// Primary colors
'--primary-main': colors.primary.dark.main,
'--primary-light': colors.primary.dark.light,
'--primary-dark': colors.primary.dark.dark,
'--primary-contrast-text': colors.primary.dark.contrastText,
// Secondary colors
'--secondary-main': colors.secondary.dark.main,
'--secondary-light': colors.secondary.dark.light,
'--secondary-dark': colors.secondary.dark.dark,
'--secondary-contrast-text': colors.secondary.dark.contrastText,
// Error colors
'--error-main': colors.error.dark.main,
'--error-light': colors.error.dark.light,
'--error-dark': colors.error.dark.dark,
// Warning colors
'--warning-main': colors.warning.dark.main,
'--warning-light': colors.warning.dark.light,
'--warning-dark': colors.warning.dark.dark,
// Info colors
'--info-main': colors.info.dark.main,
'--info-light': colors.info.dark.light,
'--info-dark': colors.info.dark.dark,
// Success colors
'--success-main': colors.success.dark.main,
'--success-light': colors.success.dark.light,
'--success-dark': colors.success.dark.dark,
// Neutral colors
'--neutral-50': colors.neutral[50],
'--neutral-100': colors.neutral[100],
'--neutral-200': colors.neutral[200],
'--neutral-300': colors.neutral[300],
'--neutral-400': colors.neutral[400],
'--neutral-500': colors.neutral[500],
'--neutral-600': colors.neutral[600],
'--neutral-700': colors.neutral[700],
'--neutral-800': colors.neutral[800],
'--neutral-900': colors.neutral[900],
'--neutral-950': colors.neutral[950],
// Background
'--background-default': colors.neutral[950],
'--background-paper': colors.neutral[900],
// Text
'--text-primary': colors.neutral[100],
'--text-secondary': colors.neutral[400],
'--text-disabled': colors.neutral[600],
// Divider
'--divider': colors.neutral[800],
// Action states
'--action-active': colors.neutral[300],
'--action-hover': `rgba(161, 161, 170, 0.12)`, // neutral[400] at 12%
'--action-selected': `rgba(167, 139, 250, 0.2)`, // primary.dark.main at 20%
'--action-disabled': colors.neutral[600],
'--action-disabled-background': colors.neutral[800],
// Typography
'--font-family-body': fonts.body,
'--font-family-heading': fonts.heading,
'--font-family-mono': fonts.mono,
// Layout
'--spacing-unit': `${layout.spacing}px`,
'--border-radius': '8px',
'--border-radius-none': `${layout.borderRadius.none}px`,
'--border-radius-sm': `${layout.borderRadius.sm}px`,
'--border-radius-md': `${layout.borderRadius.md}px`,
'--border-radius-lg': `${layout.borderRadius.lg}px`,
'--border-radius-xl': `${layout.borderRadius.xl}px`,
'--border-radius-full': `${layout.borderRadius.full}px`,
// Content widths
'--content-width-sm': layout.contentWidth.sm,
'--content-width-md': layout.contentWidth.md,
'--content-width-lg': layout.contentWidth.lg,
'--content-width-xl': layout.contentWidth.xl,
'--content-width-full': layout.contentWidth.full,
// Sidebar
'--sidebar-width': `${layout.sidebar.width}px`,
'--sidebar-collapsed-width': `${layout.sidebar.collapsedWidth}px`,
// Header
'--header-height': `${layout.header.height}px`,
// Shadows (dark theme uses opacity 0.4)
'--shadow-1': '0 1px 2px rgba(0,0,0,0.2)',
'--shadow-2': '0 1px 3px rgba(0,0,0,0.4)',
'--shadow-3': '0 4px 6px rgba(0,0,0,0.4)',
'--shadow-4': '0 10px 15px rgba(0,0,0,0.4)',
'--shadow-5': '0 20px 25px rgba(0,0,0,0.4)',
'--shadow-6': '0 25px 50px rgba(0,0,0,1)',
} as const
/** Type for dark theme CSS variables */
export type DarkThemeVars = typeof darkTheme
+68 -5
View File
@@ -1,10 +1,73 @@
/** Theme Module - Central export for all theme functionality */
/**
* Theme Module - Central export for all theme functionality
*
* This module provides CSS custom property-based theming for fakemui compatibility.
* Themes are exported as objects containing CSS variable definitions.
*/
export { colors } from './colors'
export { darkTheme } from './dark-theme'
export { darkTheme, type DarkThemeVars } from './dark-theme'
export { fonts } from './fonts'
export { layout } from './layout'
export { lightTheme } from './light-theme'
export { lightTheme, type LightThemeVars } from './light-theme'
export { typography } from './typography'
export type { SxProps, Theme } from '@mui/material/styles'
export { alpha, styled, useTheme } from '@mui/material/styles'
/** Theme variable keys - union of all CSS custom property names */
export type ThemeVarKey = keyof typeof import('./light-theme').lightTheme
/** Theme object type */
export type ThemeVars = Record<ThemeVarKey, string>
/**
* Apply theme CSS variables to an element
* @param element - The element to apply the theme to (defaults to document.documentElement)
* @param theme - The theme object containing CSS variable definitions
*/
export const applyTheme = (
theme: Record<string, string>,
element: HTMLElement = typeof document !== 'undefined' ? document.documentElement : null!
): void => {
if (!element) return
Object.entries(theme).forEach(([key, value]) => {
element.style.setProperty(key, value)
})
}
/**
* Generate CSS string from theme variables
* @param theme - The theme object containing CSS variable definitions
* @param selector - CSS selector to scope the variables (default: ':root')
* @returns CSS string with variable definitions
*/
export const themeToCSS = (
theme: Record<string, string>,
selector: string = ':root'
): string => {
const vars = Object.entries(theme)
.map(([key, value]) => ` ${key}: ${value};`)
.join('\n')
return `${selector} {\n${vars}\n}`
}
/**
* Get CSS variable reference
* @param varName - The CSS variable name (with or without --)
* @param fallback - Optional fallback value
* @returns CSS var() function string
*/
export const cssVar = (varName: string, fallback?: string): string => {
const name = varName.startsWith('--') ? varName : `--${varName}`
return fallback ? `var(${name}, ${fallback})` : `var(${name})`
}
/**
* Create an alpha/transparent version of a color using CSS color-mix
* Replacement for MUI's alpha() function
* @param color - The base color (can be a CSS variable reference)
* @param opacity - Opacity value between 0 and 1
* @returns CSS color-mix() function string
*/
export const alpha = (color: string, opacity: number): string => {
const percentage = Math.round(opacity * 100)
return `color-mix(in srgb, ${color} ${percentage}%, transparent)`
}
+114 -56
View File
@@ -1,62 +1,120 @@
'use client'
export * from './modes/light-theme'
import { alpha, createTheme, type Shadows } from '@mui/material/styles'
/**
* Light Theme - CSS Custom Properties
*
* This theme exports CSS variable definitions for the light color scheme.
* Apply these variables to your root element or use with data-theme="light".
*/
import { colors } from './colors'
import { getComponentOverrides } from './components'
import { fonts } from './fonts'
import { layout } from './layout'
import { typography } from './typography'
const custom = {
fonts,
borderRadius: layout.borderRadius,
contentWidth: layout.contentWidth,
sidebar: layout.sidebar,
header: layout.header,
}
const shadows = (o: number): Shadows =>
[
'none',
`0 1px 2px rgba(0,0,0,${o / 2})`,
`0 1px 3px rgba(0,0,0,${o})`,
`0 4px 6px rgba(0,0,0,${o})`,
`0 10px 15px rgba(0,0,0,${o})`,
`0 20px 25px rgba(0,0,0,${o})`,
`0 25px 50px rgba(0,0,0,${o * 2.5})`,
...Array(18).fill(`0 25px 50px rgba(0,0,0,${o * 2.5})`),
] as Shadows
/** CSS custom property definitions for light theme */
export const lightTheme = {
// Mode
'--theme-mode': 'light',
export const lightTheme = createTheme({
palette: {
mode: 'light',
primary: colors.primary.light,
secondary: colors.secondary.light,
error: colors.error.light,
warning: colors.warning.light,
info: colors.info.light,
success: colors.success.light,
neutral: colors.neutral,
background: { default: '#ffffff', paper: colors.neutral[50] },
text: {
primary: colors.neutral[900],
secondary: colors.neutral[600],
disabled: colors.neutral[400],
},
divider: colors.neutral[200],
action: {
active: colors.neutral[700],
hover: alpha(colors.neutral[500], 0.08),
selected: alpha(colors.primary.light.main, 0.12),
disabled: colors.neutral[400],
disabledBackground: colors.neutral[200],
},
},
typography,
spacing: layout.spacing,
shape: { borderRadius: 8 },
shadows: shadows(0.1),
components: getComponentOverrides('light'),
custom,
})
// Primary colors
'--primary-main': colors.primary.light.main,
'--primary-light': colors.primary.light.light,
'--primary-dark': colors.primary.light.dark,
'--primary-contrast-text': colors.primary.light.contrastText,
// Secondary colors
'--secondary-main': colors.secondary.light.main,
'--secondary-light': colors.secondary.light.light,
'--secondary-dark': colors.secondary.light.dark,
'--secondary-contrast-text': colors.secondary.light.contrastText,
// Error colors
'--error-main': colors.error.light.main,
'--error-light': colors.error.light.light,
'--error-dark': colors.error.light.dark,
// Warning colors
'--warning-main': colors.warning.light.main,
'--warning-light': colors.warning.light.light,
'--warning-dark': colors.warning.light.dark,
// Info colors
'--info-main': colors.info.light.main,
'--info-light': colors.info.light.light,
'--info-dark': colors.info.light.dark,
// Success colors
'--success-main': colors.success.light.main,
'--success-light': colors.success.light.light,
'--success-dark': colors.success.light.dark,
// Neutral colors
'--neutral-50': colors.neutral[50],
'--neutral-100': colors.neutral[100],
'--neutral-200': colors.neutral[200],
'--neutral-300': colors.neutral[300],
'--neutral-400': colors.neutral[400],
'--neutral-500': colors.neutral[500],
'--neutral-600': colors.neutral[600],
'--neutral-700': colors.neutral[700],
'--neutral-800': colors.neutral[800],
'--neutral-900': colors.neutral[900],
'--neutral-950': colors.neutral[950],
// Background
'--background-default': '#ffffff',
'--background-paper': colors.neutral[50],
// Text
'--text-primary': colors.neutral[900],
'--text-secondary': colors.neutral[600],
'--text-disabled': colors.neutral[400],
// Divider
'--divider': colors.neutral[200],
// Action states
'--action-active': colors.neutral[700],
'--action-hover': `rgba(113, 113, 122, 0.08)`, // neutral[500] at 8%
'--action-selected': `rgba(139, 92, 246, 0.12)`, // primary.light.main at 12%
'--action-disabled': colors.neutral[400],
'--action-disabled-background': colors.neutral[200],
// Typography
'--font-family-body': fonts.body,
'--font-family-heading': fonts.heading,
'--font-family-mono': fonts.mono,
// Layout
'--spacing-unit': `${layout.spacing}px`,
'--border-radius': '8px',
'--border-radius-none': `${layout.borderRadius.none}px`,
'--border-radius-sm': `${layout.borderRadius.sm}px`,
'--border-radius-md': `${layout.borderRadius.md}px`,
'--border-radius-lg': `${layout.borderRadius.lg}px`,
'--border-radius-xl': `${layout.borderRadius.xl}px`,
'--border-radius-full': `${layout.borderRadius.full}px`,
// Content widths
'--content-width-sm': layout.contentWidth.sm,
'--content-width-md': layout.contentWidth.md,
'--content-width-lg': layout.contentWidth.lg,
'--content-width-xl': layout.contentWidth.xl,
'--content-width-full': layout.contentWidth.full,
// Sidebar
'--sidebar-width': `${layout.sidebar.width}px`,
'--sidebar-collapsed-width': `${layout.sidebar.collapsedWidth}px`,
// Header
'--header-height': `${layout.header.height}px`,
// Shadows (light theme uses opacity 0.1)
'--shadow-1': '0 1px 2px rgba(0,0,0,0.05)',
'--shadow-2': '0 1px 3px rgba(0,0,0,0.1)',
'--shadow-3': '0 4px 6px rgba(0,0,0,0.1)',
'--shadow-4': '0 10px 15px rgba(0,0,0,0.1)',
'--shadow-5': '0 20px 25px rgba(0,0,0,0.1)',
'--shadow-6': '0 25px 50px rgba(0,0,0,0.25)',
} as const
/** Type for light theme CSS variables */
export type LightThemeVars = typeof lightTheme
+7 -60
View File
@@ -1,61 +1,8 @@
'use client'
/**
* Dark Theme (modes) - Re-export from parent
*
* This file re-exports the dark theme from the parent theme directory
* for backwards compatibility with existing imports.
*/
import { alpha, createTheme, type Shadows } from '@mui/material/styles'
import { colors } from '../colors'
import { getComponentOverrides } from '../components'
import { fonts } from '../fonts'
import { layout } from '../layout'
import { typography } from '../typography'
const custom = {
fonts,
borderRadius: layout.borderRadius,
contentWidth: layout.contentWidth,
sidebar: layout.sidebar,
header: layout.header,
}
const shadows = (o: number): Shadows =>
[
'none',
`0 1px 2px rgba(0,0,0,${o / 2})`,
`0 1px 3px rgba(0,0,0,${o})`,
`0 4px 6px rgba(0,0,0,${o})`,
`0 10px 15px rgba(0,0,0,${o})`,
`0 20px 25px rgba(0,0,0,${o})`,
`0 25px 50px rgba(0,0,0,${o * 2.5})`,
...Array(18).fill(`0 25px 50px rgba(0,0,0,${o * 2.5})`),
] as Shadows
export const darkTheme = createTheme({
palette: {
mode: 'dark',
primary: colors.primary.dark,
secondary: colors.secondary.dark,
error: colors.error.dark,
warning: colors.warning.dark,
info: colors.info.dark,
success: colors.success.dark,
neutral: colors.neutral,
background: { default: colors.neutral[950], paper: colors.neutral[900] },
text: {
primary: colors.neutral[100],
secondary: colors.neutral[400],
disabled: colors.neutral[600],
},
divider: colors.neutral[800],
action: {
active: colors.neutral[300],
hover: alpha(colors.neutral[400], 0.12),
selected: alpha(colors.primary.dark.main, 0.2),
disabled: colors.neutral[600],
disabledBackground: colors.neutral[800],
},
},
typography,
spacing: layout.spacing,
shape: { borderRadius: 8 },
shadows: shadows(0.4),
components: getComponentOverrides('dark'),
custom,
})
export { darkTheme, type DarkThemeVars } from '../dark-theme'
@@ -1,61 +1,8 @@
'use client'
/**
* Light Theme (modes) - Re-export from parent
*
* This file re-exports the light theme from the parent theme directory
* for backwards compatibility with existing imports.
*/
import { alpha, createTheme, type Shadows } from '@mui/material/styles'
import { colors } from '../colors'
import { getComponentOverrides } from '../components'
import { fonts } from '../fonts'
import { layout } from '../layout'
import { typography } from '../typography'
const custom = {
fonts,
borderRadius: layout.borderRadius,
contentWidth: layout.contentWidth,
sidebar: layout.sidebar,
header: layout.header,
}
const shadows = (o: number): Shadows =>
[
'none',
`0 1px 2px rgba(0,0,0,${o / 2})`,
`0 1px 3px rgba(0,0,0,${o})`,
`0 4px 6px rgba(0,0,0,${o})`,
`0 10px 15px rgba(0,0,0,${o})`,
`0 20px 25px rgba(0,0,0,${o})`,
`0 25px 50px rgba(0,0,0,${o * 2.5})`,
...Array(18).fill(`0 25px 50px rgba(0,0,0,${o * 2.5})`),
] as Shadows
export const lightTheme = createTheme({
palette: {
mode: 'light',
primary: colors.primary.light,
secondary: colors.secondary.light,
error: colors.error.light,
warning: colors.warning.light,
info: colors.info.light,
success: colors.success.light,
neutral: colors.neutral,
background: { default: '#ffffff', paper: colors.neutral[50] },
text: {
primary: colors.neutral[900],
secondary: colors.neutral[600],
disabled: colors.neutral[400],
},
divider: colors.neutral[200],
action: {
active: colors.neutral[700],
hover: alpha(colors.neutral[500], 0.08),
selected: alpha(colors.primary.light.main, 0.12),
disabled: colors.neutral[400],
disabledBackground: colors.neutral[200],
},
},
typography,
spacing: layout.spacing,
shape: { borderRadius: 8 },
shadows: shadows(0.1),
components: getComponentOverrides('light'),
custom,
})
export { lightTheme, type LightThemeVars } from '../light-theme'
+57 -55
View File
@@ -1,71 +1,73 @@
import '@mui/material/styles'
import '@mui/material/Typography'
import '@mui/material/Button'
import '@mui/material/Chip'
import '@mui/material/IconButton'
import '@mui/material/Badge'
import '@mui/material/Alert'
/**
* FakeMUI Component Type Extensions
*
* This file defines additional component variants and props for the fakemui component library.
* These types extend the base component interfaces to support custom variants.
*/
// Typography variants and component overrides
declare module '@mui/material/styles' {
interface TypographyVariants {
code: React.CSSProperties
kbd: React.CSSProperties
label: React.CSSProperties
}
import type { CSSProperties } from 'react'
interface TypographyVariantsOptions {
code?: React.CSSProperties
kbd?: React.CSSProperties
label?: React.CSSProperties
}
/** Typography variant extensions */
export interface TypographyVariants {
code: CSSProperties
kbd: CSSProperties
label: CSSProperties
}
declare module '@mui/material/Typography' {
interface TypographyPropsVariantOverrides {
code: true
kbd: true
label: true
}
/** Button variant extensions */
export interface ButtonVariants {
soft: true
ghost: true
}
declare module '@mui/material/Button' {
interface ButtonPropsVariantOverrides {
soft: true
ghost: true
}
interface ButtonPropsColorOverrides {
neutral: true
}
/** Button color extensions */
export interface ButtonColors {
neutral: true
}
declare module '@mui/material/Chip' {
interface ChipPropsVariantOverrides {
soft: true
}
interface ChipPropsColorOverrides {
neutral: true
}
/** Chip variant extensions */
export interface ChipVariants {
soft: true
}
declare module '@mui/material/IconButton' {
interface IconButtonPropsColorOverrides {
neutral: true
}
/** Chip color extensions */
export interface ChipColors {
neutral: true
}
declare module '@mui/material/Badge' {
interface BadgePropsColorOverrides {
neutral: true
}
/** IconButton color extensions */
export interface IconButtonColors {
neutral: true
}
declare module '@mui/material/Alert' {
interface AlertPropsVariantOverrides {
soft: true
}
/** Badge color extensions */
export interface BadgeColors {
neutral: true
}
export {}
/** Alert variant extensions */
export interface AlertVariants {
soft: true
}
/** Combined component extensions for fakemui */
export interface FakeMUIComponentExtensions {
typography: TypographyVariants
button: {
variants: ButtonVariants
colors: ButtonColors
}
chip: {
variants: ChipVariants
colors: ChipColors
}
iconButton: {
colors: IconButtonColors
}
badge: {
colors: BadgeColors
}
alert: {
variants: AlertVariants
}
}
+67 -33
View File
@@ -1,38 +1,72 @@
import '@mui/material/styles'
/**
* FakeMUI Palette Type Definitions
*
* This file defines the color palette types used by the fakemui theme system.
* These replace MUI's palette augmentation with standalone types.
*/
// Extend palette with custom neutral colors
declare module '@mui/material/styles' {
interface Palette {
neutral: {
50: string
100: string
200: string
300: string
400: string
500: string
600: string
700: string
800: string
900: string
950: string
}
/** Neutral color scale (50-950) */
export interface NeutralPalette {
50: string
100: string
200: string
300: string
400: string
500: string
600: string
700: string
800: string
900: string
950: string
}
/** Standard color palette with main, light, dark variants */
export interface ColorPalette {
main: string
light: string
dark: string
contrastText?: string
}
/** Full theme palette structure */
export interface ThemePalette {
mode: 'light' | 'dark'
primary: ColorPalette
secondary: ColorPalette
error: ColorPalette
warning: ColorPalette
info: ColorPalette
success: ColorPalette
neutral: NeutralPalette
background: {
default: string
paper: string
}
interface PaletteOptions {
neutral?: {
50?: string
100?: string
200?: string
300?: string
400?: string
500?: string
600?: string
700?: string
800?: string
900?: string
950?: string
}
text: {
primary: string
secondary: string
disabled: string
}
divider: string
action: {
active: string
hover: string
selected: string
disabled: string
disabledBackground: string
}
}
export {}
/** CSS variable names for palette colors */
export type PaletteVarName =
| `--primary-${'main' | 'light' | 'dark' | 'contrast-text'}`
| `--secondary-${'main' | 'light' | 'dark' | 'contrast-text'}`
| `--error-${'main' | 'light' | 'dark'}`
| `--warning-${'main' | 'light' | 'dark'}`
| `--info-${'main' | 'light' | 'dark'}`
| `--success-${'main' | 'light' | 'dark'}`
| `--neutral-${50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950}`
| `--background-${'default' | 'paper'}`
| `--text-${'primary' | 'secondary' | 'disabled'}`
| '--divider'
| `--action-${'active' | 'hover' | 'selected' | 'disabled' | 'disabled-background'}`