Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions lib/design-system/tokens/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import type { ColorPaletteProp, VariantProp } from "@mui/joy";
import type { RotationBin, Genre, Format } from "@wxyc/shared";

/**
* Semantic Color Token System
*
* This module provides meaningful, domain-specific color abstractions while
* maintaining backwards compatibility with MUI Joy's color system.
*
* Benefits:
* - Single source of truth for all color decisions
* - Type safety (TypeScript catches invalid color usage at compile time)
* - Consistency across the codebase
* - Semantic clarity in code
*/

// =============================================================================
// TYPES
// =============================================================================

/** Semantic color result with optional variant */
export interface SemanticColor {
color: ColorPaletteProp;
variant?: VariantProp;
}

// =============================================================================
// ROTATION COLORS - Single source of truth
// =============================================================================

/**
* Rotation bin colors indicate radio station rotation frequency.
* - H (Heavy): High rotation, most played - danger (red, attention-grabbing)
* - M (Medium): Medium rotation - warning (yellow/amber)
* - L (Light): Light rotation - success (green)
* - S (Special): Special rotation - primary (blue, distinctive)
*/
const ROTATION_COLORS: Record<RotationBin, ColorPaletteProp> = {
H: "danger",
M: "warning",
L: "success",
S: "primary",
};

/** Get the color for a rotation bin */
export function getRotationColor(bin: RotationBin): ColorPaletteProp {
return ROTATION_COLORS[bin];
}

/** Backwards compatibility export */
export const RotationStyles = ROTATION_COLORS;

// =============================================================================
// GENRE COLORS
// =============================================================================

/**
* Genre colors categorize music by genre.
* Each genre has both a color and variant for visual distinction.
*/
const GENRE_COLORS: Record<Genre, SemanticColor> = {
Rock: { color: "primary", variant: "solid" },
Blues: { color: "success", variant: "soft" },
Electronic: { color: "success", variant: "solid" },
Hiphop: { color: "primary", variant: "soft" },
Jazz: { color: "warning", variant: "solid" },
Classical: { color: "neutral", variant: "soft" },
Reggae: { color: "warning", variant: "soft" },
Soundtracks: { color: "neutral", variant: "soft" },
OCS: { color: "success", variant: "soft" },
Unknown: { color: "neutral", variant: "soft" },
};

/** Get the color and variant for a genre */
export function getGenreColor(genre: Genre): SemanticColor {
return GENRE_COLORS[genre] ?? GENRE_COLORS.Unknown;
}

// =============================================================================
// FORMAT COLORS
// =============================================================================

/**
* Format colors distinguish physical media types.
* - Vinyl: primary (blue, classic/premium)
* - CD: warning (amber, standard)
* - Unknown/Digital: neutral (gray)
*/
const FORMAT_COLORS: Record<Format, ColorPaletteProp> = {
Vinyl: "primary",
CD: "warning",
Unknown: "neutral",
};

/** Get the color for a format */
export function getFormatColor(format: Format): ColorPaletteProp {
return FORMAT_COLORS[format] ?? FORMAT_COLORS.Unknown;
}

// =============================================================================
// ENTRY TYPE COLORS
// =============================================================================

/** Flowsheet entry types */
export type FlowsheetEntryType =
| "startShow"
| "endShow"
| "talkset"
| "breakpoint"
| "message";

/**
* Entry type colors differentiate flowsheet entry types.
* - startShow: success (green, positive start)
* - endShow: primary (blue, completion)
* - talkset: danger (red, attention/break)
* - breakpoint: warning (amber, marker)
* - message: warning (amber, informational)
*/
const ENTRY_TYPE_COLORS: Record<FlowsheetEntryType, ColorPaletteProp> = {
startShow: "success",
endShow: "primary",
talkset: "danger",
breakpoint: "warning",
message: "warning",
};

/** Get the color for a flowsheet entry type */
export function getEntryTypeColor(type: FlowsheetEntryType): ColorPaletteProp {
return ENTRY_TYPE_COLORS[type];
}

// =============================================================================
// ACTION COLORS
// =============================================================================

/**
* Action colors indicate interactive element intent.
* Use these for buttons and clickable elements.
*/
export const ActionColors = {
/** Main CTA (submit, save) */
primary: "primary" as const,
/** Secondary actions */
secondary: "neutral" as const,
/** Delete, remove */
destructive: "danger" as const,
/** Add, create */
constructive: "success" as const,
};

// =============================================================================
// STATUS COLORS
// =============================================================================

/**
* Status colors indicate current state of UI elements.
*/
export const StatusColors = {
/** Currently playing track */
playing: "primary" as const,
/** Track in queue */
queued: "success" as const,
/** Not active/playing */
inactive: "neutral" as const,
/** Show is live */
live: "primary" as const,
/** Show is offline */
offline: "neutral" as const,
/** Item is selected */
selected: "primary" as const,
};

// =============================================================================
// FEEDBACK COLORS
// =============================================================================

/**
* Feedback colors communicate operation results.
*/
export const FeedbackColors = {
/** Operation succeeded */
success: "success" as const,
/** Operation failed */
error: "danger" as const,
/** Caution/attention needed */
warning: "warning" as const,
};
26 changes: 23 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 14 additions & 55 deletions src/components/experiences/modern/catalog/ArtistAvatar.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,23 @@
import { ArtistEntry, Format, Genre } from "@/lib/features/catalog/types";
import { RotationStyles } from "@/src/utilities/modern/rotationstyles";
import type { RotationBin } from "@wxyc/shared";
import {
Avatar,
Badge,
ColorPaletteProp,
Stack,
Tooltip,
Typography,
VariantProp,
} from "@mui/joy";
getGenreColor,
getRotationColor,
getFormatColor,
} from "@/lib/design-system/tokens";
import type { RotationBin } from "@wxyc/shared";
import { Avatar, Badge, Stack, Tooltip, Typography } from "@mui/joy";

interface ArtistAvatarProps {
artist?: ArtistEntry;
entry?: number;
background?: string;
rotation?: RotationBin;
format?: Format;
format: Format;
}

// Re-export for backwards compatibility
export const ROTATION_STYLES = RotationStyles;

const GENRE_COLORS: { [id in Genre]: ColorPaletteProp } = {
Rock: "primary",
Blues: "success",
Electronic: "success",
Hiphop: "primary",
Jazz: "warning",
Classical: "neutral",
Reggae: "warning",
Soundtracks: "neutral",
OCS: "success",
Unknown: "neutral",
};

const GENRE_VARIANTS: { [id in Genre]: VariantProp } = {
Rock: "solid",
Electronic: "solid",
Hiphop: "soft",
Jazz: "solid",
Blues: "soft",
Classical: "soft",
Reggae: "soft",
Soundtracks: "soft",
OCS: "soft",
Unknown: "soft",
};

export const ArtistAvatar = (props: ArtistAvatarProps): JSX.Element => {
let color_choice = GENRE_COLORS[(props.artist?.genre as Genre) ?? "Unknown"];
if (color_choice === undefined) {
color_choice = "neutral";
}

let variant_choice =
GENRE_VARIANTS[(props.artist?.genre as Genre) ?? "Unknown"];
if (variant_choice === undefined) {
variant_choice = "solid";
}
const genre = (props.artist?.genre as Genre) ?? "Unknown";
const { color: genreColor, variant: genreVariant } = getGenreColor(genre);

return (
<Tooltip
Expand All @@ -72,12 +31,12 @@ export const ArtistAvatar = (props: ArtistAvatarProps): JSX.Element => {
>
<Badge
badgeContent={props.rotation ?? null}
color={props.rotation && ROTATION_STYLES[props.rotation]}
color={props.rotation ? getRotationColor(props.rotation) : undefined}
size="sm"
>
<Avatar
variant={variant_choice}
color={color_choice}
variant={genreVariant}
color={genreColor}
sx={{
width: "3.2rem",
height: "3.2rem",
Expand Down Expand Up @@ -115,8 +74,8 @@ export const ArtistAvatar = (props: ArtistAvatarProps): JSX.Element => {
{props.artist?.numbercode ?? "|"}
</Typography>
<Avatar
variant={variant_choice == "solid" ? "soft" : "solid"}
color={(props.format ?? "") == "CD" ? "primary" : "warning"}
variant={genreVariant === "solid" ? "soft" : "solid"}
color={getFormatColor(props.format)}
sx={{
width: "1.4rem",
height: "1.4rem",
Expand Down
3 changes: 2 additions & 1 deletion src/components/experiences/modern/catalog/Results/Result.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { AlbumEntry } from "@/lib/features/catalog/types";
import { getFormatColor } from "@/lib/design-system/tokens";
import Box from "@mui/joy/Box";
import Checkbox from "@mui/joy/Checkbox";
import Chip from "@mui/joy/Chip";
Expand Down Expand Up @@ -84,7 +85,7 @@ export default function CatalogResult({ album }: { album: AlbumEntry }) {
<Chip
variant="soft"
size="sm"
color={album.format.includes("Vinyl") ? "primary" : "warning"}
color={getFormatColor(album.format)}
>
{album.format}
</Chip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { flowsheetSlice } from "@/lib/features/flowsheet/frontend";
import { useAppDispatch, useAppSelector } from "@/lib/hooks";
import { ArtistAvatar } from "@/src/components/experiences/modern/catalog/ArtistAvatar";
import { useFlowsheetSubmit } from "@/src/hooks/flowsheetHooks";
import { Chip, ColorPaletteProp, Stack, Typography } from "@mui/joy";
import { getFormatColor } from "@/lib/design-system/tokens";
import { Chip, Stack, Typography } from "@mui/joy";

export default function FlowsheetBackendResult({
entry,
Expand Down Expand Up @@ -69,16 +70,12 @@ export default function FlowsheetBackendResult({
<Chip
variant="soft"
size="sm"
color={
(entry.format.includes("vinyl")
? "primary"
: "info") as ColorPaletteProp
}
color={getFormatColor(entry.format)}
sx={{
ml: 2,
}}
>
{entry.format.includes("vinyl") ? "vinyl" : "cd"}
{entry.format}
</Chip>
</Typography>
</Stack>
Expand Down
Loading