wrolpi/app/public/index.html
Roland 57985cea94
ui: media filtering is a per-theme setting, not a night-mode hardcode
Filtering media was keyed on `data-theme="night"`, which made it a property of
the theme that no user could turn off.  Some people want their media tinted to
match the interface; some want to see the actual image.

A theme now declares a filter and whether it starts on, and the user decides:

  night   night-red    on by default.  An unfiltered thumbnail undoes dark
                       adaptation, which is the whole point of the mode.
  amber   amber-mono   off by default.  Amber is a look, not a night-vision
                       aid, so media keeps its real colors until asked for.
  light   -            no filter offered, so no toggle is shown.
  dark    -

The choice is stored per theme, so wanting amber's tint says nothing about
wanting night's.  The toggle lives in the theme picker, which puts it on the
Settings page and in the gallery at once.

Mechanically the rules now key on a `data-media-filter` attribute rather than
`data-theme`: a rule keyed on the theme could not be turned off.  index.html
stamps that attribute before first paint alongside data-theme -- without it,
night mode paints a screen of unfiltered thumbnails while the bundle arrives
over a hotspot, costing the user the adaptation the theme exists to protect.
Tests cover both the attribute rule and the fact that the inline script's
duplicated ids and defaults still agree with names.ts.

The amber matrix is the same luminance collapse tinted to #ff9500 (red full,
green 0.584, blue zero) with a gentler gamma than night's: amber carries far
more luminance and clips to a flat wash otherwise.

The gallery gains a Media filtering section showing the WROLPi logo filtered,
beside an unfiltered copy that opts out with `night-unfiltered`.  That copy is
behind a click on purpose -- an unfiltered image is a bright patch, and night
mode should not spring one on someone reading in the dark.

ui-design.md gains the media filtering contract it never got in Phase 0,
including the note that a Phase 3 custom theme names an existing filter id and
cannot ship its own matrix, since that would be code rather than data.
2026-08-08 18:04:00 -06:00

94 lines
4.2 KiB
HTML

<!DOCTYPE html>
<html lang="en" style="height: 100%">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
<meta name="theme-color" content="#000000"/>
<meta
name="description"
content="The WROLPI webapp"
/>
<link rel="icon" href="%PUBLIC_URL%/favicon.ico"/>
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png">
<link rel="mask-icon" href="%PUBLIC_URL%/safari-pinned-tab.svg" color="#5bbad5">
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>WROLPi: Create your own off-grid library</title>
<script>
/*
Stamp the saved theme, and the media filter it resolves to, on <html> before first
paint. ThemeProvider sets the same attributes once React mounts, but the bundle can
take a while to arrive over a hotspot — without this the page would paint in the wrong
theme first, and in night mode a flash of unfiltered thumbnails would cost the user the
dark adaptation the theme exists to protect.
Keep the storage keys, theme names, and filter defaults in sync with
src/themes/names.ts.
*/
(function () {
var theme = 'light';
var saved = null;
// Only the storage read can throw (blocked cookies/storage). Keep the try
// around just that, so a device that prefers dark still gets dark.
try {
saved = localStorage.getItem('color-scheme');
} catch (e) {
saved = null;
}
if (saved === 'light' || saved === 'dark' || saved === 'night' || saved === 'amber') {
theme = saved;
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
// `system`, unset, or written by an older version.
theme = 'dark';
}
document.documentElement.setAttribute('data-theme', theme);
// Which filter each theme uses, and whether it applies unless the user says otherwise.
var filters = {night: 'night-red', amber: 'amber-mono'};
var defaults = {night: true, amber: false};
if (filters[theme]) {
var enabled = defaults[theme];
try {
var overrides = JSON.parse(localStorage.getItem('media-filter') || '{}');
if (typeof overrides[theme] === 'boolean') {
enabled = overrides[theme];
}
} catch (e) {
// Blocked storage or malformed JSON: fall back to the theme's default.
}
if (enabled) {
document.documentElement.setAttribute('data-media-filter', filters[theme]);
}
}
})();
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>