@volksverpetzer/ui-web 0.6.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/Alert.css +14 -0
- package/dist/Alert.d.ts +13 -0
- package/dist/Alert.js +17 -0
- package/dist/Input.css +52 -0
- package/dist/Input.d.ts +11 -0
- package/dist/Input.js +14 -0
- package/dist/Slider.css +60 -0
- package/dist/Slider.d.ts +17 -0
- package/dist/Slider.js +20 -0
- package/dist/ThemeToggle.css +6 -0
- package/dist/ThemeToggle.d.ts +9 -0
- package/dist/ThemeToggle.js +75 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/styles.css +136 -0
- package/package.json +2 -2
package/dist/Alert.css
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
.vvp-ui-alert {
|
|
2
|
+
border-radius: var(--vvp-radius-sm, 8px);
|
|
3
|
+
border: 1px solid var(--vvp-surface-input, #badde8);
|
|
4
|
+
background: var(--vvp-background, #fff);
|
|
5
|
+
color: var(--vvp-text, #111);
|
|
6
|
+
padding: var(--vvp-spacing-md, 10px) var(--vvp-spacing-lg, 16px);
|
|
7
|
+
font-size: var(--vvp-font-size-sm, 14px);
|
|
8
|
+
line-height: 1.5;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.vvp-ui-alert--error {
|
|
12
|
+
border-color: var(--vvp-error, #c62828);
|
|
13
|
+
color: var(--vvp-error, #c62828);
|
|
14
|
+
}
|
package/dist/Alert.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
import "./Alert.css";
|
|
3
|
+
export type AlertVariant = "neutral" | "error";
|
|
4
|
+
export interface AlertProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** @default "neutral" */
|
|
6
|
+
variant?: AlertVariant;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Inline form/status feedback — validation errors, submission results.
|
|
10
|
+
* Plain container like `Card`; put whatever content (text, a title, an
|
|
11
|
+
* icon) you need inside.
|
|
12
|
+
*/
|
|
13
|
+
export declare function Alert({ variant, className, ...rest }: AlertProps): import("react").JSX.Element;
|
package/dist/Alert.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import "./Alert.css";
|
|
3
|
+
/**
|
|
4
|
+
* Inline form/status feedback — validation errors, submission results.
|
|
5
|
+
* Plain container like `Card`; put whatever content (text, a title, an
|
|
6
|
+
* icon) you need inside.
|
|
7
|
+
*/
|
|
8
|
+
export function Alert({ variant = "neutral", className, ...rest }) {
|
|
9
|
+
const classes = [
|
|
10
|
+
"vvp-ui-alert",
|
|
11
|
+
variant === "error" && "vvp-ui-alert--error",
|
|
12
|
+
className,
|
|
13
|
+
]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join(" ");
|
|
16
|
+
return _jsx("div", { role: "alert", className: classes, ...rest });
|
|
17
|
+
}
|
package/dist/Input.css
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
*/
|
|
10
|
+
.vvp-ui-input {
|
|
11
|
+
display: block;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
width: 100%;
|
|
14
|
+
min-width: 0;
|
|
15
|
+
border-radius: var(--vvp-radius-md, 12px);
|
|
16
|
+
border: 1px solid var(--vvp-surface, #e2f0f5);
|
|
17
|
+
background: var(--vvp-background, #fff);
|
|
18
|
+
color: var(--vvp-text, #111);
|
|
19
|
+
padding: var(--vvp-spacing-lg, 16px);
|
|
20
|
+
font-family: inherit;
|
|
21
|
+
font-size: var(--vvp-font-size-base, 16px);
|
|
22
|
+
line-height: 1.4;
|
|
23
|
+
transition: border-color 0.15s ease;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.vvp-ui-input::placeholder {
|
|
27
|
+
color: var(--vvp-text-muted, #666);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.vvp-ui-input:focus-visible {
|
|
31
|
+
outline: 2px solid var(--vvp-primary-muted, #3893c0);
|
|
32
|
+
outline-offset: 2px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.vvp-ui-input:disabled {
|
|
36
|
+
background: var(--vvp-surface-disabled, #ddd);
|
|
37
|
+
color: var(--vvp-text-muted, #666);
|
|
38
|
+
cursor: not-allowed;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
* Consumers flag a failed-validation field via the native aria-invalid
|
|
43
|
+
* attribute rather than a bespoke prop — most form libraries already set
|
|
44
|
+
* it automatically, and it's the accessible way to mark the field anyway.
|
|
45
|
+
*/
|
|
46
|
+
.vvp-ui-input[aria-invalid="true"] {
|
|
47
|
+
border-color: var(--vvp-error, #c62828);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.vvp-ui-input[aria-invalid="true"]:focus-visible {
|
|
51
|
+
outline-color: var(--vvp-error, #c62828);
|
|
52
|
+
}
|
package/dist/Input.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { InputHTMLAttributes } from "react";
|
|
2
|
+
import "./Input.css";
|
|
3
|
+
export type InputProps = InputHTMLAttributes<HTMLInputElement>;
|
|
4
|
+
/**
|
|
5
|
+
* Shared brand text input. Styled entirely off the `--vvp-*` custom
|
|
6
|
+
* properties emitted by `@volksverpetzer/design-tokens`, same as `Button`.
|
|
7
|
+
* Mark a field invalid with the native `aria-invalid` attribute rather than
|
|
8
|
+
* a bespoke `invalid` prop, so it composes with whatever validation a
|
|
9
|
+
* consumer already has (react-hook-form, Formik, or hand-rolled).
|
|
10
|
+
*/
|
|
11
|
+
export declare const Input: import("react").ForwardRefExoticComponent<InputProps & import("react").RefAttributes<HTMLInputElement>>;
|
package/dist/Input.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import "./Input.css";
|
|
4
|
+
/**
|
|
5
|
+
* Shared brand text input. Styled entirely off the `--vvp-*` custom
|
|
6
|
+
* properties emitted by `@volksverpetzer/design-tokens`, same as `Button`.
|
|
7
|
+
* Mark a field invalid with the native `aria-invalid` attribute rather than
|
|
8
|
+
* a bespoke `invalid` prop, so it composes with whatever validation a
|
|
9
|
+
* consumer already has (react-hook-form, Formik, or hand-rolled).
|
|
10
|
+
*/
|
|
11
|
+
export const Input = forwardRef(function Input({ className, ...rest }, ref) {
|
|
12
|
+
const classes = ["vvp-ui-input", className].filter(Boolean).join(" ");
|
|
13
|
+
return _jsx("input", { ref: ref, className: classes, ...rest });
|
|
14
|
+
});
|
package/dist/Slider.css
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
.vvp-ui-slider {
|
|
2
|
+
-webkit-appearance: none;
|
|
3
|
+
appearance: none;
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: var(--vvp-spacing-sm, 8px);
|
|
6
|
+
margin: 0;
|
|
7
|
+
border-radius: var(--vvp-radius-full, 9999px);
|
|
8
|
+
background: linear-gradient(
|
|
9
|
+
to right,
|
|
10
|
+
var(--vvp-primary, #1b7194) 0%,
|
|
11
|
+
var(--vvp-primary, #1b7194) var(--vvp-ui-slider-fill, 0%),
|
|
12
|
+
var(--vvp-surface, #e2f0f5) var(--vvp-ui-slider-fill, 0%),
|
|
13
|
+
var(--vvp-surface, #e2f0f5) 100%
|
|
14
|
+
);
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.vvp-ui-slider:disabled {
|
|
19
|
+
opacity: 0.6;
|
|
20
|
+
cursor: not-allowed;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* Thumb: duplicated across the -webkit-/-moz- pseudo-elements because
|
|
25
|
+
* browsers drop an entire rule if any selector in it is unsupported — they
|
|
26
|
+
* can't be combined into one shared rule.
|
|
27
|
+
*/
|
|
28
|
+
.vvp-ui-slider::-webkit-slider-thumb {
|
|
29
|
+
-webkit-appearance: none;
|
|
30
|
+
appearance: none;
|
|
31
|
+
width: var(--vvp-spacing-lg, 16px);
|
|
32
|
+
height: var(--vvp-spacing-lg, 16px);
|
|
33
|
+
border-radius: var(--vvp-radius-full, 9999px);
|
|
34
|
+
background: var(--vvp-primary, #1b7194);
|
|
35
|
+
border: 2px solid var(--vvp-background, #fff);
|
|
36
|
+
box-shadow: var(--vvp-elevation-xs, 0px 1px 1.41px rgba(0, 0, 0, 0.2));
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.vvp-ui-slider::-moz-range-thumb {
|
|
41
|
+
width: var(--vvp-spacing-lg, 16px);
|
|
42
|
+
height: var(--vvp-spacing-lg, 16px);
|
|
43
|
+
border-radius: var(--vvp-radius-full, 9999px);
|
|
44
|
+
background: var(--vvp-primary, #1b7194);
|
|
45
|
+
border: 2px solid var(--vvp-background, #fff);
|
|
46
|
+
box-shadow: var(--vvp-elevation-xs, 0px 1px 1.41px rgba(0, 0, 0, 0.2));
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.vvp-ui-slider::-moz-range-track {
|
|
51
|
+
background: transparent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.vvp-ui-slider:disabled::-webkit-slider-thumb {
|
|
55
|
+
cursor: not-allowed;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.vvp-ui-slider:disabled::-moz-range-thumb {
|
|
59
|
+
cursor: not-allowed;
|
|
60
|
+
}
|
package/dist/Slider.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { InputHTMLAttributes } from "react";
|
|
2
|
+
import "./Slider.css";
|
|
3
|
+
export interface SliderProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "defaultValue" | "onChange"> {
|
|
4
|
+
value: number;
|
|
5
|
+
min?: number;
|
|
6
|
+
max: number;
|
|
7
|
+
step?: number;
|
|
8
|
+
onValueChange?: (value: number) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Single-thumb range control — audio/video scrubbing, volume, numeric
|
|
12
|
+
* filters. A native `<input type="range">` under the hood (keyboard, touch,
|
|
13
|
+
* and screen-reader support come for free) themed with `--vvp-*` tokens.
|
|
14
|
+
* The filled portion up to the thumb is a CSS gradient driven by a custom
|
|
15
|
+
* property computed from `value`, so no extra DOM is needed for the track.
|
|
16
|
+
*/
|
|
17
|
+
export declare const Slider: import("react").ForwardRefExoticComponent<SliderProps & import("react").RefAttributes<HTMLInputElement>>;
|
package/dist/Slider.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import "./Slider.css";
|
|
4
|
+
/**
|
|
5
|
+
* Single-thumb range control — audio/video scrubbing, volume, numeric
|
|
6
|
+
* filters. A native `<input type="range">` under the hood (keyboard, touch,
|
|
7
|
+
* and screen-reader support come for free) themed with `--vvp-*` tokens.
|
|
8
|
+
* The filled portion up to the thumb is a CSS gradient driven by a custom
|
|
9
|
+
* property computed from `value`, so no extra DOM is needed for the track.
|
|
10
|
+
*/
|
|
11
|
+
export const Slider = forwardRef(function Slider({ value, min = 0, max, step = 1, onValueChange, className, style, ...rest }, ref) {
|
|
12
|
+
const rawPercent = max > min ? ((value - min) / (max - min)) * 100 : 0;
|
|
13
|
+
const percent = Math.min(100, Math.max(0, rawPercent));
|
|
14
|
+
const classes = ["vvp-ui-slider", className].filter(Boolean).join(" ");
|
|
15
|
+
const fillStyle = {
|
|
16
|
+
...style,
|
|
17
|
+
"--vvp-ui-slider-fill": `${percent}%`,
|
|
18
|
+
};
|
|
19
|
+
return (_jsx("input", { ref: ref, type: "range", className: classes, value: value, min: min, max: max, step: step, style: fillStyle, onChange: (event) => onValueChange?.(event.target.valueAsNumber), ...rest }));
|
|
20
|
+
});
|
|
@@ -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
|
@@ -2,3 +2,7 @@ export { Button, type ButtonProps, type ButtonVariant, type ButtonSize, } from "
|
|
|
2
2
|
export { Badge, type BadgeProps, type BadgeVariant } from "./Badge";
|
|
3
3
|
export { Card, type CardProps } from "./Card";
|
|
4
4
|
export { ProgressBar, type ProgressBarProps, type ProgressBarMilestone, } from "./ProgressBar";
|
|
5
|
+
export { Input, type InputProps } from "./Input";
|
|
6
|
+
export { Alert, type AlertProps, type AlertVariant } from "./Alert";
|
|
7
|
+
export { Slider, type SliderProps } from "./Slider";
|
|
8
|
+
export { ThemeToggle } from "./ThemeToggle";
|
package/dist/index.js
CHANGED
|
@@ -2,3 +2,7 @@ export { Button, } from "./Button";
|
|
|
2
2
|
export { Badge } from "./Badge";
|
|
3
3
|
export { Card } from "./Card";
|
|
4
4
|
export { ProgressBar, } from "./ProgressBar";
|
|
5
|
+
export { Input } from "./Input";
|
|
6
|
+
export { Alert } from "./Alert";
|
|
7
|
+
export { Slider } from "./Slider";
|
|
8
|
+
export { ThemeToggle } from "./ThemeToggle";
|
package/dist/styles.css
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
.vvp-ui-alert {
|
|
2
|
+
border-radius: var(--vvp-radius-sm, 8px);
|
|
3
|
+
border: 1px solid var(--vvp-surface-input, #badde8);
|
|
4
|
+
background: var(--vvp-background, #fff);
|
|
5
|
+
color: var(--vvp-text, #111);
|
|
6
|
+
padding: var(--vvp-spacing-md, 10px) var(--vvp-spacing-lg, 16px);
|
|
7
|
+
font-size: var(--vvp-font-size-sm, 14px);
|
|
8
|
+
line-height: 1.5;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.vvp-ui-alert--error {
|
|
12
|
+
border-color: var(--vvp-error, #c62828);
|
|
13
|
+
color: var(--vvp-error, #c62828);
|
|
14
|
+
}
|
|
15
|
+
|
|
1
16
|
.vvp-ui-badge {
|
|
2
17
|
display: inline-flex;
|
|
3
18
|
align-items: center;
|
|
@@ -148,6 +163,59 @@
|
|
|
148
163
|
border: 1px solid var(--vvp-surface-input, #badde8);
|
|
149
164
|
}
|
|
150
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
|
+
*/
|
|
175
|
+
.vvp-ui-input {
|
|
176
|
+
display: block;
|
|
177
|
+
box-sizing: border-box;
|
|
178
|
+
width: 100%;
|
|
179
|
+
min-width: 0;
|
|
180
|
+
border-radius: var(--vvp-radius-md, 12px);
|
|
181
|
+
border: 1px solid var(--vvp-surface, #e2f0f5);
|
|
182
|
+
background: var(--vvp-background, #fff);
|
|
183
|
+
color: var(--vvp-text, #111);
|
|
184
|
+
padding: var(--vvp-spacing-lg, 16px);
|
|
185
|
+
font-family: inherit;
|
|
186
|
+
font-size: var(--vvp-font-size-base, 16px);
|
|
187
|
+
line-height: 1.4;
|
|
188
|
+
transition: border-color 0.15s ease;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.vvp-ui-input::placeholder {
|
|
192
|
+
color: var(--vvp-text-muted, #666);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.vvp-ui-input:focus-visible {
|
|
196
|
+
outline: 2px solid var(--vvp-primary-muted, #3893c0);
|
|
197
|
+
outline-offset: 2px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.vvp-ui-input:disabled {
|
|
201
|
+
background: var(--vvp-surface-disabled, #ddd);
|
|
202
|
+
color: var(--vvp-text-muted, #666);
|
|
203
|
+
cursor: not-allowed;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/*
|
|
207
|
+
* Consumers flag a failed-validation field via the native aria-invalid
|
|
208
|
+
* attribute rather than a bespoke prop — most form libraries already set
|
|
209
|
+
* it automatically, and it's the accessible way to mark the field anyway.
|
|
210
|
+
*/
|
|
211
|
+
.vvp-ui-input[aria-invalid="true"] {
|
|
212
|
+
border-color: var(--vvp-error, #c62828);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.vvp-ui-input[aria-invalid="true"]:focus-visible {
|
|
216
|
+
outline-color: var(--vvp-error, #c62828);
|
|
217
|
+
}
|
|
218
|
+
|
|
151
219
|
.vvp-ui-progress {
|
|
152
220
|
width: 100%;
|
|
153
221
|
text-align: center;
|
|
@@ -231,3 +299,71 @@
|
|
|
231
299
|
color: var(--vvp-text, #111);
|
|
232
300
|
}
|
|
233
301
|
|
|
302
|
+
.vvp-ui-slider {
|
|
303
|
+
-webkit-appearance: none;
|
|
304
|
+
appearance: none;
|
|
305
|
+
width: 100%;
|
|
306
|
+
height: var(--vvp-spacing-sm, 8px);
|
|
307
|
+
margin: 0;
|
|
308
|
+
border-radius: var(--vvp-radius-full, 9999px);
|
|
309
|
+
background: linear-gradient(
|
|
310
|
+
to right,
|
|
311
|
+
var(--vvp-primary, #1b7194) 0%,
|
|
312
|
+
var(--vvp-primary, #1b7194) var(--vvp-ui-slider-fill, 0%),
|
|
313
|
+
var(--vvp-surface, #e2f0f5) var(--vvp-ui-slider-fill, 0%),
|
|
314
|
+
var(--vvp-surface, #e2f0f5) 100%
|
|
315
|
+
);
|
|
316
|
+
cursor: pointer;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.vvp-ui-slider:disabled {
|
|
320
|
+
opacity: 0.6;
|
|
321
|
+
cursor: not-allowed;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/*
|
|
325
|
+
* Thumb: duplicated across the -webkit-/-moz- pseudo-elements because
|
|
326
|
+
* browsers drop an entire rule if any selector in it is unsupported — they
|
|
327
|
+
* can't be combined into one shared rule.
|
|
328
|
+
*/
|
|
329
|
+
.vvp-ui-slider::-webkit-slider-thumb {
|
|
330
|
+
-webkit-appearance: none;
|
|
331
|
+
appearance: none;
|
|
332
|
+
width: var(--vvp-spacing-lg, 16px);
|
|
333
|
+
height: var(--vvp-spacing-lg, 16px);
|
|
334
|
+
border-radius: var(--vvp-radius-full, 9999px);
|
|
335
|
+
background: var(--vvp-primary, #1b7194);
|
|
336
|
+
border: 2px solid var(--vvp-background, #fff);
|
|
337
|
+
box-shadow: var(--vvp-elevation-xs, 0px 1px 1.41px rgba(0, 0, 0, 0.2));
|
|
338
|
+
cursor: pointer;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.vvp-ui-slider::-moz-range-thumb {
|
|
342
|
+
width: var(--vvp-spacing-lg, 16px);
|
|
343
|
+
height: var(--vvp-spacing-lg, 16px);
|
|
344
|
+
border-radius: var(--vvp-radius-full, 9999px);
|
|
345
|
+
background: var(--vvp-primary, #1b7194);
|
|
346
|
+
border: 2px solid var(--vvp-background, #fff);
|
|
347
|
+
box-shadow: var(--vvp-elevation-xs, 0px 1px 1.41px rgba(0, 0, 0, 0.2));
|
|
348
|
+
cursor: pointer;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.vvp-ui-slider::-moz-range-track {
|
|
352
|
+
background: transparent;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.vvp-ui-slider:disabled::-webkit-slider-thumb {
|
|
356
|
+
cursor: not-allowed;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.vvp-ui-slider:disabled::-moz-range-thumb {
|
|
360
|
+
cursor: not-allowed;
|
|
361
|
+
}
|
|
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) 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": {
|