feat(abacus-react): add automatic theme detection for numeral colors
Implement Option 3: Make AbacusReact theme-aware to automatically
adjust numeral colors based on the document's theme.
**New Features:**
- Add `useSystemTheme` hook that detects theme from document root
- Watches for `data-theme` attribute changes
- Watches for `.light` / `.dark` class changes
- Returns 'light' or 'dark' theme
- SSR-safe with proper fallback
**Changes:**
- AbacusReact now automatically sets dark numeral color (rgba(0,0,0,0.8))
as default when no custom color is provided
- Works on white/translucent abacus frames in both light/dark page themes
- Users can still override with custom `customStyles.numerals.color`
- Theme detection uses MutationObserver for automatic updates
**Exports:**
- Export `useSystemTheme` hook for consumer use
- Export `SystemTheme` type ('light' | 'dark')
**Benefits:**
- Numerals always visible regardless of page theme
- No manual coordination needed
- Works automatically with web app's ThemeContext
- Zero breaking changes (respects existing customStyles)
Fixes numeral visibility issue where white numerals appeared on
white abacus frames in dark mode.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
} from "./AbacusUtils";
|
||||
import { AbacusSVGRenderer } from "./AbacusSVGRenderer";
|
||||
import { AbacusAnimatedBead } from "./AbacusAnimatedBead";
|
||||
import { useSystemTheme } from "./hooks/useSystemTheme";
|
||||
import "./Abacus3D.css";
|
||||
|
||||
// Types
|
||||
@@ -1663,6 +1664,9 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
return columns;
|
||||
}, [columns, value, showEmptyColumns]);
|
||||
|
||||
// Detect system theme for automatic numeral color adjustment
|
||||
const systemTheme = useSystemTheme();
|
||||
|
||||
// Switch to place-value architecture!
|
||||
const maxPlaceValue = (effectiveColumns - 1) as ValidPlaceValues;
|
||||
const {
|
||||
@@ -2247,6 +2251,26 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
],
|
||||
);
|
||||
|
||||
// Merge theme-aware numeral colors into customStyles
|
||||
// Default numeral color is dark (rgba(0,0,0,0.8)) which works on white/light abacus frames
|
||||
// Only override if user hasn't explicitly set numeral color
|
||||
const themeAwareCustomStyles = useMemo(() => {
|
||||
if (!customStyles?.numerals?.color) {
|
||||
// User hasn't set a custom numeral color, so we use theme-aware default
|
||||
// Keep numerals dark regardless of theme, since abacus frame is typically white/light
|
||||
return {
|
||||
...customStyles,
|
||||
numerals: {
|
||||
...customStyles?.numerals,
|
||||
color: "rgba(0, 0, 0, 0.8)",
|
||||
fontWeight: customStyles?.numerals?.fontWeight || "600",
|
||||
},
|
||||
};
|
||||
}
|
||||
// User has set custom color, respect it
|
||||
return customStyles;
|
||||
}, [customStyles, systemTheme]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={containerClasses}
|
||||
@@ -2289,7 +2313,7 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
hideInactiveBeads={finalConfig.hideInactiveBeads}
|
||||
frameVisible={finalConfig.frameVisible}
|
||||
showNumbers={false}
|
||||
customStyles={customStyles}
|
||||
customStyles={themeAwareCustomStyles}
|
||||
interactive={finalConfig.interactive}
|
||||
highlightColumns={highlightColumns}
|
||||
columnLabels={columnLabels}
|
||||
|
||||
86
packages/abacus-react/src/hooks/useSystemTheme.ts
Normal file
86
packages/abacus-react/src/hooks/useSystemTheme.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Hook to detect the current theme from the document root
|
||||
* Works with theme systems that set data-theme attribute or class on <html>
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export type SystemTheme = "light" | "dark";
|
||||
|
||||
/**
|
||||
* Detects the current theme from the document root
|
||||
* Looks for:
|
||||
* 1. data-theme="light" or data-theme="dark" attribute
|
||||
* 2. .light or .dark class on document.documentElement
|
||||
* 3. Falls back to "dark" as default
|
||||
*
|
||||
* @returns Current theme ("light" or "dark")
|
||||
*/
|
||||
export function useSystemTheme(): SystemTheme {
|
||||
const [theme, setTheme] = useState<SystemTheme>(() => {
|
||||
// SSR-safe initialization
|
||||
if (typeof window === "undefined") {
|
||||
return "dark";
|
||||
}
|
||||
|
||||
// Check data-theme attribute
|
||||
const root = document.documentElement;
|
||||
const dataTheme = root.getAttribute("data-theme");
|
||||
if (dataTheme === "light" || dataTheme === "dark") {
|
||||
return dataTheme;
|
||||
}
|
||||
|
||||
// Check for class
|
||||
if (root.classList.contains("light")) return "light";
|
||||
if (root.classList.contains("dark")) return "dark";
|
||||
|
||||
// Default
|
||||
return "dark";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
|
||||
// Update theme when data-theme attribute changes
|
||||
const updateTheme = () => {
|
||||
const dataTheme = root.getAttribute("data-theme");
|
||||
if (dataTheme === "light" || dataTheme === "dark") {
|
||||
setTheme(dataTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for class changes
|
||||
if (root.classList.contains("light")) {
|
||||
setTheme("light");
|
||||
} else if (root.classList.contains("dark")) {
|
||||
setTheme("dark");
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for attribute changes
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (
|
||||
mutation.type === "attributes" &&
|
||||
(mutation.attributeName === "data-theme" ||
|
||||
mutation.attributeName === "class")
|
||||
) {
|
||||
updateTheme();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(root, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme", "class"],
|
||||
});
|
||||
|
||||
// Initial update
|
||||
updateTheme();
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return theme;
|
||||
}
|
||||
@@ -68,3 +68,6 @@ export type {
|
||||
} from "./AbacusUtils";
|
||||
|
||||
export { useAbacusDiff, useAbacusState } from "./AbacusHooks";
|
||||
|
||||
export { useSystemTheme } from "./hooks/useSystemTheme";
|
||||
export type { SystemTheme } from "./hooks/useSystemTheme";
|
||||
|
||||
Reference in New Issue
Block a user