All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m44s
CI / Build container images (pull_request) Successful in 4m2s
CI / Auth e2e pack (pull_request) Successful in 10m50s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 17s
CD / Smoke tests against Test (push) Successful in 4m2s
CI / Lint, typecheck, test (push) Successful in 4m47s
CI / Build container images (push) Has been skipped
CD / Promote to Int (push) Successful in 14s
CI / Auth e2e pack (push) Successful in 10m7s
CI / Import/export fidelity gate (push) Successful in 56s
Release / Build release images and notes (push) Successful in 1m11s
Release / Release-candidate operations QA (push) Successful in 1m0s
Prod deploy / Deploy the released images to Prod (push) Successful in 17s
pondSettingsSchema gains theme = { accent: '#rrggbb' | null } (null =
inherit the viewer's theme), exposed as a top-level key of the flat
updatePondInputSchema and included in the PondsService settings merge
(the known silent-no-op pitfall). The server validates only the hex;
conformance arises at render time: PondThemeScope (mounted around the
page content next to PondFontScope) derives the accent pair for the
EFFECTIVE mode via useEffectiveTheme and sets it as inline custom
properties — inline beats both tokens.css and the user-theme <style>,
which IS the cascade precedence pond > user > default.
Pond settings get a PondThemeSection (inherit | presets | custom color
with per-mode preview swatches, explicit save like the font manager);
AccentSwatches extracted for reuse; i18n de+en. The no-JS public shell
stays deliberately un-themed (ADR 0018 amendment).
Tests: pond DB test (theme merge keeps fonts, invalid hex 400), e2e
pond-theme.spec (scope boundary content vs. chrome, per-mode
re-derivation, axe on the pond settings page; resets the fixture pond).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRtCnB3uLdQtFmvp9HXcRX
123 lines
4.1 KiB
TypeScript
123 lines
4.1 KiB
TypeScript
import { deriveAccentTokens, THEME_PRESETS, type PondTheme } from '@dorfteich/shared';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { FormError } from '../components/forms';
|
|
import { apiPatch } from '../lib/api';
|
|
import { AccentSwatches } from './AccentSwatches';
|
|
|
|
/**
|
|
* Pond accent setting (issue #186, ADR 0018 stage C): inherit | preset |
|
|
* custom color, applied to the pond's content via PondThemeScope. Explicit
|
|
* save like the font manager (AppearanceManager); Pond-Admin-gated by the
|
|
* api's PATCH guard. Presets here include the default green — unlike the
|
|
* user setting, picking it PINS the pond to green for every viewer.
|
|
*/
|
|
export function PondThemeSection({
|
|
pondId,
|
|
pondSlug,
|
|
theme,
|
|
}: {
|
|
pondId: string;
|
|
pondSlug: string;
|
|
theme: PondTheme;
|
|
}): React.JSX.Element {
|
|
const { t } = useTranslation('font');
|
|
const { t: tSettings } = useTranslation('settings');
|
|
const queryClient = useQueryClient();
|
|
const [accent, setAccent] = useState<string | null>(theme.accent);
|
|
const [status, setStatus] = useState<'idle' | 'saving' | 'saved'>('idle');
|
|
const [error, setError] = useState<unknown>(null);
|
|
|
|
const presetHexes = THEME_PRESETS.map((preset) => preset.accent);
|
|
const isCustom = accent !== null && !presetHexes.includes(accent);
|
|
const [customHex, setCustomHex] = useState(isCustom ? accent : '#2f6f4f');
|
|
|
|
const choose = (value: string | null): void => {
|
|
setAccent(value);
|
|
setStatus('idle');
|
|
};
|
|
|
|
const derivedPair = (hex: string): { light: string; dark: string } => ({
|
|
light: deriveAccentTokens(hex, 'light').accent,
|
|
dark: deriveAccentTokens(hex, 'dark').accent,
|
|
});
|
|
|
|
async function save(): Promise<void> {
|
|
setStatus('saving');
|
|
setError(null);
|
|
try {
|
|
await apiPatch(`/ponds/${pondId}`, { theme: { accent } });
|
|
await queryClient.invalidateQueries({ queryKey: ['pond', pondSlug] });
|
|
setStatus('saved');
|
|
} catch (err) {
|
|
setError(err);
|
|
setStatus('idle');
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="pond-theme">
|
|
<FormError error={error} />
|
|
<fieldset className="settings-fieldset">
|
|
<legend>{t('pondTheme.legend')}</legend>
|
|
<label className="settings-checkbox">
|
|
<input
|
|
type="radio"
|
|
name="pond-theme-accent"
|
|
value=""
|
|
checked={accent === null}
|
|
onChange={() => choose(null)}
|
|
/>
|
|
{t('pondTheme.inherit')}
|
|
</label>
|
|
{THEME_PRESETS.map((preset) => (
|
|
<label key={preset.id} className="settings-checkbox">
|
|
<input
|
|
type="radio"
|
|
name="pond-theme-accent"
|
|
value={preset.accent}
|
|
checked={accent === preset.accent}
|
|
onChange={() => choose(preset.accent)}
|
|
/>
|
|
{tSettings(`appearance.presets.${preset.id}`)}
|
|
<AccentSwatches {...derivedPair(preset.accent)} />
|
|
</label>
|
|
))}
|
|
<div className="settings-checkbox">
|
|
<input
|
|
type="radio"
|
|
id="pond-theme-custom"
|
|
name="pond-theme-accent"
|
|
value="custom"
|
|
checked={isCustom}
|
|
onChange={() => choose(customHex)}
|
|
/>
|
|
<label htmlFor="pond-theme-custom">{tSettings('appearance.custom')}</label>
|
|
<input
|
|
type="color"
|
|
className="accent-color-input"
|
|
value={isCustom ? accent : customHex}
|
|
aria-label={tSettings('appearance.customPick')}
|
|
onChange={(event) => {
|
|
setCustomHex(event.target.value);
|
|
choose(event.target.value);
|
|
}}
|
|
/>
|
|
{isCustom && <AccentSwatches {...derivedPair(accent)} />}
|
|
</div>
|
|
</fieldset>
|
|
<p className="field__hint">{t('pondTheme.hint')}</p>
|
|
<button
|
|
type="button"
|
|
className="button"
|
|
onClick={() => void save()}
|
|
disabled={status === 'saving'}
|
|
>
|
|
{status === 'saved' ? t('pondTheme.saved') : t('pondTheme.save')}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|