@volksverpetzer/ui-web 0.7.0 → 0.8.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/dist/Input.css +16 -12
- package/dist/ThemeToggle.css +6 -0
- package/dist/ThemeToggle.d.ts +9 -0
- package/dist/ThemeToggle.js +75 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/styles.css +23 -12
- package/package.json +2 -2
package/dist/Input.css
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* radius/border-color/padding are pinned to whichever existing token lands
|
|
3
|
+
* closest to vvp_crowdfunding's donation-form input (the component's actual
|
|
4
|
+
* first real consumer, pre-dating this package): radius 14px sits exactly
|
|
5
|
+
* between md/lg, resolved to md since that's the scale's own "buttons and
|
|
6
|
+
* form controls" step; padding 14px/16px both round to lg; the border color
|
|
7
|
+
* (#d8e6ee) is closer to --vvp-surface than the likelier-sounding
|
|
8
|
+
* --vvp-surface-input by actual RGB distance (~16 vs ~32).
|
|
9
|
+
*/
|
|
1
10
|
.vvp-ui-input {
|
|
2
11
|
display: block;
|
|
3
12
|
box-sizing: border-box;
|
|
4
13
|
width: 100%;
|
|
5
14
|
min-width: 0;
|
|
6
|
-
border-radius: var(--vvp-radius-
|
|
7
|
-
border: 1px solid var(--vvp-surface
|
|
15
|
+
border-radius: var(--vvp-radius-md, 12px);
|
|
16
|
+
border: 1px solid var(--vvp-surface, #e2f0f5);
|
|
8
17
|
background: var(--vvp-background, #fff);
|
|
9
18
|
color: var(--vvp-text, #111);
|
|
10
|
-
padding: var(--vvp-spacing-
|
|
19
|
+
padding: var(--vvp-spacing-lg, 16px);
|
|
11
20
|
font-family: inherit;
|
|
12
21
|
font-size: var(--vvp-font-size-base, 16px);
|
|
13
22
|
line-height: 1.4;
|
|
14
|
-
transition:
|
|
15
|
-
border-color 0.15s ease,
|
|
16
|
-
box-shadow 0.15s ease;
|
|
23
|
+
transition: border-color 0.15s ease;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
.vvp-ui-input::placeholder {
|
|
@@ -21,10 +28,8 @@
|
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
.vvp-ui-input:focus-visible {
|
|
24
|
-
outline:
|
|
25
|
-
|
|
26
|
-
box-shadow: 0 0 0 3px
|
|
27
|
-
color-mix(in srgb, var(--vvp-primary, #1b7194) 25%, transparent);
|
|
31
|
+
outline: 2px solid var(--vvp-primary-muted, #3893c0);
|
|
32
|
+
outline-offset: 2px;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
.vvp-ui-input:disabled {
|
|
@@ -43,6 +48,5 @@
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
.vvp-ui-input[aria-invalid="true"]:focus-visible {
|
|
46
|
-
|
|
47
|
-
color-mix(in srgb, var(--vvp-error, #c62828) 25%, transparent);
|
|
51
|
+
outline-color: var(--vvp-error, #c62828);
|
|
48
52
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "./ThemeToggle.css";
|
|
2
|
+
/**
|
|
3
|
+
* Fixed-corner light/dark toggle. Persists to localStorage, falls back to
|
|
4
|
+
* `prefers-color-scheme`, and flips the `dark` class on `<html>` — the same
|
|
5
|
+
* convention the `--vvp-*` tokens' `.dark` scope expects. Browser-only (no
|
|
6
|
+
* Divi/React Native use case), so it always renders — there's no server
|
|
7
|
+
* variant to opt out of.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ThemeToggle(): import("react").JSX.Element;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
4
|
+
import { Button } from "./Button";
|
|
5
|
+
import "./ThemeToggle.css";
|
|
6
|
+
const STORAGE_KEY = "vvp-theme";
|
|
7
|
+
const listeners = new Set();
|
|
8
|
+
// Session fallback for when localStorage throws (private/sandboxed
|
|
9
|
+
// contexts): without it, setIsDark's write would fail silently and
|
|
10
|
+
// getIsDark would keep re-deriving the same value from matchMedia on every
|
|
11
|
+
// read, making the toggle look inert even though the click registered.
|
|
12
|
+
let memoryIsDark = null;
|
|
13
|
+
function getIsDark() {
|
|
14
|
+
if (memoryIsDark !== null)
|
|
15
|
+
return memoryIsDark;
|
|
16
|
+
try {
|
|
17
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
18
|
+
if (stored)
|
|
19
|
+
return stored === "dark";
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// localStorage can throw (e.g. blocked storage in private/sandboxed
|
|
23
|
+
// contexts) — fall back to OS preference below.
|
|
24
|
+
}
|
|
25
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
26
|
+
}
|
|
27
|
+
// The server can't know the stored/OS preference, so it always reports
|
|
28
|
+
// light. useSyncExternalStore uses this consistently during hydration
|
|
29
|
+
// (matching the server-rendered markup exactly), then re-renders with the
|
|
30
|
+
// real client value right after — no hydration mismatch, unlike reading
|
|
31
|
+
// localStorage in a mount effect.
|
|
32
|
+
function getServerIsDark() {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
function subscribe(callback) {
|
|
36
|
+
listeners.add(callback);
|
|
37
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
38
|
+
media.addEventListener("change", callback);
|
|
39
|
+
return () => {
|
|
40
|
+
listeners.delete(callback);
|
|
41
|
+
media.removeEventListener("change", callback);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function setIsDark(next) {
|
|
45
|
+
memoryIsDark = next;
|
|
46
|
+
try {
|
|
47
|
+
localStorage.setItem(STORAGE_KEY, next ? "dark" : "light");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Storage unavailable — the toggle still works for this session via
|
|
51
|
+
// memoryIsDark above, it just won't persist across reloads.
|
|
52
|
+
}
|
|
53
|
+
listeners.forEach((listener) => listener());
|
|
54
|
+
}
|
|
55
|
+
function applyTheme(dark) {
|
|
56
|
+
document.documentElement.classList.toggle("dark", dark);
|
|
57
|
+
}
|
|
58
|
+
const SunIcon = () => (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("circle", { cx: "12", cy: "12", r: "4" }), _jsx("path", { d: "M12 2v2" }), _jsx("path", { d: "M12 20v2" }), _jsx("path", { d: "m4.93 4.93 1.41 1.41" }), _jsx("path", { d: "m17.66 17.66 1.41 1.41" }), _jsx("path", { d: "M2 12h2" }), _jsx("path", { d: "M20 12h2" }), _jsx("path", { d: "m6.34 17.66-1.41 1.41" }), _jsx("path", { d: "m19.07 4.93-1.41 1.41" })] }));
|
|
59
|
+
const MoonIcon = () => (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" }) }));
|
|
60
|
+
/**
|
|
61
|
+
* Fixed-corner light/dark toggle. Persists to localStorage, falls back to
|
|
62
|
+
* `prefers-color-scheme`, and flips the `dark` class on `<html>` — the same
|
|
63
|
+
* convention the `--vvp-*` tokens' `.dark` scope expects. Browser-only (no
|
|
64
|
+
* Divi/React Native use case), so it always renders — there's no server
|
|
65
|
+
* variant to opt out of.
|
|
66
|
+
*/
|
|
67
|
+
export function ThemeToggle() {
|
|
68
|
+
const isDark = useSyncExternalStore(subscribe, getIsDark, getServerIsDark);
|
|
69
|
+
// Only synchronizes the DOM with the resolved value — never calls
|
|
70
|
+
// setState, so it can't trigger cascading renders.
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
applyTheme(isDark);
|
|
73
|
+
}, [isDark]);
|
|
74
|
+
return (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setIsDark(!isDark), "aria-label": isDark ? "Switch to light mode" : "Switch to dark mode", className: "vvp-ui-theme-toggle", children: isDark ? _jsx(SunIcon, {}) : _jsx(MoonIcon, {}) }));
|
|
75
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export { ProgressBar, type ProgressBarProps, type ProgressBarMilestone, } from "
|
|
|
5
5
|
export { Input, type InputProps } from "./Input";
|
|
6
6
|
export { Alert, type AlertProps, type AlertVariant } from "./Alert";
|
|
7
7
|
export { Slider, type SliderProps } from "./Slider";
|
|
8
|
+
export { ThemeToggle } from "./ThemeToggle";
|
package/dist/index.js
CHANGED
package/dist/styles.css
CHANGED
|
@@ -163,22 +163,29 @@
|
|
|
163
163
|
border: 1px solid var(--vvp-surface-input, #badde8);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/*
|
|
167
|
+
* radius/border-color/padding are pinned to whichever existing token lands
|
|
168
|
+
* closest to vvp_crowdfunding's donation-form input (the component's actual
|
|
169
|
+
* first real consumer, pre-dating this package): radius 14px sits exactly
|
|
170
|
+
* between md/lg, resolved to md since that's the scale's own "buttons and
|
|
171
|
+
* form controls" step; padding 14px/16px both round to lg; the border color
|
|
172
|
+
* (#d8e6ee) is closer to --vvp-surface than the likelier-sounding
|
|
173
|
+
* --vvp-surface-input by actual RGB distance (~16 vs ~32).
|
|
174
|
+
*/
|
|
166
175
|
.vvp-ui-input {
|
|
167
176
|
display: block;
|
|
168
177
|
box-sizing: border-box;
|
|
169
178
|
width: 100%;
|
|
170
179
|
min-width: 0;
|
|
171
|
-
border-radius: var(--vvp-radius-
|
|
172
|
-
border: 1px solid var(--vvp-surface
|
|
180
|
+
border-radius: var(--vvp-radius-md, 12px);
|
|
181
|
+
border: 1px solid var(--vvp-surface, #e2f0f5);
|
|
173
182
|
background: var(--vvp-background, #fff);
|
|
174
183
|
color: var(--vvp-text, #111);
|
|
175
|
-
padding: var(--vvp-spacing-
|
|
184
|
+
padding: var(--vvp-spacing-lg, 16px);
|
|
176
185
|
font-family: inherit;
|
|
177
186
|
font-size: var(--vvp-font-size-base, 16px);
|
|
178
187
|
line-height: 1.4;
|
|
179
|
-
transition:
|
|
180
|
-
border-color 0.15s ease,
|
|
181
|
-
box-shadow 0.15s ease;
|
|
188
|
+
transition: border-color 0.15s ease;
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
.vvp-ui-input::placeholder {
|
|
@@ -186,10 +193,8 @@
|
|
|
186
193
|
}
|
|
187
194
|
|
|
188
195
|
.vvp-ui-input:focus-visible {
|
|
189
|
-
outline:
|
|
190
|
-
|
|
191
|
-
box-shadow: 0 0 0 3px
|
|
192
|
-
color-mix(in srgb, var(--vvp-primary, #1b7194) 25%, transparent);
|
|
196
|
+
outline: 2px solid var(--vvp-primary-muted, #3893c0);
|
|
197
|
+
outline-offset: 2px;
|
|
193
198
|
}
|
|
194
199
|
|
|
195
200
|
.vvp-ui-input:disabled {
|
|
@@ -208,8 +213,7 @@
|
|
|
208
213
|
}
|
|
209
214
|
|
|
210
215
|
.vvp-ui-input[aria-invalid="true"]:focus-visible {
|
|
211
|
-
|
|
212
|
-
color-mix(in srgb, var(--vvp-error, #c62828) 25%, transparent);
|
|
216
|
+
outline-color: var(--vvp-error, #c62828);
|
|
213
217
|
}
|
|
214
218
|
|
|
215
219
|
.vvp-ui-progress {
|
|
@@ -356,3 +360,10 @@
|
|
|
356
360
|
cursor: not-allowed;
|
|
357
361
|
}
|
|
358
362
|
|
|
363
|
+
.vvp-ui-theme-toggle {
|
|
364
|
+
position: fixed;
|
|
365
|
+
top: var(--vvp-spacing-lg, 16px);
|
|
366
|
+
right: var(--vvp-spacing-lg, 16px);
|
|
367
|
+
z-index: 50;
|
|
368
|
+
}
|
|
369
|
+
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volksverpetzer/ui-web",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared brand-visible React components (Button, Badge, Card, ProgressBar, Input, Alert) for vvp_link_shortener, vvp_divi5_extensions, and
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Shared brand-visible React components (Button, Badge, Card, ProgressBar, Input, Alert, Slider, ThemeToggle) for vvp_link_shortener, vvp_divi5_extensions, crowdfunding, and vectorcrawl. Plain, hand-scoped CSS (vvp-ui-* class prefix) consuming the --vvp-* custom properties emitted by @volksverpetzer/design-tokens — no Tailwind classes, so it drops into a Tailwind app and Divi's WordPress-embedded CSS alike.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|