@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 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-sm, 8px);
7
- border: 1px solid var(--vvp-surface-input, #badde8);
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-sm, 8px) var(--vvp-spacing-md, 10px);
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: none;
25
- border-color: var(--vvp-primary, #1b7194);
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
- box-shadow: 0 0 0 3px
47
- color-mix(in srgb, var(--vvp-error, #c62828) 25%, transparent);
51
+ outline-color: var(--vvp-error, #c62828);
48
52
  }
@@ -0,0 +1,6 @@
1
+ .vvp-ui-theme-toggle {
2
+ position: fixed;
3
+ top: var(--vvp-spacing-lg, 16px);
4
+ right: var(--vvp-spacing-lg, 16px);
5
+ z-index: 50;
6
+ }
@@ -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
@@ -5,3 +5,4 @@ export { ProgressBar, } from "./ProgressBar";
5
5
  export { Input } from "./Input";
6
6
  export { Alert } from "./Alert";
7
7
  export { Slider } from "./Slider";
8
+ export { ThemeToggle } from "./ThemeToggle";
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-sm, 8px);
172
- border: 1px solid var(--vvp-surface-input, #badde8);
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-sm, 8px) var(--vvp-spacing-md, 10px);
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: none;
190
- border-color: var(--vvp-primary, #1b7194);
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
- box-shadow: 0 0 0 3px
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.7.0",
4
- "description": "Shared brand-visible React components (Button, Badge, Card, ProgressBar, Input, Alert) for vvp_link_shortener, vvp_divi5_extensions, and crowdfunding. 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.",
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": {