Add self-contained docker/ stack for the web UI
`docker/` now holds the whole web build — frontend (Vite + React + CanvasKit),
backend (Fastify + SQLite) and the compose file — so the folder can be moved to
another machine and run without the React Native project:
cd docker && cp .env.example .env && docker compose up -d --build
Only `${WEB_PORT:-8090}` is published; nginx serves the SPA and proxies /api to
the `api` container over Docker's DNS. Photos never reach the server.
The shared render code is vendored into `docker/frontend/shared/` and aliased to
a CanvasKit shim, so the app's own frameUtils/toneShader/jpegDpi run unchanged.
Fix the all-black render on GPU surfaces: `MakeWebGLCanvasSurface` creates a
separate WebGL context per call, and a texture from one context cannot be
sampled by a surface on another — so any pass that drew a snapshot onto a second
surface (output sharpen, screen sharpen, polaroid/wallframe cards) came out
solid black, while the raster fallback was correct. Use one shared
GrDirectContext + MakeRenderTarget instead.
Verified in headless Chromium against the running stack: 12MP JPEG in, preview
mean=120.5 sd=60.5, export 2048x1536 mean=107.2 sd=62.1, JFIF density 300/300,
EXIF present, no console errors; health/signup/login/me/recipes all 2xx through
the nginx proxy.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||
import { ACCENTS, applyTheme, readAccent, readTheme, type AccentId, type ThemeMode } from './themes';
|
||||
|
||||
interface ThemeValue {
|
||||
mode: ThemeMode;
|
||||
accent: AccentId;
|
||||
setMode: (mode: ThemeMode) => void;
|
||||
setAccent: (accent: AccentId) => void;
|
||||
}
|
||||
|
||||
const Ctx = createContext<ThemeValue | null>(null);
|
||||
|
||||
// The tokens live in CSS; this provider only owns the two attributes on <html>
|
||||
// and the persisted choice. No component ever reads a colour from JS.
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [mode, setModeState] = useState<ThemeMode>(readTheme);
|
||||
const [accent, setAccentState] = useState<AccentId>(readAccent);
|
||||
|
||||
useEffect(() => {
|
||||
applyTheme(mode, accent);
|
||||
}, [mode, accent]);
|
||||
|
||||
const setMode = useCallback((next: ThemeMode) => setModeState(next), []);
|
||||
const setAccent = useCallback((next: AccentId) => setAccentState(next), []);
|
||||
|
||||
const value = useMemo(() => ({ mode, accent, setMode, setAccent }), [mode, accent, setMode, setAccent]);
|
||||
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
||||
}
|
||||
|
||||
export function useTheme(): ThemeValue {
|
||||
const value = useContext(Ctx);
|
||||
if (!value) throw new Error('useTheme used outside ThemeProvider');
|
||||
return value;
|
||||
}
|
||||
|
||||
export { ACCENTS };
|
||||
export type { AccentId, ThemeMode };
|
||||
@@ -0,0 +1,41 @@
|
||||
// Theme data only — the CSS in styles/tokens.css owns the values. Adding an
|
||||
// accent group is one entry here plus one 1-line block in the stylesheet.
|
||||
|
||||
export type ThemeMode = 'light' | 'dark';
|
||||
export type AccentId = 'amber' | 'blue' | 'emerald' | 'rose' | 'violet';
|
||||
|
||||
export const ACCENTS: { id: AccentId; label: string }[] = [
|
||||
{ id: 'amber', label: 'Amber' },
|
||||
{ id: 'blue', label: 'Blue' },
|
||||
{ id: 'emerald', label: 'Emerald' },
|
||||
{ id: 'rose', label: 'Rose' },
|
||||
{ id: 'violet', label: 'Violet' },
|
||||
];
|
||||
|
||||
export const THEME_COLOR: Record<ThemeMode, string> = {
|
||||
light: '#ffffff',
|
||||
dark: '#0a0a0a',
|
||||
};
|
||||
|
||||
const KEY_THEME = 'rc.theme';
|
||||
const KEY_ACCENT = 'rc.accent';
|
||||
|
||||
export function readTheme(): ThemeMode {
|
||||
const stored = localStorage.getItem(KEY_THEME);
|
||||
if (stored === 'light' || stored === 'dark') return stored;
|
||||
return window.matchMedia?.('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
export function readAccent(): AccentId {
|
||||
const stored = localStorage.getItem(KEY_ACCENT);
|
||||
return ACCENTS.some((a) => a.id === stored) ? (stored as AccentId) : 'amber';
|
||||
}
|
||||
|
||||
export function applyTheme(mode: ThemeMode, accent: AccentId): void {
|
||||
const root = document.documentElement;
|
||||
root.dataset.theme = mode;
|
||||
root.dataset.accent = accent;
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', THEME_COLOR[mode]);
|
||||
localStorage.setItem(KEY_THEME, mode);
|
||||
localStorage.setItem(KEY_ACCENT, accent);
|
||||
}
|
||||
Reference in New Issue
Block a user