@cavebatsofware/riposte-pickers 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +214 -0
- package/dist/chunk-6FAEMGAR.js +93 -0
- package/dist/chunk-6FAEMGAR.js.map +1 -0
- package/dist/chunk-6OZDKKEP.cjs +95 -0
- package/dist/chunk-6OZDKKEP.cjs.map +1 -0
- package/dist/chunk-7JL223UJ.cjs +84 -0
- package/dist/chunk-7JL223UJ.cjs.map +1 -0
- package/dist/chunk-A4OVAXLP.js +261 -0
- package/dist/chunk-A4OVAXLP.js.map +1 -0
- package/dist/chunk-ATS6MI5Q.js +3 -0
- package/dist/chunk-ATS6MI5Q.js.map +1 -0
- package/dist/chunk-GDYHLOSQ.cjs +268 -0
- package/dist/chunk-GDYHLOSQ.cjs.map +1 -0
- package/dist/chunk-IWUGV7HL.js +113 -0
- package/dist/chunk-IWUGV7HL.js.map +1 -0
- package/dist/chunk-LIGL56YJ.cjs +116 -0
- package/dist/chunk-LIGL56YJ.cjs.map +1 -0
- package/dist/chunk-U73YJG4C.cjs +4 -0
- package/dist/chunk-U73YJG4C.cjs.map +1 -0
- package/dist/chunk-XWM3AYYR.js +82 -0
- package/dist/chunk-XWM3AYYR.js.map +1 -0
- package/dist/i18n/index.cjs +192 -0
- package/dist/i18n/index.cjs.map +1 -0
- package/dist/i18n/index.d.cts +156 -0
- package/dist/i18n/index.d.ts +156 -0
- package/dist/i18n/index.js +189 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/index.cjs +52 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/language.cjs +18 -0
- package/dist/language.cjs.map +1 -0
- package/dist/language.d.cts +39 -0
- package/dist/language.d.ts +39 -0
- package/dist/language.js +5 -0
- package/dist/language.js.map +1 -0
- package/dist/shared.cjs +18 -0
- package/dist/shared.cjs.map +1 -0
- package/dist/shared.d.cts +29 -0
- package/dist/shared.d.ts +29 -0
- package/dist/shared.js +5 -0
- package/dist/shared.js.map +1 -0
- package/dist/theme.cjs +33 -0
- package/dist/theme.cjs.map +1 -0
- package/dist/theme.d.cts +54 -0
- package/dist/theme.d.ts +54 -0
- package/dist/theme.js +4 -0
- package/dist/theme.js.map +1 -0
- package/package.json +97 -0
- package/styles/index.css +3 -0
- package/styles/language.css +89 -0
- package/styles/palette.css +614 -0
- package/styles/picker.css +139 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { PopoverPicker } from './chunk-6FAEMGAR.js';
|
|
2
|
+
import { createContext, useContext, useState, useLayoutEffect, useEffect, useRef } from 'react';
|
|
3
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
var COLORWAYS = [
|
|
7
|
+
{ id: "forest", label: "Forest & Cream", swatch: "#2d4a37" },
|
|
8
|
+
{ id: "warm", label: "Warm Editorial", swatch: "#1d3557" },
|
|
9
|
+
{ id: "plum", label: "Plum & Apricot", swatch: "#5b1f4d" },
|
|
10
|
+
// Avernus & Clouds: opposite-pole light/dark. The picker swatch shows
|
|
11
|
+
// both halves at once via a diagonal gradient so the dichotomy is
|
|
12
|
+
// visible at a glance.
|
|
13
|
+
{
|
|
14
|
+
id: "avernus",
|
|
15
|
+
label: "Avernus & Clouds",
|
|
16
|
+
swatch: "linear-gradient(135deg, #4f6dab 50%, #ed5e3a 50%)"
|
|
17
|
+
},
|
|
18
|
+
{ id: "mineral", label: "Rocks & Minerals", swatch: "#8e4a1f" },
|
|
19
|
+
// Accessibility colorways. Each swatch shows the theme's signature
|
|
20
|
+
// accent so the picker telegraphs the palette's actual aesthetic.
|
|
21
|
+
{ id: "daltonia", label: "Red-Green Accessible", swatch: "#d4a334" },
|
|
22
|
+
{ id: "tritan", label: "Blue-Yellow Accessible", swatch: "#c25d4a" },
|
|
23
|
+
{
|
|
24
|
+
id: "achroma",
|
|
25
|
+
label: "High Contrast",
|
|
26
|
+
swatch: "linear-gradient(135deg, #000000 50%, #ffffff 50%)"
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
var DEFAULT_COLORWAY = "forest";
|
|
30
|
+
var DEFAULT_STORAGE_KEY = "rs_theme_v1";
|
|
31
|
+
var ThemeContext = createContext(null);
|
|
32
|
+
function useTheme() {
|
|
33
|
+
const ctx = useContext(ThemeContext);
|
|
34
|
+
if (!ctx) {
|
|
35
|
+
throw new Error("useTheme must be used within ThemeProvider");
|
|
36
|
+
}
|
|
37
|
+
return ctx;
|
|
38
|
+
}
|
|
39
|
+
function isValidThemeId(id, colorways) {
|
|
40
|
+
if (!id) return false;
|
|
41
|
+
const colorway = id.endsWith("-dark") ? id.slice(0, -"-dark".length) : id;
|
|
42
|
+
return colorways.some((c) => c.id === colorway);
|
|
43
|
+
}
|
|
44
|
+
function resolveInitialTheme(colorways, defaultColorway, storageKey) {
|
|
45
|
+
let stored = null;
|
|
46
|
+
try {
|
|
47
|
+
stored = localStorage.getItem(storageKey);
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
if (isValidThemeId(stored, colorways)) return stored;
|
|
51
|
+
let prefersDark = false;
|
|
52
|
+
try {
|
|
53
|
+
prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
return prefersDark ? `${defaultColorway}-dark` : defaultColorway;
|
|
57
|
+
}
|
|
58
|
+
function ThemeProvider({
|
|
59
|
+
children,
|
|
60
|
+
colorways = COLORWAYS,
|
|
61
|
+
defaultColorway = DEFAULT_COLORWAY,
|
|
62
|
+
storageKey = DEFAULT_STORAGE_KEY
|
|
63
|
+
}) {
|
|
64
|
+
const [theme, setThemeState] = useState(
|
|
65
|
+
() => resolveInitialTheme(colorways, defaultColorway, storageKey)
|
|
66
|
+
);
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
document.documentElement.dataset.theme = theme;
|
|
69
|
+
}, [theme]);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
let media;
|
|
72
|
+
try {
|
|
73
|
+
media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
74
|
+
} catch {
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
function onChange(e) {
|
|
78
|
+
let stored = null;
|
|
79
|
+
try {
|
|
80
|
+
stored = localStorage.getItem(storageKey);
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
if (isValidThemeId(stored, colorways)) return;
|
|
84
|
+
const next = e.matches ? `${defaultColorway}-dark` : defaultColorway;
|
|
85
|
+
setThemeState(next);
|
|
86
|
+
}
|
|
87
|
+
media.addEventListener?.("change", onChange);
|
|
88
|
+
return () => {
|
|
89
|
+
media.removeEventListener?.("change", onChange);
|
|
90
|
+
};
|
|
91
|
+
}, [colorways, defaultColorway, storageKey]);
|
|
92
|
+
function setTheme(id) {
|
|
93
|
+
if (!isValidThemeId(id, colorways)) return;
|
|
94
|
+
setThemeState(id);
|
|
95
|
+
try {
|
|
96
|
+
localStorage.setItem(storageKey, id);
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function setMode(nextMode) {
|
|
101
|
+
if (nextMode !== "light" && nextMode !== "dark") return;
|
|
102
|
+
const colorway = theme.endsWith("-dark") ? theme.slice(0, -"-dark".length) : theme;
|
|
103
|
+
setTheme(nextMode === "dark" ? `${colorway}-dark` : colorway);
|
|
104
|
+
}
|
|
105
|
+
const mode = theme.endsWith("-dark") ? "dark" : "light";
|
|
106
|
+
return /* @__PURE__ */ jsx(ThemeContext.Provider, { value: { theme, setTheme, colorways, mode, setMode }, children });
|
|
107
|
+
}
|
|
108
|
+
var ARROW_KEYS = ["ArrowDown", "ArrowUp", "ArrowRight", "ArrowLeft", "Home", "End"];
|
|
109
|
+
function nextRadioIndex(key, currentIndex, length) {
|
|
110
|
+
switch (key) {
|
|
111
|
+
case "Home":
|
|
112
|
+
return 0;
|
|
113
|
+
case "End":
|
|
114
|
+
return length - 1;
|
|
115
|
+
case "ArrowDown":
|
|
116
|
+
case "ArrowRight":
|
|
117
|
+
return (currentIndex + 1) % length;
|
|
118
|
+
case "ArrowUp":
|
|
119
|
+
case "ArrowLeft":
|
|
120
|
+
return (currentIndex - 1 + length) % length;
|
|
121
|
+
default:
|
|
122
|
+
return currentIndex;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function ThemePicker({ variant = "popover", namespace = "common" }) {
|
|
126
|
+
const { theme, setTheme, colorways, mode, setMode } = useTheme();
|
|
127
|
+
const [open, setOpen] = useState(false);
|
|
128
|
+
const colorwayBtnRefs = useRef([]);
|
|
129
|
+
const modeBtnRefs = useRef([]);
|
|
130
|
+
const { t } = useTranslation(namespace);
|
|
131
|
+
const currentColorway = theme.endsWith("-dark") ? theme.slice(0, -"-dark".length) : theme;
|
|
132
|
+
const grid = /* @__PURE__ */ jsxs("div", { className: "theme-picker-grid", children: [
|
|
133
|
+
/* @__PURE__ */ jsx("div", { className: "theme-picker-title", children: t("theme.title") }),
|
|
134
|
+
/* @__PURE__ */ jsx(
|
|
135
|
+
"div",
|
|
136
|
+
{
|
|
137
|
+
className: "theme-swatches",
|
|
138
|
+
role: "radiogroup",
|
|
139
|
+
tabIndex: -1,
|
|
140
|
+
"aria-label": t("theme.colorwayAria"),
|
|
141
|
+
children: colorways.map((c, idx) => {
|
|
142
|
+
const active = c.id === currentColorway;
|
|
143
|
+
const label = t(`theme.colorways.${c.id}`, { defaultValue: c.label });
|
|
144
|
+
return /* @__PURE__ */ jsxs(
|
|
145
|
+
"button",
|
|
146
|
+
{
|
|
147
|
+
ref: (el) => {
|
|
148
|
+
colorwayBtnRefs.current[idx] = el;
|
|
149
|
+
},
|
|
150
|
+
type: "button",
|
|
151
|
+
role: "radio",
|
|
152
|
+
"aria-checked": active,
|
|
153
|
+
tabIndex: active ? 0 : -1,
|
|
154
|
+
className: `theme-swatch ${active ? "active" : ""}`,
|
|
155
|
+
onClick: () => {
|
|
156
|
+
setTheme(`${c.id}${mode === "dark" ? "-dark" : ""}`);
|
|
157
|
+
if (variant === "popover") setOpen(false);
|
|
158
|
+
},
|
|
159
|
+
onKeyDown: (e) => {
|
|
160
|
+
if (!ARROW_KEYS.includes(e.key)) return;
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
const next = nextRadioIndex(e.key, idx, colorways.length);
|
|
163
|
+
const target = colorways[next];
|
|
164
|
+
setTheme(`${target.id}${mode === "dark" ? "-dark" : ""}`);
|
|
165
|
+
colorwayBtnRefs.current[next]?.focus();
|
|
166
|
+
},
|
|
167
|
+
children: [
|
|
168
|
+
/* @__PURE__ */ jsx(
|
|
169
|
+
"span",
|
|
170
|
+
{
|
|
171
|
+
className: "theme-swatch-color",
|
|
172
|
+
style: { background: c.swatch },
|
|
173
|
+
"aria-hidden": "true"
|
|
174
|
+
}
|
|
175
|
+
),
|
|
176
|
+
/* @__PURE__ */ jsx("span", { className: "theme-swatch-label", children: label }),
|
|
177
|
+
active && /* @__PURE__ */ jsx("span", { className: "theme-swatch-check", "aria-hidden": "true", children: "\u2713" })
|
|
178
|
+
]
|
|
179
|
+
},
|
|
180
|
+
c.id
|
|
181
|
+
);
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
),
|
|
185
|
+
/* @__PURE__ */ jsx(
|
|
186
|
+
"div",
|
|
187
|
+
{
|
|
188
|
+
className: "theme-mode-row",
|
|
189
|
+
role: "radiogroup",
|
|
190
|
+
tabIndex: -1,
|
|
191
|
+
"aria-label": t("theme.modeAria"),
|
|
192
|
+
children: (() => {
|
|
193
|
+
const modes = [
|
|
194
|
+
{ id: "light", label: t("theme.mode.light") },
|
|
195
|
+
{ id: "dark", label: t("theme.mode.dark") }
|
|
196
|
+
];
|
|
197
|
+
return modes.map((m, idx) => /* @__PURE__ */ jsx(
|
|
198
|
+
"button",
|
|
199
|
+
{
|
|
200
|
+
ref: (el) => {
|
|
201
|
+
modeBtnRefs.current[idx] = el;
|
|
202
|
+
},
|
|
203
|
+
type: "button",
|
|
204
|
+
role: "radio",
|
|
205
|
+
"aria-checked": mode === m.id,
|
|
206
|
+
tabIndex: mode === m.id ? 0 : -1,
|
|
207
|
+
className: `theme-mode-btn ${mode === m.id ? "active" : ""}`,
|
|
208
|
+
onClick: () => setMode(m.id),
|
|
209
|
+
onKeyDown: (e) => {
|
|
210
|
+
if (!ARROW_KEYS.includes(e.key)) return;
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
const next = nextRadioIndex(e.key, idx, modes.length);
|
|
213
|
+
setMode(modes[next].id);
|
|
214
|
+
modeBtnRefs.current[next]?.focus();
|
|
215
|
+
},
|
|
216
|
+
children: m.label
|
|
217
|
+
},
|
|
218
|
+
m.id
|
|
219
|
+
));
|
|
220
|
+
})()
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
] });
|
|
224
|
+
return /* @__PURE__ */ jsx(
|
|
225
|
+
PopoverPicker,
|
|
226
|
+
{
|
|
227
|
+
variant,
|
|
228
|
+
open,
|
|
229
|
+
onOpenChange: setOpen,
|
|
230
|
+
className: "theme-picker",
|
|
231
|
+
toggleAriaLabel: t("theme.toggleAria"),
|
|
232
|
+
popoverAriaLabel: t("theme.dialogAria"),
|
|
233
|
+
toggleIcon: /* @__PURE__ */ jsxs(
|
|
234
|
+
"svg",
|
|
235
|
+
{
|
|
236
|
+
width: "20",
|
|
237
|
+
height: "20",
|
|
238
|
+
viewBox: "0 0 24 24",
|
|
239
|
+
fill: "none",
|
|
240
|
+
stroke: "currentColor",
|
|
241
|
+
strokeWidth: "1.8",
|
|
242
|
+
strokeLinecap: "round",
|
|
243
|
+
strokeLinejoin: "round",
|
|
244
|
+
"aria-hidden": "true",
|
|
245
|
+
children: [
|
|
246
|
+
/* @__PURE__ */ jsx("circle", { cx: "13.5", cy: "6.5", r: "0.5", fill: "currentColor" }),
|
|
247
|
+
/* @__PURE__ */ jsx("circle", { cx: "17.5", cy: "10.5", r: "0.5", fill: "currentColor" }),
|
|
248
|
+
/* @__PURE__ */ jsx("circle", { cx: "8.5", cy: "7.5", r: "0.5", fill: "currentColor" }),
|
|
249
|
+
/* @__PURE__ */ jsx("circle", { cx: "6.5", cy: "12.5", r: "0.5", fill: "currentColor" }),
|
|
250
|
+
/* @__PURE__ */ jsx("path", { d: "M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c1 0 1.5-.5 1.5-1.2 0-.4-.1-.7-.4-1-.2-.3-.4-.6-.4-1 0-.7.5-1.2 1.2-1.2H16c3.3 0 6-2.7 6-6 0-5-4.5-9-10-9z" })
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
),
|
|
254
|
+
children: grid
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export { COLORWAYS, DEFAULT_COLORWAY, DEFAULT_STORAGE_KEY, ThemePicker, ThemeProvider, useTheme };
|
|
260
|
+
//# sourceMappingURL=chunk-A4OVAXLP.js.map
|
|
261
|
+
//# sourceMappingURL=chunk-A4OVAXLP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/theme/ThemeContext.tsx","../src/theme/ThemePicker.tsx"],"names":["useState","jsx"],"mappings":"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","file":"chunk-A4OVAXLP.js","sourcesContent":["/* This file is part of @cavebatsofware/riposte-pickers\n * Copyright (C) 2026 Grant DeFayette\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, version 3 of the License (GPL-3.0-only).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.\n */\nimport {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useState,\n type ReactNode,\n} from \"react\";\n\nexport interface Colorway {\n /** Stable id; must match a `[data-theme=\"<id>\"]` block in your CSS. */\n id: string;\n /** Fallback display label (i18n catalog labels take precedence in the UI). */\n label: string;\n /** CSS `background` value for the picker swatch (color or gradient). */\n swatch: string;\n}\n\nexport type ThemeMode = \"light\" | \"dark\";\n\nexport interface ThemeContextValue {\n /** Resolved theme id of record, e.g. `forest` or `forest-dark`. */\n theme: string;\n /** Set the theme by id; ignored if the id is not in the catalog. */\n setTheme: (id: string) => void;\n /** The active catalog. */\n colorways: Colorway[];\n /** Derived light/dark mode for the current theme. */\n mode: ThemeMode;\n /** Flip just the mode, keeping the current colorway. */\n setMode: (mode: ThemeMode) => void;\n}\n\n/// Default colorways. Each one has a light and a dark variant in the bundled\n/// `styles/palette.css` under `[data-theme=\"<id>\"]` and `[data-theme=\"<id>-dark\"]`.\n/// The picker UI splits this into two axes: colorway and mode (light/dark).\n///\n/// The catalog mixes evocative aesthetic colorways with descriptive\n/// accessibility colorways. The accessibility entries are named after\n/// the deficiency they target so users can pick the right one without\n/// having to learn the project's poetic naming.\nexport const COLORWAYS: Colorway[] = [\n { id: \"forest\", label: \"Forest & Cream\", swatch: \"#2d4a37\" },\n { id: \"warm\", label: \"Warm Editorial\", swatch: \"#1d3557\" },\n { id: \"plum\", label: \"Plum & Apricot\", swatch: \"#5b1f4d\" },\n // Avernus & Clouds: opposite-pole light/dark. The picker swatch shows\n // both halves at once via a diagonal gradient so the dichotomy is\n // visible at a glance.\n {\n id: \"avernus\",\n label: \"Avernus & Clouds\",\n swatch: \"linear-gradient(135deg, #4f6dab 50%, #ed5e3a 50%)\",\n },\n { id: \"mineral\", label: \"Rocks & Minerals\", swatch: \"#8e4a1f\" },\n // Accessibility colorways. Each swatch shows the theme's signature\n // accent so the picker telegraphs the palette's actual aesthetic.\n { id: \"daltonia\", label: \"Red-Green Accessible\", swatch: \"#d4a334\" },\n { id: \"tritan\", label: \"Blue-Yellow Accessible\", swatch: \"#c25d4a\" },\n {\n id: \"achroma\",\n label: \"High Contrast\",\n swatch: \"linear-gradient(135deg, #000000 50%, #ffffff 50%)\",\n },\n];\n\nexport const DEFAULT_COLORWAY = \"forest\";\nexport const DEFAULT_STORAGE_KEY = \"rs_theme_v1\";\n\nconst ThemeContext = createContext<ThemeContextValue | null>(null);\n\nexport function useTheme(): ThemeContextValue {\n const ctx = useContext(ThemeContext);\n if (!ctx) {\n throw new Error(\"useTheme must be used within ThemeProvider\");\n }\n return ctx;\n}\n\nfunction isValidThemeId(id: string | null, colorways: Colorway[]): id is string {\n if (!id) return false;\n const colorway = id.endsWith(\"-dark\") ? id.slice(0, -\"-dark\".length) : id;\n return colorways.some((c) => c.id === colorway);\n}\n\nfunction resolveInitialTheme(\n colorways: Colorway[],\n defaultColorway: string,\n storageKey: string,\n): string {\n let stored: string | null = null;\n try {\n stored = localStorage.getItem(storageKey);\n } catch {\n // private mode without storage access; defaults stand\n }\n if (isValidThemeId(stored, colorways)) return stored;\n\n // No explicit user choice yet: honor the OS-level preference.\n let prefersDark = false;\n try {\n prefersDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n } catch {\n // matchMedia unavailable (rare); default to light\n }\n return prefersDark ? `${defaultColorway}-dark` : defaultColorway;\n}\n\nexport interface ThemeProviderProps {\n children: ReactNode;\n /** Colorway catalog. Defaults to the bundled riposte `COLORWAYS`. */\n colorways?: Colorway[];\n /** Colorway used when no choice is stored. Defaults to `forest`. */\n defaultColorway?: string;\n /** localStorage key for the persisted choice. Defaults to `rs_theme_v1`. */\n storageKey?: string;\n}\n\n/// Apply the theme to the document and persist the user's choice.\n///\n/// State of record is the resolved id (e.g. `forest-dark`); the picker UI\n/// derives colorway and mode from it. The DOM attribute is applied in\n/// useLayoutEffect so the first paint and every subsequent change pick up\n/// the correct theme without a flash.\nexport function ThemeProvider({\n children,\n colorways = COLORWAYS,\n defaultColorway = DEFAULT_COLORWAY,\n storageKey = DEFAULT_STORAGE_KEY,\n}: ThemeProviderProps) {\n const [theme, setThemeState] = useState(() =>\n resolveInitialTheme(colorways, defaultColorway, storageKey),\n );\n useLayoutEffect(() => {\n document.documentElement.dataset.theme = theme;\n }, [theme]);\n\n // Subscribe to OS preference changes so a fresh visitor (no explicit\n // choice yet) tracks their system theme, and stop tracking the moment\n // they pick. Pure subscription effect; no state-from-effect on mount.\n useEffect(() => {\n let media: MediaQueryList;\n try {\n media = window.matchMedia(\"(prefers-color-scheme: dark)\");\n } catch {\n return undefined;\n }\n function onChange(e: MediaQueryListEvent) {\n let stored: string | null = null;\n try {\n stored = localStorage.getItem(storageKey);\n } catch {\n // ignore\n }\n // User has made an explicit choice; don't override it.\n if (isValidThemeId(stored, colorways)) return;\n const next = e.matches ? `${defaultColorway}-dark` : defaultColorway;\n setThemeState(next);\n }\n media.addEventListener?.(\"change\", onChange);\n return () => {\n media.removeEventListener?.(\"change\", onChange);\n };\n }, [colorways, defaultColorway, storageKey]);\n\n function setTheme(id: string) {\n if (!isValidThemeId(id, colorways)) return;\n setThemeState(id);\n try {\n localStorage.setItem(storageKey, id);\n } catch {\n // ignore; the in-memory state still reflects the choice for this session\n }\n }\n\n // Convenience: flip just the mode while keeping the colorway. The picker\n // exposes this as a separate row so users can change one without\n // accidentally re-picking the other.\n function setMode(nextMode: ThemeMode) {\n if (nextMode !== \"light\" && nextMode !== \"dark\") return;\n const colorway = theme.endsWith(\"-dark\") ? theme.slice(0, -\"-dark\".length) : theme;\n setTheme(nextMode === \"dark\" ? `${colorway}-dark` : colorway);\n }\n\n const mode: ThemeMode = theme.endsWith(\"-dark\") ? \"dark\" : \"light\";\n\n return (\n <ThemeContext.Provider value={{ theme, setTheme, colorways, mode, setMode }}>\n {children}\n </ThemeContext.Provider>\n );\n}\n","/* This file is part of @cavebatsofware/riposte-pickers\n * Copyright (C) 2026 Grant DeFayette\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, version 3 of the License (GPL-3.0-only).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.\n */\nimport { useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useTheme, type ThemeMode } from \"./ThemeContext\";\nimport PopoverPicker from \"../shared/PopoverPicker\";\n\nconst ARROW_KEYS = [\"ArrowDown\", \"ArrowUp\", \"ArrowRight\", \"ArrowLeft\", \"Home\", \"End\"];\n\n/// Resolve the next index for a roving radiogroup keystroke. ArrowDown /\n/// ArrowRight advance, ArrowUp / ArrowLeft retreat, Home / End jump. The\n/// list wraps at the ends.\nfunction nextRadioIndex(key: string, currentIndex: number, length: number): number {\n switch (key) {\n case \"Home\":\n return 0;\n case \"End\":\n return length - 1;\n case \"ArrowDown\":\n case \"ArrowRight\":\n return (currentIndex + 1) % length;\n case \"ArrowUp\":\n case \"ArrowLeft\":\n return (currentIndex - 1 + length) % length;\n default:\n return currentIndex;\n }\n}\n\nexport interface ThemePickerProps {\n /**\n * `popover` (default): round icon button in a header that opens a popover\n * grid. `inline`: the grid rendered directly, for use inside a drawer.\n */\n variant?: \"popover\" | \"inline\";\n /**\n * i18next namespace holding the `theme.*` keys. Defaults to `common`.\n * Merge the package's `themeResources` into this namespace.\n */\n namespace?: string;\n}\n\n/// Theme picker: a colorway switch and a light/dark mode toggle rendered as\n/// two adjacent ARIA radiogroups with roving tabindex. The underlying state\n/// is a single id of the form \"<colorway>-<mode>\" or just \"<colorway>\" for\n/// light-mode values (both handled by `ThemeContext.setTheme`).\nexport default function ThemePicker({ variant = \"popover\", namespace = \"common\" }: ThemePickerProps) {\n const { theme, setTheme, colorways, mode, setMode } = useTheme();\n const [open, setOpen] = useState(false);\n const colorwayBtnRefs = useRef<Array<HTMLButtonElement | null>>([]);\n const modeBtnRefs = useRef<Array<HTMLButtonElement | null>>([]);\n const { t } = useTranslation(namespace);\n\n // Today the theme id is \"<colorway>\" (light) or \"<colorway>-dark\". Derive\n // the current colorway from the resolved theme so the swatches show the\n // right active state regardless of which form is stored.\n const currentColorway = theme.endsWith(\"-dark\") ? theme.slice(0, -\"-dark\".length) : theme;\n\n const grid = (\n <div className=\"theme-picker-grid\">\n <div className=\"theme-picker-title\">{t(\"theme.title\")}</div>\n <div\n className=\"theme-swatches\"\n role=\"radiogroup\"\n tabIndex={-1}\n aria-label={t(\"theme.colorwayAria\")}\n >\n {colorways.map((c, idx) => {\n const active = c.id === currentColorway;\n // Colorway display name comes from the catalog so each language can\n // localize the marketing-style names. Fall back to the catalog's\n // hardcoded `c.label` if a key is missing.\n const label = t(`theme.colorways.${c.id}`, { defaultValue: c.label });\n return (\n <button\n key={c.id}\n ref={(el) => {\n colorwayBtnRefs.current[idx] = el;\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={active}\n tabIndex={active ? 0 : -1}\n className={`theme-swatch ${active ? \"active\" : \"\"}`}\n onClick={() => {\n setTheme(`${c.id}${mode === \"dark\" ? \"-dark\" : \"\"}`);\n if (variant === \"popover\") setOpen(false);\n }}\n onKeyDown={(e) => {\n if (!ARROW_KEYS.includes(e.key)) return;\n e.preventDefault();\n const next = nextRadioIndex(e.key, idx, colorways.length);\n const target = colorways[next];\n setTheme(`${target.id}${mode === \"dark\" ? \"-dark\" : \"\"}`);\n colorwayBtnRefs.current[next]?.focus();\n }}\n >\n <span\n className=\"theme-swatch-color\"\n style={{ background: c.swatch }}\n aria-hidden=\"true\"\n />\n <span className=\"theme-swatch-label\">{label}</span>\n {active && (\n <span className=\"theme-swatch-check\" aria-hidden=\"true\">\n ✓\n </span>\n )}\n </button>\n );\n })}\n </div>\n <div\n className=\"theme-mode-row\"\n role=\"radiogroup\"\n tabIndex={-1}\n aria-label={t(\"theme.modeAria\")}\n >\n {(() => {\n const modes: Array<{ id: ThemeMode; label: string }> = [\n { id: \"light\", label: t(\"theme.mode.light\") },\n { id: \"dark\", label: t(\"theme.mode.dark\") },\n ];\n return modes.map((m, idx) => (\n <button\n key={m.id}\n ref={(el) => {\n modeBtnRefs.current[idx] = el;\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={mode === m.id}\n tabIndex={mode === m.id ? 0 : -1}\n className={`theme-mode-btn ${mode === m.id ? \"active\" : \"\"}`}\n onClick={() => setMode(m.id)}\n onKeyDown={(e) => {\n if (!ARROW_KEYS.includes(e.key)) return;\n e.preventDefault();\n const next = nextRadioIndex(e.key, idx, modes.length);\n setMode(modes[next].id);\n modeBtnRefs.current[next]?.focus();\n }}\n >\n {m.label}\n </button>\n ));\n })()}\n </div>\n </div>\n );\n\n return (\n <PopoverPicker\n variant={variant}\n open={open}\n onOpenChange={setOpen}\n className=\"theme-picker\"\n toggleAriaLabel={t(\"theme.toggleAria\")}\n popoverAriaLabel={t(\"theme.dialogAria\")}\n toggleIcon={\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"13.5\" cy=\"6.5\" r=\"0.5\" fill=\"currentColor\" />\n <circle cx=\"17.5\" cy=\"10.5\" r=\"0.5\" fill=\"currentColor\" />\n <circle cx=\"8.5\" cy=\"7.5\" r=\"0.5\" fill=\"currentColor\" />\n <circle cx=\"6.5\" cy=\"12.5\" r=\"0.5\" fill=\"currentColor\" />\n <path d=\"M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c1 0 1.5-.5 1.5-1.2 0-.4-.1-.7-.4-1-.2-.3-.4-.6-.4-1 0-.7.5-1.2 1.2-1.2H16c3.3 0 6-2.7 6-6 0-5-4.5-9-10-9z\" />\n </svg>\n }\n >\n {grid}\n </PopoverPicker>\n );\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-ATS6MI5Q.js"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunk6OZDKKEP_cjs = require('./chunk-6OZDKKEP.cjs');
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
var reactI18next = require('react-i18next');
|
|
7
|
+
|
|
8
|
+
var COLORWAYS = [
|
|
9
|
+
{ id: "forest", label: "Forest & Cream", swatch: "#2d4a37" },
|
|
10
|
+
{ id: "warm", label: "Warm Editorial", swatch: "#1d3557" },
|
|
11
|
+
{ id: "plum", label: "Plum & Apricot", swatch: "#5b1f4d" },
|
|
12
|
+
// Avernus & Clouds: opposite-pole light/dark. The picker swatch shows
|
|
13
|
+
// both halves at once via a diagonal gradient so the dichotomy is
|
|
14
|
+
// visible at a glance.
|
|
15
|
+
{
|
|
16
|
+
id: "avernus",
|
|
17
|
+
label: "Avernus & Clouds",
|
|
18
|
+
swatch: "linear-gradient(135deg, #4f6dab 50%, #ed5e3a 50%)"
|
|
19
|
+
},
|
|
20
|
+
{ id: "mineral", label: "Rocks & Minerals", swatch: "#8e4a1f" },
|
|
21
|
+
// Accessibility colorways. Each swatch shows the theme's signature
|
|
22
|
+
// accent so the picker telegraphs the palette's actual aesthetic.
|
|
23
|
+
{ id: "daltonia", label: "Red-Green Accessible", swatch: "#d4a334" },
|
|
24
|
+
{ id: "tritan", label: "Blue-Yellow Accessible", swatch: "#c25d4a" },
|
|
25
|
+
{
|
|
26
|
+
id: "achroma",
|
|
27
|
+
label: "High Contrast",
|
|
28
|
+
swatch: "linear-gradient(135deg, #000000 50%, #ffffff 50%)"
|
|
29
|
+
}
|
|
30
|
+
];
|
|
31
|
+
var DEFAULT_COLORWAY = "forest";
|
|
32
|
+
var DEFAULT_STORAGE_KEY = "rs_theme_v1";
|
|
33
|
+
var ThemeContext = react.createContext(null);
|
|
34
|
+
function useTheme() {
|
|
35
|
+
const ctx = react.useContext(ThemeContext);
|
|
36
|
+
if (!ctx) {
|
|
37
|
+
throw new Error("useTheme must be used within ThemeProvider");
|
|
38
|
+
}
|
|
39
|
+
return ctx;
|
|
40
|
+
}
|
|
41
|
+
function isValidThemeId(id, colorways) {
|
|
42
|
+
if (!id) return false;
|
|
43
|
+
const colorway = id.endsWith("-dark") ? id.slice(0, -"-dark".length) : id;
|
|
44
|
+
return colorways.some((c) => c.id === colorway);
|
|
45
|
+
}
|
|
46
|
+
function resolveInitialTheme(colorways, defaultColorway, storageKey) {
|
|
47
|
+
let stored = null;
|
|
48
|
+
try {
|
|
49
|
+
stored = localStorage.getItem(storageKey);
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
if (isValidThemeId(stored, colorways)) return stored;
|
|
53
|
+
let prefersDark = false;
|
|
54
|
+
try {
|
|
55
|
+
prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
return prefersDark ? `${defaultColorway}-dark` : defaultColorway;
|
|
59
|
+
}
|
|
60
|
+
function ThemeProvider({
|
|
61
|
+
children,
|
|
62
|
+
colorways = COLORWAYS,
|
|
63
|
+
defaultColorway = DEFAULT_COLORWAY,
|
|
64
|
+
storageKey = DEFAULT_STORAGE_KEY
|
|
65
|
+
}) {
|
|
66
|
+
const [theme, setThemeState] = react.useState(
|
|
67
|
+
() => resolveInitialTheme(colorways, defaultColorway, storageKey)
|
|
68
|
+
);
|
|
69
|
+
react.useLayoutEffect(() => {
|
|
70
|
+
document.documentElement.dataset.theme = theme;
|
|
71
|
+
}, [theme]);
|
|
72
|
+
react.useEffect(() => {
|
|
73
|
+
let media;
|
|
74
|
+
try {
|
|
75
|
+
media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
76
|
+
} catch {
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
function onChange(e) {
|
|
80
|
+
let stored = null;
|
|
81
|
+
try {
|
|
82
|
+
stored = localStorage.getItem(storageKey);
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
if (isValidThemeId(stored, colorways)) return;
|
|
86
|
+
const next = e.matches ? `${defaultColorway}-dark` : defaultColorway;
|
|
87
|
+
setThemeState(next);
|
|
88
|
+
}
|
|
89
|
+
media.addEventListener?.("change", onChange);
|
|
90
|
+
return () => {
|
|
91
|
+
media.removeEventListener?.("change", onChange);
|
|
92
|
+
};
|
|
93
|
+
}, [colorways, defaultColorway, storageKey]);
|
|
94
|
+
function setTheme(id) {
|
|
95
|
+
if (!isValidThemeId(id, colorways)) return;
|
|
96
|
+
setThemeState(id);
|
|
97
|
+
try {
|
|
98
|
+
localStorage.setItem(storageKey, id);
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function setMode(nextMode) {
|
|
103
|
+
if (nextMode !== "light" && nextMode !== "dark") return;
|
|
104
|
+
const colorway = theme.endsWith("-dark") ? theme.slice(0, -"-dark".length) : theme;
|
|
105
|
+
setTheme(nextMode === "dark" ? `${colorway}-dark` : colorway);
|
|
106
|
+
}
|
|
107
|
+
const mode = theme.endsWith("-dark") ? "dark" : "light";
|
|
108
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ThemeContext.Provider, { value: { theme, setTheme, colorways, mode, setMode }, children });
|
|
109
|
+
}
|
|
110
|
+
var ARROW_KEYS = ["ArrowDown", "ArrowUp", "ArrowRight", "ArrowLeft", "Home", "End"];
|
|
111
|
+
function nextRadioIndex(key, currentIndex, length) {
|
|
112
|
+
switch (key) {
|
|
113
|
+
case "Home":
|
|
114
|
+
return 0;
|
|
115
|
+
case "End":
|
|
116
|
+
return length - 1;
|
|
117
|
+
case "ArrowDown":
|
|
118
|
+
case "ArrowRight":
|
|
119
|
+
return (currentIndex + 1) % length;
|
|
120
|
+
case "ArrowUp":
|
|
121
|
+
case "ArrowLeft":
|
|
122
|
+
return (currentIndex - 1 + length) % length;
|
|
123
|
+
default:
|
|
124
|
+
return currentIndex;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function ThemePicker({ variant = "popover", namespace = "common" }) {
|
|
128
|
+
const { theme, setTheme, colorways, mode, setMode } = useTheme();
|
|
129
|
+
const [open, setOpen] = react.useState(false);
|
|
130
|
+
const colorwayBtnRefs = react.useRef([]);
|
|
131
|
+
const modeBtnRefs = react.useRef([]);
|
|
132
|
+
const { t } = reactI18next.useTranslation(namespace);
|
|
133
|
+
const currentColorway = theme.endsWith("-dark") ? theme.slice(0, -"-dark".length) : theme;
|
|
134
|
+
const grid = /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "theme-picker-grid", children: [
|
|
135
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "theme-picker-title", children: t("theme.title") }),
|
|
136
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
137
|
+
"div",
|
|
138
|
+
{
|
|
139
|
+
className: "theme-swatches",
|
|
140
|
+
role: "radiogroup",
|
|
141
|
+
tabIndex: -1,
|
|
142
|
+
"aria-label": t("theme.colorwayAria"),
|
|
143
|
+
children: colorways.map((c, idx) => {
|
|
144
|
+
const active = c.id === currentColorway;
|
|
145
|
+
const label = t(`theme.colorways.${c.id}`, { defaultValue: c.label });
|
|
146
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
147
|
+
"button",
|
|
148
|
+
{
|
|
149
|
+
ref: (el) => {
|
|
150
|
+
colorwayBtnRefs.current[idx] = el;
|
|
151
|
+
},
|
|
152
|
+
type: "button",
|
|
153
|
+
role: "radio",
|
|
154
|
+
"aria-checked": active,
|
|
155
|
+
tabIndex: active ? 0 : -1,
|
|
156
|
+
className: `theme-swatch ${active ? "active" : ""}`,
|
|
157
|
+
onClick: () => {
|
|
158
|
+
setTheme(`${c.id}${mode === "dark" ? "-dark" : ""}`);
|
|
159
|
+
if (variant === "popover") setOpen(false);
|
|
160
|
+
},
|
|
161
|
+
onKeyDown: (e) => {
|
|
162
|
+
if (!ARROW_KEYS.includes(e.key)) return;
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
const next = nextRadioIndex(e.key, idx, colorways.length);
|
|
165
|
+
const target = colorways[next];
|
|
166
|
+
setTheme(`${target.id}${mode === "dark" ? "-dark" : ""}`);
|
|
167
|
+
colorwayBtnRefs.current[next]?.focus();
|
|
168
|
+
},
|
|
169
|
+
children: [
|
|
170
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
171
|
+
"span",
|
|
172
|
+
{
|
|
173
|
+
className: "theme-swatch-color",
|
|
174
|
+
style: { background: c.swatch },
|
|
175
|
+
"aria-hidden": "true"
|
|
176
|
+
}
|
|
177
|
+
),
|
|
178
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "theme-swatch-label", children: label }),
|
|
179
|
+
active && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "theme-swatch-check", "aria-hidden": "true", children: "\u2713" })
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
c.id
|
|
183
|
+
);
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
),
|
|
187
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
188
|
+
"div",
|
|
189
|
+
{
|
|
190
|
+
className: "theme-mode-row",
|
|
191
|
+
role: "radiogroup",
|
|
192
|
+
tabIndex: -1,
|
|
193
|
+
"aria-label": t("theme.modeAria"),
|
|
194
|
+
children: (() => {
|
|
195
|
+
const modes = [
|
|
196
|
+
{ id: "light", label: t("theme.mode.light") },
|
|
197
|
+
{ id: "dark", label: t("theme.mode.dark") }
|
|
198
|
+
];
|
|
199
|
+
return modes.map((m, idx) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
200
|
+
"button",
|
|
201
|
+
{
|
|
202
|
+
ref: (el) => {
|
|
203
|
+
modeBtnRefs.current[idx] = el;
|
|
204
|
+
},
|
|
205
|
+
type: "button",
|
|
206
|
+
role: "radio",
|
|
207
|
+
"aria-checked": mode === m.id,
|
|
208
|
+
tabIndex: mode === m.id ? 0 : -1,
|
|
209
|
+
className: `theme-mode-btn ${mode === m.id ? "active" : ""}`,
|
|
210
|
+
onClick: () => setMode(m.id),
|
|
211
|
+
onKeyDown: (e) => {
|
|
212
|
+
if (!ARROW_KEYS.includes(e.key)) return;
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
const next = nextRadioIndex(e.key, idx, modes.length);
|
|
215
|
+
setMode(modes[next].id);
|
|
216
|
+
modeBtnRefs.current[next]?.focus();
|
|
217
|
+
},
|
|
218
|
+
children: m.label
|
|
219
|
+
},
|
|
220
|
+
m.id
|
|
221
|
+
));
|
|
222
|
+
})()
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
] });
|
|
226
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
227
|
+
chunk6OZDKKEP_cjs.PopoverPicker,
|
|
228
|
+
{
|
|
229
|
+
variant,
|
|
230
|
+
open,
|
|
231
|
+
onOpenChange: setOpen,
|
|
232
|
+
className: "theme-picker",
|
|
233
|
+
toggleAriaLabel: t("theme.toggleAria"),
|
|
234
|
+
popoverAriaLabel: t("theme.dialogAria"),
|
|
235
|
+
toggleIcon: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
236
|
+
"svg",
|
|
237
|
+
{
|
|
238
|
+
width: "20",
|
|
239
|
+
height: "20",
|
|
240
|
+
viewBox: "0 0 24 24",
|
|
241
|
+
fill: "none",
|
|
242
|
+
stroke: "currentColor",
|
|
243
|
+
strokeWidth: "1.8",
|
|
244
|
+
strokeLinecap: "round",
|
|
245
|
+
strokeLinejoin: "round",
|
|
246
|
+
"aria-hidden": "true",
|
|
247
|
+
children: [
|
|
248
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "13.5", cy: "6.5", r: "0.5", fill: "currentColor" }),
|
|
249
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "17.5", cy: "10.5", r: "0.5", fill: "currentColor" }),
|
|
250
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8.5", cy: "7.5", r: "0.5", fill: "currentColor" }),
|
|
251
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "6.5", cy: "12.5", r: "0.5", fill: "currentColor" }),
|
|
252
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c1 0 1.5-.5 1.5-1.2 0-.4-.1-.7-.4-1-.2-.3-.4-.6-.4-1 0-.7.5-1.2 1.2-1.2H16c3.3 0 6-2.7 6-6 0-5-4.5-9-10-9z" })
|
|
253
|
+
]
|
|
254
|
+
}
|
|
255
|
+
),
|
|
256
|
+
children: grid
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
exports.COLORWAYS = COLORWAYS;
|
|
262
|
+
exports.DEFAULT_COLORWAY = DEFAULT_COLORWAY;
|
|
263
|
+
exports.DEFAULT_STORAGE_KEY = DEFAULT_STORAGE_KEY;
|
|
264
|
+
exports.ThemePicker = ThemePicker;
|
|
265
|
+
exports.ThemeProvider = ThemeProvider;
|
|
266
|
+
exports.useTheme = useTheme;
|
|
267
|
+
//# sourceMappingURL=chunk-GDYHLOSQ.cjs.map
|
|
268
|
+
//# sourceMappingURL=chunk-GDYHLOSQ.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/theme/ThemeContext.tsx","../src/theme/ThemePicker.tsx"],"names":["createContext","useContext","useState","useLayoutEffect","useEffect","jsx","useRef","useTranslation","jsxs","PopoverPicker"],"mappings":"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","file":"chunk-GDYHLOSQ.cjs","sourcesContent":["/* This file is part of @cavebatsofware/riposte-pickers\n * Copyright (C) 2026 Grant DeFayette\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, version 3 of the License (GPL-3.0-only).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.\n */\nimport {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useState,\n type ReactNode,\n} from \"react\";\n\nexport interface Colorway {\n /** Stable id; must match a `[data-theme=\"<id>\"]` block in your CSS. */\n id: string;\n /** Fallback display label (i18n catalog labels take precedence in the UI). */\n label: string;\n /** CSS `background` value for the picker swatch (color or gradient). */\n swatch: string;\n}\n\nexport type ThemeMode = \"light\" | \"dark\";\n\nexport interface ThemeContextValue {\n /** Resolved theme id of record, e.g. `forest` or `forest-dark`. */\n theme: string;\n /** Set the theme by id; ignored if the id is not in the catalog. */\n setTheme: (id: string) => void;\n /** The active catalog. */\n colorways: Colorway[];\n /** Derived light/dark mode for the current theme. */\n mode: ThemeMode;\n /** Flip just the mode, keeping the current colorway. */\n setMode: (mode: ThemeMode) => void;\n}\n\n/// Default colorways. Each one has a light and a dark variant in the bundled\n/// `styles/palette.css` under `[data-theme=\"<id>\"]` and `[data-theme=\"<id>-dark\"]`.\n/// The picker UI splits this into two axes: colorway and mode (light/dark).\n///\n/// The catalog mixes evocative aesthetic colorways with descriptive\n/// accessibility colorways. The accessibility entries are named after\n/// the deficiency they target so users can pick the right one without\n/// having to learn the project's poetic naming.\nexport const COLORWAYS: Colorway[] = [\n { id: \"forest\", label: \"Forest & Cream\", swatch: \"#2d4a37\" },\n { id: \"warm\", label: \"Warm Editorial\", swatch: \"#1d3557\" },\n { id: \"plum\", label: \"Plum & Apricot\", swatch: \"#5b1f4d\" },\n // Avernus & Clouds: opposite-pole light/dark. The picker swatch shows\n // both halves at once via a diagonal gradient so the dichotomy is\n // visible at a glance.\n {\n id: \"avernus\",\n label: \"Avernus & Clouds\",\n swatch: \"linear-gradient(135deg, #4f6dab 50%, #ed5e3a 50%)\",\n },\n { id: \"mineral\", label: \"Rocks & Minerals\", swatch: \"#8e4a1f\" },\n // Accessibility colorways. Each swatch shows the theme's signature\n // accent so the picker telegraphs the palette's actual aesthetic.\n { id: \"daltonia\", label: \"Red-Green Accessible\", swatch: \"#d4a334\" },\n { id: \"tritan\", label: \"Blue-Yellow Accessible\", swatch: \"#c25d4a\" },\n {\n id: \"achroma\",\n label: \"High Contrast\",\n swatch: \"linear-gradient(135deg, #000000 50%, #ffffff 50%)\",\n },\n];\n\nexport const DEFAULT_COLORWAY = \"forest\";\nexport const DEFAULT_STORAGE_KEY = \"rs_theme_v1\";\n\nconst ThemeContext = createContext<ThemeContextValue | null>(null);\n\nexport function useTheme(): ThemeContextValue {\n const ctx = useContext(ThemeContext);\n if (!ctx) {\n throw new Error(\"useTheme must be used within ThemeProvider\");\n }\n return ctx;\n}\n\nfunction isValidThemeId(id: string | null, colorways: Colorway[]): id is string {\n if (!id) return false;\n const colorway = id.endsWith(\"-dark\") ? id.slice(0, -\"-dark\".length) : id;\n return colorways.some((c) => c.id === colorway);\n}\n\nfunction resolveInitialTheme(\n colorways: Colorway[],\n defaultColorway: string,\n storageKey: string,\n): string {\n let stored: string | null = null;\n try {\n stored = localStorage.getItem(storageKey);\n } catch {\n // private mode without storage access; defaults stand\n }\n if (isValidThemeId(stored, colorways)) return stored;\n\n // No explicit user choice yet: honor the OS-level preference.\n let prefersDark = false;\n try {\n prefersDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n } catch {\n // matchMedia unavailable (rare); default to light\n }\n return prefersDark ? `${defaultColorway}-dark` : defaultColorway;\n}\n\nexport interface ThemeProviderProps {\n children: ReactNode;\n /** Colorway catalog. Defaults to the bundled riposte `COLORWAYS`. */\n colorways?: Colorway[];\n /** Colorway used when no choice is stored. Defaults to `forest`. */\n defaultColorway?: string;\n /** localStorage key for the persisted choice. Defaults to `rs_theme_v1`. */\n storageKey?: string;\n}\n\n/// Apply the theme to the document and persist the user's choice.\n///\n/// State of record is the resolved id (e.g. `forest-dark`); the picker UI\n/// derives colorway and mode from it. The DOM attribute is applied in\n/// useLayoutEffect so the first paint and every subsequent change pick up\n/// the correct theme without a flash.\nexport function ThemeProvider({\n children,\n colorways = COLORWAYS,\n defaultColorway = DEFAULT_COLORWAY,\n storageKey = DEFAULT_STORAGE_KEY,\n}: ThemeProviderProps) {\n const [theme, setThemeState] = useState(() =>\n resolveInitialTheme(colorways, defaultColorway, storageKey),\n );\n useLayoutEffect(() => {\n document.documentElement.dataset.theme = theme;\n }, [theme]);\n\n // Subscribe to OS preference changes so a fresh visitor (no explicit\n // choice yet) tracks their system theme, and stop tracking the moment\n // they pick. Pure subscription effect; no state-from-effect on mount.\n useEffect(() => {\n let media: MediaQueryList;\n try {\n media = window.matchMedia(\"(prefers-color-scheme: dark)\");\n } catch {\n return undefined;\n }\n function onChange(e: MediaQueryListEvent) {\n let stored: string | null = null;\n try {\n stored = localStorage.getItem(storageKey);\n } catch {\n // ignore\n }\n // User has made an explicit choice; don't override it.\n if (isValidThemeId(stored, colorways)) return;\n const next = e.matches ? `${defaultColorway}-dark` : defaultColorway;\n setThemeState(next);\n }\n media.addEventListener?.(\"change\", onChange);\n return () => {\n media.removeEventListener?.(\"change\", onChange);\n };\n }, [colorways, defaultColorway, storageKey]);\n\n function setTheme(id: string) {\n if (!isValidThemeId(id, colorways)) return;\n setThemeState(id);\n try {\n localStorage.setItem(storageKey, id);\n } catch {\n // ignore; the in-memory state still reflects the choice for this session\n }\n }\n\n // Convenience: flip just the mode while keeping the colorway. The picker\n // exposes this as a separate row so users can change one without\n // accidentally re-picking the other.\n function setMode(nextMode: ThemeMode) {\n if (nextMode !== \"light\" && nextMode !== \"dark\") return;\n const colorway = theme.endsWith(\"-dark\") ? theme.slice(0, -\"-dark\".length) : theme;\n setTheme(nextMode === \"dark\" ? `${colorway}-dark` : colorway);\n }\n\n const mode: ThemeMode = theme.endsWith(\"-dark\") ? \"dark\" : \"light\";\n\n return (\n <ThemeContext.Provider value={{ theme, setTheme, colorways, mode, setMode }}>\n {children}\n </ThemeContext.Provider>\n );\n}\n","/* This file is part of @cavebatsofware/riposte-pickers\n * Copyright (C) 2026 Grant DeFayette\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, version 3 of the License (GPL-3.0-only).\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.\n */\nimport { useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useTheme, type ThemeMode } from \"./ThemeContext\";\nimport PopoverPicker from \"../shared/PopoverPicker\";\n\nconst ARROW_KEYS = [\"ArrowDown\", \"ArrowUp\", \"ArrowRight\", \"ArrowLeft\", \"Home\", \"End\"];\n\n/// Resolve the next index for a roving radiogroup keystroke. ArrowDown /\n/// ArrowRight advance, ArrowUp / ArrowLeft retreat, Home / End jump. The\n/// list wraps at the ends.\nfunction nextRadioIndex(key: string, currentIndex: number, length: number): number {\n switch (key) {\n case \"Home\":\n return 0;\n case \"End\":\n return length - 1;\n case \"ArrowDown\":\n case \"ArrowRight\":\n return (currentIndex + 1) % length;\n case \"ArrowUp\":\n case \"ArrowLeft\":\n return (currentIndex - 1 + length) % length;\n default:\n return currentIndex;\n }\n}\n\nexport interface ThemePickerProps {\n /**\n * `popover` (default): round icon button in a header that opens a popover\n * grid. `inline`: the grid rendered directly, for use inside a drawer.\n */\n variant?: \"popover\" | \"inline\";\n /**\n * i18next namespace holding the `theme.*` keys. Defaults to `common`.\n * Merge the package's `themeResources` into this namespace.\n */\n namespace?: string;\n}\n\n/// Theme picker: a colorway switch and a light/dark mode toggle rendered as\n/// two adjacent ARIA radiogroups with roving tabindex. The underlying state\n/// is a single id of the form \"<colorway>-<mode>\" or just \"<colorway>\" for\n/// light-mode values (both handled by `ThemeContext.setTheme`).\nexport default function ThemePicker({ variant = \"popover\", namespace = \"common\" }: ThemePickerProps) {\n const { theme, setTheme, colorways, mode, setMode } = useTheme();\n const [open, setOpen] = useState(false);\n const colorwayBtnRefs = useRef<Array<HTMLButtonElement | null>>([]);\n const modeBtnRefs = useRef<Array<HTMLButtonElement | null>>([]);\n const { t } = useTranslation(namespace);\n\n // Today the theme id is \"<colorway>\" (light) or \"<colorway>-dark\". Derive\n // the current colorway from the resolved theme so the swatches show the\n // right active state regardless of which form is stored.\n const currentColorway = theme.endsWith(\"-dark\") ? theme.slice(0, -\"-dark\".length) : theme;\n\n const grid = (\n <div className=\"theme-picker-grid\">\n <div className=\"theme-picker-title\">{t(\"theme.title\")}</div>\n <div\n className=\"theme-swatches\"\n role=\"radiogroup\"\n tabIndex={-1}\n aria-label={t(\"theme.colorwayAria\")}\n >\n {colorways.map((c, idx) => {\n const active = c.id === currentColorway;\n // Colorway display name comes from the catalog so each language can\n // localize the marketing-style names. Fall back to the catalog's\n // hardcoded `c.label` if a key is missing.\n const label = t(`theme.colorways.${c.id}`, { defaultValue: c.label });\n return (\n <button\n key={c.id}\n ref={(el) => {\n colorwayBtnRefs.current[idx] = el;\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={active}\n tabIndex={active ? 0 : -1}\n className={`theme-swatch ${active ? \"active\" : \"\"}`}\n onClick={() => {\n setTheme(`${c.id}${mode === \"dark\" ? \"-dark\" : \"\"}`);\n if (variant === \"popover\") setOpen(false);\n }}\n onKeyDown={(e) => {\n if (!ARROW_KEYS.includes(e.key)) return;\n e.preventDefault();\n const next = nextRadioIndex(e.key, idx, colorways.length);\n const target = colorways[next];\n setTheme(`${target.id}${mode === \"dark\" ? \"-dark\" : \"\"}`);\n colorwayBtnRefs.current[next]?.focus();\n }}\n >\n <span\n className=\"theme-swatch-color\"\n style={{ background: c.swatch }}\n aria-hidden=\"true\"\n />\n <span className=\"theme-swatch-label\">{label}</span>\n {active && (\n <span className=\"theme-swatch-check\" aria-hidden=\"true\">\n ✓\n </span>\n )}\n </button>\n );\n })}\n </div>\n <div\n className=\"theme-mode-row\"\n role=\"radiogroup\"\n tabIndex={-1}\n aria-label={t(\"theme.modeAria\")}\n >\n {(() => {\n const modes: Array<{ id: ThemeMode; label: string }> = [\n { id: \"light\", label: t(\"theme.mode.light\") },\n { id: \"dark\", label: t(\"theme.mode.dark\") },\n ];\n return modes.map((m, idx) => (\n <button\n key={m.id}\n ref={(el) => {\n modeBtnRefs.current[idx] = el;\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={mode === m.id}\n tabIndex={mode === m.id ? 0 : -1}\n className={`theme-mode-btn ${mode === m.id ? \"active\" : \"\"}`}\n onClick={() => setMode(m.id)}\n onKeyDown={(e) => {\n if (!ARROW_KEYS.includes(e.key)) return;\n e.preventDefault();\n const next = nextRadioIndex(e.key, idx, modes.length);\n setMode(modes[next].id);\n modeBtnRefs.current[next]?.focus();\n }}\n >\n {m.label}\n </button>\n ));\n })()}\n </div>\n </div>\n );\n\n return (\n <PopoverPicker\n variant={variant}\n open={open}\n onOpenChange={setOpen}\n className=\"theme-picker\"\n toggleAriaLabel={t(\"theme.toggleAria\")}\n popoverAriaLabel={t(\"theme.dialogAria\")}\n toggleIcon={\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"13.5\" cy=\"6.5\" r=\"0.5\" fill=\"currentColor\" />\n <circle cx=\"17.5\" cy=\"10.5\" r=\"0.5\" fill=\"currentColor\" />\n <circle cx=\"8.5\" cy=\"7.5\" r=\"0.5\" fill=\"currentColor\" />\n <circle cx=\"6.5\" cy=\"12.5\" r=\"0.5\" fill=\"currentColor\" />\n <path d=\"M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c1 0 1.5-.5 1.5-1.2 0-.4-.1-.7-.4-1-.2-.3-.4-.6-.4-1 0-.7.5-1.2 1.2-1.2H16c3.3 0 6-2.7 6-6 0-5-4.5-9-10-9z\" />\n </svg>\n }\n >\n {grid}\n </PopoverPicker>\n );\n}\n"]}
|