@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
411 lines (381 loc) • 11.6 kB
text/typescript
import type { Token } from '@furystack/inject'
import { defineService } from '@furystack/inject'
import { EventHub, type DeepPartial } from '@furystack/utils'
import { cssVariableTheme, useThemeCssVariables } from './css-variable-theme.js'
/**
* Represents a CSS color value.
* Can be a hex color, rgba, or CSS variable reference.
* @example '#3f51b5', 'rgba(255, 255, 255, 0.7)', 'var(--my-color)'
*/
export type Color = string
/**
* Color variants for a palette color with their corresponding contrast text colors.
* Each variant (light, main, dark) has an associated contrast color that should be
* used for text or icons displayed on top of that variant's background.
*/
export type ColorVariants = {
/** The lighter shade of the color */
light: Color
/** Text/icon color that contrasts well with the light variant */
lightContrast: Color
/** The primary/default shade of the color */
main: Color
/** Text/icon color that contrasts well with the main variant */
mainContrast: Color
/** The darker shade of the color */
dark: Color
/** Text/icon color that contrasts well with the dark variant */
darkContrast: Color
}
/**
* The color palette containing semantic colors for the application.
* Each color has light, main, and dark variants with corresponding contrast colors.
*/
export interface Palette {
/** Primary brand color, used for main actions and emphasis */
primary: ColorVariants
/** Secondary brand color, used for less prominent actions */
secondary: ColorVariants
/** Color indicating errors or destructive actions */
error: ColorVariants
/** Color indicating warnings or caution */
warning: ColorVariants
/** Color indicating success or positive outcomes */
success: ColorVariants
/** Color for informational content */
info: ColorVariants
}
/**
* Text color definitions for different emphasis levels.
*/
export interface Text {
/** High-emphasis text color for important content */
primary: Color
/** Medium-emphasis text color for secondary content */
secondary: Color
/** Low-emphasis text color for disabled or hint text */
disabled: Color
}
/**
* Button-specific color definitions for various states.
*/
export interface ButtonColor {
/** Color when button is actively pressed */
active: Color
/** Background color on hover */
hover: Color
/** Background color when selected/checked */
selected: Color
/** Text color when button is disabled */
disabled: Color
/** Background color when button is disabled */
disabledBackground: Color
}
/**
* Background color definitions for different surface levels.
*/
export interface Background {
/** Default page/app background color */
default: Color
/** Elevated surface background (cards, dialogs, etc.) */
paper: Color
/** CSS background-image for paper surfaces (e.g. a tiled texture). Use 'none' for no image. */
paperImage: string
}
/**
* Interactive state colors for hover, selection, focus and overlay backgrounds.
*/
export type ActionColors = {
/** Background color on hover for interactive elements */
hoverBackground: Color
/** Background color for selected/checked elements */
selectedBackground: Color
/** Background color for actively pressed elements */
activeBackground: Color
/** Box-shadow value for focus ring indicators */
focusRing: string
/** CSS outline value for keyboard/spatial focus indicators (e.g. '2px solid #3f51b5') */
focusOutline: string
/** Opacity value for disabled elements (e.g. '0.6') */
disabledOpacity: string
/** Overlay background color for backdrops (drawers, modals) */
backdrop: Color
/** Subtle border color for structural dividers, input borders, and dropdown outlines */
subtleBorder: Color
}
/**
* Border radius scale for consistent rounded corners.
*/
export type BorderRadiusScale = {
/** 2px - grid cells, small elements */
xs: string
/** 4px - badges, compact items */
sm: string
/** 8px - buttons, paper, inputs, cards */
md: string
/** 12px - suggest, command palette */
lg: string
/** 50% - circular elements (avatar, FAB, loader) */
full: string
}
/**
* Shape tokens for geometric properties.
*/
export type Shape = {
/** Border radius scale */
borderRadius: BorderRadiusScale
/** Border width for surface components (paper, card, etc.). Use '0px' for no border. */
borderWidth: string
}
/**
* Elevation shadow presets from subtle to prominent.
*/
export type Shadows = {
/** No shadow */
none: string
/** Subtle shadow (paper elevation 1, notifications) */
sm: string
/** Medium shadow (paper elevation 2, context-menu, dropdowns) */
md: string
/** Large shadow (paper elevation 3, modals, overlays) */
lg: string
/** Extra large shadow (floating elements like FAB, avatar hover) */
xl: string
}
/**
* Font size scale for consistent text sizing.
*/
export type FontSizeScale = {
/** 11px - labels, helper text */
xs: string
/** 13px - small body, secondary text */
sm: string
/** 14px - body text, buttons */
md: string
/** 16px - large body, icons */
lg: string
/** 24px - subheadings, large UI elements */
xl: string
/** 30px - small headings (h3) */
xxl: string
/** 36px - medium headings (h2) */
xxxl: string
/** 48px - large headings (h1) */
xxxxl: string
}
/**
* Font weight scale.
*/
export type FontWeightScale = {
/** 400 - normal weight */
normal: string
/** 500 - medium weight */
medium: string
/** 600 - semibold weight */
semibold: string
/** 700 - bold weight */
bold: string
}
/**
* Line height scale.
*/
export type LineHeightScale = {
/** 1.3 - headings, compact text */
tight: string
/** 1.5 - standard body text */
normal: string
/** 1.75 - relaxed spacing */
relaxed: string
}
/**
* Letter spacing scale for consistent character spacing.
*/
export type LetterSpacingScale = {
/** -0.5px - tight spacing for large display text */
tight: string
/** -0.25px - slightly tight spacing for headings */
dense: string
/** 0px - default spacing */
normal: string
/** 0.15px - slightly wider for body text */
wide: string
/** 0.5px - wider for buttons and labels */
wider: string
/** 1.5px - widest for overline/caption text */
widest: string
}
/**
* Typography tokens for text styling.
*/
export type ThemeTypography = {
/** Base font family stack */
fontFamily: string
/** Font size scale */
fontSize: FontSizeScale
/** Font weight scale */
fontWeight: FontWeightScale
/** Line height scale */
lineHeight: LineHeightScale
/** Letter spacing scale */
letterSpacing: LetterSpacingScale
/** CSS text-shadow value applied globally to text. Use 'none' for no shadow. */
textShadow: string
}
/**
* Transition duration presets.
*/
export type TransitionDurations = {
/** 150ms - micro-interactions (hover, active states) */
fast: string
/** 200ms - default transitions */
normal: string
/** 300ms - layout changes, drawers */
slow: string
}
/**
* Transition easing presets.
*/
export type TransitionEasings = {
/** Standard Material easing - cubic-bezier(0.4, 0, 0.2, 1) */
default: string
/** Decelerate easing - cubic-bezier(0.23, 1.0, 0.32, 1.0) */
easeOut: string
/** Symmetric easing - ease-in-out */
easeInOut: string
}
/**
* Transition timing tokens for animations and state changes.
*/
export type Transitions = {
/** Duration presets */
duration: TransitionDurations
/** Easing function presets */
easing: TransitionEasings
}
/**
* Spacing scale for consistent padding, margins and gaps.
*/
export type Spacing = {
/** 4px */
xs: string
/** 8px */
sm: string
/** 16px */
md: string
/** 24px */
lg: string
/** 32px */
xl: string
}
/**
* Z-index layer scale for consistent stacking context.
*/
export type ZIndex = {
/** 1000 - drawers and sidebar panels */
drawer: string
/** 1100 - app bars and sticky headers */
appBar: string
/** 1200 - modals and dialogs */
modal: string
/** 1300 - tooltips and popovers */
tooltip: string
/** 1400 - dropdowns and context menus */
dropdown: string
}
/**
* Visual effect tokens for blur and backdrop effects.
*/
export type Effects = {
/** 4px - subtle blur for glassy surfaces */
blurSm: string
/** 8px - medium blur for overlays */
blurMd: string
/** 15px - strong blur for app bar / prominent overlays */
blurLg: string
/** 20px - heavy blur for command palette / suggestion lists */
blurXl: string
}
/**
* Complete theme definition containing all design tokens for the application.
* Themes can be switched at runtime to support light/dark modes or custom branding.
*
* **Future extension — Component size variants:**
* When introducing size variants for form controls (e.g. sm/md/lg Button, Input),
* add a `componentDefaults` section to this interface:
*
* ```typescript
* type ComponentSize = 'sm' | 'md' | 'lg'
* type SizeTokens = { height: string; padding: string; fontSize: string; borderRadius: string; iconSize: string }
* componentDefaults: { size: Record<ComponentSize, SizeTokens> }
* ```
*
* The existing `typography.fontSize` and `shape.borderRadius` scales provide
* the building blocks that size tokens should reference.
*/
export interface Theme {
/** Unique identifier for the theme */
name: string
/** Semantic color palette */
palette: Palette
/** Text colors for different emphasis levels */
text: Text
/** Button-specific colors */
button: ButtonColor
/** Background colors */
background: Background
/** Color for dividers and borders */
divider: Color
/** Interactive state colors */
action: ActionColors
/** Shape tokens (border radius) */
shape: Shape
/** Elevation shadow presets */
shadows: Shadows
/** Typography scale (font sizes, weights, line heights, letter spacing) */
typography: ThemeTypography
/** Transition timing tokens */
transitions: Transitions
/** Spacing scale */
spacing: Spacing
/** Z-index stacking layers */
zIndex: ZIndex
/** Visual effect tokens (blur, backdrop) */
effects: Effects
}
export type ThemeProviderServiceEvents = { themeChanged: DeepPartial<Theme> }
/**
* Service for theme-related operations.
*/
export interface ThemeProviderService extends EventHub<ThemeProviderServiceEvents> {
readonly theme: typeof cssVariableTheme
/** Returns the last assigned theme object. */
getAssignedTheme(): DeepPartial<Theme>
/**
* Assigns a new theme, updates the CSS variables and emits a themeChanged event.
* @param theme The Theme instance.
* @param root Optional HTML element to scope CSS variables to. Defaults to `:root`.
*/
setAssignedTheme(theme: DeepPartial<Theme>, root?: HTMLElement): void
}
export const ThemeProviderService: Token<ThemeProviderService, 'singleton'> = defineService({
name: '@furystack/shades-common-components/ThemeProviderService',
lifetime: 'singleton',
factory: ({ onDispose }) => {
const hub = new EventHub<ThemeProviderServiceEvents>()
let assignedTheme: DeepPartial<Theme> = cssVariableTheme
const setAssignedTheme = (theme: DeepPartial<Theme>, root?: HTMLElement): void => {
assignedTheme = theme
useThemeCssVariables(theme, root)
hub.emit('themeChanged', theme)
}
onDispose(() => {
// eslint-disable-next-line furystack/prefer-using-wrapper -- Disposal is deferred to the injector's onDispose hook.
hub[Symbol.dispose]?.()
})
return Object.assign(hub, {
theme: cssVariableTheme,
getAssignedTheme: () => assignedTheme,
setAssignedTheme,
})
},
})