A personal media tracker built on the AT Protocol
opnshelf.xyz
1import {
2 argbFromHex,
3 themeFromSourceColor,
4} from "@material/material-color-utilities";
5
6export interface MaterialThemeColors {
7 // Primary palette
8 primary: string;
9 onPrimary: string;
10 primaryContainer: string;
11 onPrimaryContainer: string;
12
13 // Secondary palette
14 secondary: string;
15 onSecondary: string;
16 secondaryContainer: string;
17 onSecondaryContainer: string;
18
19 // Tertiary palette
20 tertiary: string;
21 onTertiary: string;
22 tertiaryContainer: string;
23 onTertiaryContainer: string;
24
25 // Error palette
26 error: string;
27 onError: string;
28 errorContainer: string;
29 onErrorContainer: string;
30
31 // Surface colors
32 surface: string;
33 onSurface: string;
34 surfaceVariant: string;
35 onSurfaceVariant: string;
36 outline: string;
37 outlineVariant: string;
38
39 // Inverse colors
40 inverseSurface: string;
41 onInverseSurface: string;
42 inversePrimary: string;
43
44 // Scrim
45 scrim: string;
46 shadow: string;
47
48 // Surface tints for elevation
49 surfaceTint: string;
50}
51
52function argbToHex(argb: number): string {
53 const r = (argb >> 16) & 0xff;
54 const g = (argb >> 8) & 0xff;
55 const b = argb & 0xff;
56 return `#${[r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("")}`;
57}
58
59export function generateMaterialTheme(
60 seedColor: string,
61 isDark = true,
62): MaterialThemeColors {
63 const argb = argbFromHex(seedColor);
64 const theme = themeFromSourceColor(argb, []);
65 const scheme = isDark ? theme.schemes.dark : theme.schemes.light;
66
67 const getColor = (color: number) => argbToHex(color);
68
69 return {
70 // Primary
71 primary: getColor(scheme.primary),
72 onPrimary: getColor(scheme.onPrimary),
73 primaryContainer: getColor(scheme.primaryContainer),
74 onPrimaryContainer: getColor(scheme.onPrimaryContainer),
75
76 // Secondary
77 secondary: getColor(scheme.secondary),
78 onSecondary: getColor(scheme.onSecondary),
79 secondaryContainer: getColor(scheme.secondaryContainer),
80 onSecondaryContainer: getColor(scheme.onSecondaryContainer),
81
82 // Tertiary
83 tertiary: getColor(scheme.tertiary),
84 onTertiary: getColor(scheme.onTertiary),
85 tertiaryContainer: getColor(scheme.tertiaryContainer),
86 onTertiaryContainer: getColor(scheme.onTertiaryContainer),
87
88 // Error
89 error: getColor(scheme.error),
90 onError: getColor(scheme.onError),
91 errorContainer: getColor(scheme.errorContainer),
92 onErrorContainer: getColor(scheme.onErrorContainer),
93
94 // Surface
95 surface: getColor(scheme.surface),
96 onSurface: getColor(scheme.onSurface),
97 surfaceVariant: getColor(scheme.surfaceVariant),
98 onSurfaceVariant: getColor(scheme.onSurfaceVariant),
99 outline: getColor(scheme.outline),
100 outlineVariant: getColor(scheme.outlineVariant),
101
102 // Inverse
103 inverseSurface: getColor(scheme.inverseSurface),
104 onInverseSurface: getColor(scheme.inverseOnSurface),
105 inversePrimary: getColor(scheme.inversePrimary),
106
107 // Scrim & Shadow
108 scrim: getColor(scheme.scrim),
109 shadow: getColor(scheme.shadow),
110
111 // Surface tint (for elevation overlays)
112 surfaceTint: getColor(scheme.primary),
113 };
114}
115
116export function applyThemeToDocument(
117 colors: MaterialThemeColors,
118 element: HTMLElement = document.documentElement,
119): void {
120 const prefix = "--md-sys-color-";
121
122 for (const [key, value] of Object.entries(colors)) {
123 const cssKey = prefix + key.replace(/([A-Z])/g, "-$1").toLowerCase();
124 element.style.setProperty(cssKey, value);
125 }
126}
127
128// Default amber seed color for OpnShelf
129export const DEFAULT_SEED_COLOR = "#F59E0B";