@volksverpetzer/ui-web 0.12.0 → 0.13.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/Badge.css CHANGED
@@ -58,3 +58,31 @@
58
58
  background: color-mix(in srgb, var(--vvp-pruefpunkt, #442674) 15%, white);
59
59
  color: var(--vvp-pruefpunkt, #442674);
60
60
  }
61
+
62
+ /*
63
+ * Dark mode: the light-mode formula above mixes toward literal `white`,
64
+ * which is theme-independent. Dark-mode brand colors are deliberately
65
+ * lightened for legibility on a near-black page, so mixed with `white` at
66
+ * 15% they lose too much contrast against their own text color (as low as
67
+ * ~2:1 for some variant/brand pairings, against a 4.5:1 requirement).
68
+ *
69
+ * Each variant below uses whichever tint direction and percentage clears
70
+ * 4.5:1 for both Volksverpetzer and Mimikama's dark palettes (verified with
71
+ * the WCAG contrast formula) — primary and accent read fine tinted toward
72
+ * white, error and pruefpunkt need black instead.
73
+ */
74
+ .dark .vvp-ui-badge--primary {
75
+ background: color-mix(in srgb, var(--vvp-primary, #1b7194) 5%, white);
76
+ }
77
+
78
+ .dark .vvp-ui-badge--accent {
79
+ background: #fff;
80
+ }
81
+
82
+ .dark .vvp-ui-badge--error {
83
+ background: color-mix(in srgb, var(--vvp-error, #ef5350) 15%, black);
84
+ }
85
+
86
+ .dark .vvp-ui-badge--pruefpunkt {
87
+ background: color-mix(in srgb, var(--vvp-pruefpunkt, #8b5cf6) 10%, black);
88
+ }
package/dist/Button.css CHANGED
@@ -56,8 +56,8 @@
56
56
 
57
57
  .vvp-ui-btn--ghost {
58
58
  background: transparent;
59
- color: var(--vvp-primary, #1b7194);
60
- border: 2px solid var(--vvp-primary, #1b7194);
59
+ color: var(--vvp-link-text, #1b7194);
60
+ border: 2px solid var(--vvp-link-text, #1b7194);
61
61
  }
62
62
 
63
63
  .vvp-ui-btn--ghost:hover:not(:disabled) {
@@ -13,7 +13,7 @@
13
13
  .vvp-ui-linkbtn {
14
14
  border-radius: var(--vvp-radius-md, 12px);
15
15
  background: transparent;
16
- color: var(--vvp-primary, #1b7194);
16
+ color: var(--vvp-link-text, #1b7194);
17
17
  border: 1.5px solid var(--vvp-surface, #e2f0f5);
18
18
  box-shadow: var(
19
19
  --vvp-elevation-accent-rest,
@@ -0,0 +1,52 @@
1
+ .vvp-ui-pill-group {
2
+ display: inline-flex;
3
+ flex-wrap: wrap;
4
+ gap: var(--vvp-spacing-sm, 8px);
5
+ }
6
+
7
+ .vvp-ui-pill-group__option {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ border: 2px solid var(--vvp-surface, #e2f0f5);
12
+ border-radius: var(--vvp-radius-full, 9999px);
13
+ background: var(--vvp-background, #fff);
14
+ color: var(--vvp-text, #111);
15
+ font-family: inherit;
16
+ font-weight: 700;
17
+ cursor: pointer;
18
+ transition:
19
+ background-color 0.15s ease,
20
+ border-color 0.15s ease,
21
+ color 0.15s ease;
22
+ }
23
+
24
+ .vvp-ui-pill-group[data-size="md"] .vvp-ui-pill-group__option {
25
+ padding: var(--vvp-spacing-md, 10px) var(--vvp-spacing-xl, 20px);
26
+ font-size: var(--vvp-font-size-base, 16px);
27
+ }
28
+
29
+ .vvp-ui-pill-group[data-size="sm"] .vvp-ui-pill-group__option {
30
+ padding: var(--vvp-spacing-sm, 8px) var(--vvp-spacing-lg, 16px);
31
+ font-size: var(--vvp-font-size-sm, 14px);
32
+ }
33
+
34
+ .vvp-ui-pill-group__option:hover:not(:disabled) {
35
+ border-color: var(--vvp-primary-muted, #3893c0);
36
+ }
37
+
38
+ .vvp-ui-pill-group__option:focus-visible {
39
+ outline: 2px solid var(--vvp-primary-muted, #3893c0);
40
+ outline-offset: 2px;
41
+ }
42
+
43
+ .vvp-ui-pill-group__option:disabled {
44
+ opacity: 0.6;
45
+ cursor: not-allowed;
46
+ }
47
+
48
+ .vvp-ui-pill-group__option[aria-checked="true"] {
49
+ background: var(--vvp-primary, #1b7194);
50
+ border-color: var(--vvp-primary, #1b7194);
51
+ color: var(--vvp-on-primary, #fff);
52
+ }
@@ -0,0 +1,25 @@
1
+ import "./PillGroup.css";
2
+ export interface PillGroupOption {
3
+ label: string;
4
+ value: string;
5
+ disabled?: boolean;
6
+ }
7
+ export interface PillGroupProps {
8
+ /** Visually hidden unless `label` is also rendered by the consumer via `aria-labelledby` — falls back to this as the accessible name. */
9
+ "aria-label"?: string;
10
+ options: PillGroupOption[];
11
+ value: string;
12
+ onChange: (value: string) => void;
13
+ size?: "sm" | "md";
14
+ disabled?: boolean;
15
+ className?: string;
16
+ }
17
+ /**
18
+ * A row of pill-shaped options for choosing one of a small, fixed set of
19
+ * values — the house alternative to a native `<select>` (see vvp_app's
20
+ * "pills over dropdowns" convention, now ported to web). Implements the
21
+ * ARIA APG radiogroup pattern: one `role="radio"` per pill, roving
22
+ * tabindex, and Left/Right (and Up/Down) arrow keys move both focus and
23
+ * selection, matching native `<select>`/radio-group keyboard behavior.
24
+ */
25
+ export declare function PillGroup({ "aria-label": ariaLabel, options, value, onChange, size, disabled, className, }: PillGroupProps): import("react").JSX.Element;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useId } from "react";
3
+ import "./PillGroup.css";
4
+ /**
5
+ * A row of pill-shaped options for choosing one of a small, fixed set of
6
+ * values — the house alternative to a native `<select>` (see vvp_app's
7
+ * "pills over dropdowns" convention, now ported to web). Implements the
8
+ * ARIA APG radiogroup pattern: one `role="radio"` per pill, roving
9
+ * tabindex, and Left/Right (and Up/Down) arrow keys move both focus and
10
+ * selection, matching native `<select>`/radio-group keyboard behavior.
11
+ */
12
+ export function PillGroup({ "aria-label": ariaLabel, options, value, onChange, size = "md", disabled = false, className, }) {
13
+ const groupId = useId();
14
+ const selectNext = (fromIndex, direction) => {
15
+ const enabled = options
16
+ .map((opt, i) => ({ opt, i }))
17
+ .filter(({ opt }) => !opt.disabled);
18
+ if (enabled.length === 0)
19
+ return;
20
+ const currentPos = enabled.findIndex(({ i }) => i === fromIndex);
21
+ const nextPos = (currentPos + direction + enabled.length) % enabled.length;
22
+ const next = enabled[nextPos];
23
+ if (next)
24
+ onChange(next.opt.value);
25
+ };
26
+ const handleKeyDown = (e, i) => {
27
+ switch (e.key) {
28
+ case "ArrowRight":
29
+ case "ArrowDown":
30
+ e.preventDefault();
31
+ selectNext(i, 1);
32
+ break;
33
+ case "ArrowLeft":
34
+ case "ArrowUp":
35
+ e.preventDefault();
36
+ selectNext(i, -1);
37
+ break;
38
+ }
39
+ };
40
+ const classes = ["vvp-ui-pill-group", className].filter(Boolean).join(" ");
41
+ return (_jsx("div", { role: "radiogroup", "aria-label": ariaLabel, className: classes, "data-size": size, children: options.map((option, i) => {
42
+ const checked = option.value === value;
43
+ return (_jsx("button", { id: `${groupId}-${option.value}`, type: "button", role: "radio", "aria-checked": checked, disabled: disabled || option.disabled, tabIndex: checked ? 0 : -1, className: "vvp-ui-pill-group__option", onClick: () => onChange(option.value), onKeyDown: (e) => handleKeyDown(e, i), children: option.label }, option.value));
44
+ }) }));
45
+ }
@@ -3,4 +3,13 @@
3
3
  top: var(--vvp-spacing-lg, 16px);
4
4
  right: var(--vvp-spacing-lg, 16px);
5
5
  z-index: 50;
6
+ /* Ghost buttons default to a transparent background, which disappears
7
+ against busy page backgrounds (e.g. crowdfunding.volksverpetzer.de).
8
+ The toggle floats over arbitrary host content, so it needs an
9
+ explicit, opaque background rather than inheriting the ghost variant's. */
10
+ background: var(--vvp-background, #fff);
11
+ }
12
+
13
+ .vvp-ui-theme-toggle:hover:not(:disabled) {
14
+ background: var(--vvp-surface, #e2f0f5);
6
15
  }
package/dist/ToolPage.css CHANGED
@@ -122,7 +122,7 @@
122
122
  .vvp-ui-tool-page__icon {
123
123
  display: inline-flex;
124
124
  align-items: center;
125
- color: var(--vvp-primary, #1b7194);
125
+ color: var(--vvp-link-text, #1b7194);
126
126
  }
127
127
 
128
128
  .vvp-ui-tool-page__tags {
package/dist/index.d.ts CHANGED
@@ -8,4 +8,5 @@ export { Alert, type AlertProps, type AlertVariant } from "./Alert";
8
8
  export { Slider, type SliderProps } from "./Slider";
9
9
  export { ThemeToggle } from "./ThemeToggle";
10
10
  export { InputButton, type InputButtonProps } from "./InputButton";
11
+ export { PillGroup, type PillGroupProps, type PillGroupOption, } from "./PillGroup";
11
12
  export { ToolPage, type ToolPageProps, type ToolPageTag } from "./ToolPage";
package/dist/index.js CHANGED
@@ -8,4 +8,5 @@ export { Alert } from "./Alert";
8
8
  export { Slider } from "./Slider";
9
9
  export { ThemeToggle } from "./ThemeToggle";
10
10
  export { InputButton } from "./InputButton";
11
+ export { PillGroup, } from "./PillGroup";
11
12
  export { ToolPage } from "./ToolPage";
package/dist/styles.css CHANGED
@@ -74,6 +74,34 @@
74
74
  color: var(--vvp-pruefpunkt, #442674);
75
75
  }
76
76
 
77
+ /*
78
+ * Dark mode: the light-mode formula above mixes toward literal `white`,
79
+ * which is theme-independent. Dark-mode brand colors are deliberately
80
+ * lightened for legibility on a near-black page, so mixed with `white` at
81
+ * 15% they lose too much contrast against their own text color (as low as
82
+ * ~2:1 for some variant/brand pairings, against a 4.5:1 requirement).
83
+ *
84
+ * Each variant below uses whichever tint direction and percentage clears
85
+ * 4.5:1 for both Volksverpetzer and Mimikama's dark palettes (verified with
86
+ * the WCAG contrast formula) — primary and accent read fine tinted toward
87
+ * white, error and pruefpunkt need black instead.
88
+ */
89
+ .dark .vvp-ui-badge--primary {
90
+ background: color-mix(in srgb, var(--vvp-primary, #1b7194) 5%, white);
91
+ }
92
+
93
+ .dark .vvp-ui-badge--accent {
94
+ background: #fff;
95
+ }
96
+
97
+ .dark .vvp-ui-badge--error {
98
+ background: color-mix(in srgb, var(--vvp-error, #ef5350) 15%, black);
99
+ }
100
+
101
+ .dark .vvp-ui-badge--pruefpunkt {
102
+ background: color-mix(in srgb, var(--vvp-pruefpunkt, #8b5cf6) 10%, black);
103
+ }
104
+
77
105
  .vvp-ui-btn {
78
106
  display: inline-flex;
79
107
  align-items: center;
@@ -132,8 +160,8 @@
132
160
 
133
161
  .vvp-ui-btn--ghost {
134
162
  background: transparent;
135
- color: var(--vvp-primary, #1b7194);
136
- border: 2px solid var(--vvp-primary, #1b7194);
163
+ color: var(--vvp-link-text, #1b7194);
164
+ border: 2px solid var(--vvp-link-text, #1b7194);
137
165
  }
138
166
 
139
167
  .vvp-ui-btn--ghost:hover:not(:disabled) {
@@ -258,7 +286,7 @@
258
286
  .vvp-ui-linkbtn {
259
287
  border-radius: var(--vvp-radius-md, 12px);
260
288
  background: transparent;
261
- color: var(--vvp-primary, #1b7194);
289
+ color: var(--vvp-link-text, #1b7194);
262
290
  border: 1.5px solid var(--vvp-surface, #e2f0f5);
263
291
  box-shadow: var(
264
292
  --vvp-elevation-accent-rest,
@@ -281,6 +309,59 @@
281
309
  );
282
310
  }
283
311
 
312
+ .vvp-ui-pill-group {
313
+ display: inline-flex;
314
+ flex-wrap: wrap;
315
+ gap: var(--vvp-spacing-sm, 8px);
316
+ }
317
+
318
+ .vvp-ui-pill-group__option {
319
+ display: inline-flex;
320
+ align-items: center;
321
+ justify-content: center;
322
+ border: 2px solid var(--vvp-surface, #e2f0f5);
323
+ border-radius: var(--vvp-radius-full, 9999px);
324
+ background: var(--vvp-background, #fff);
325
+ color: var(--vvp-text, #111);
326
+ font-family: inherit;
327
+ font-weight: 700;
328
+ cursor: pointer;
329
+ transition:
330
+ background-color 0.15s ease,
331
+ border-color 0.15s ease,
332
+ color 0.15s ease;
333
+ }
334
+
335
+ .vvp-ui-pill-group[data-size="md"] .vvp-ui-pill-group__option {
336
+ padding: var(--vvp-spacing-md, 10px) var(--vvp-spacing-xl, 20px);
337
+ font-size: var(--vvp-font-size-base, 16px);
338
+ }
339
+
340
+ .vvp-ui-pill-group[data-size="sm"] .vvp-ui-pill-group__option {
341
+ padding: var(--vvp-spacing-sm, 8px) var(--vvp-spacing-lg, 16px);
342
+ font-size: var(--vvp-font-size-sm, 14px);
343
+ }
344
+
345
+ .vvp-ui-pill-group__option:hover:not(:disabled) {
346
+ border-color: var(--vvp-primary-muted, #3893c0);
347
+ }
348
+
349
+ .vvp-ui-pill-group__option:focus-visible {
350
+ outline: 2px solid var(--vvp-primary-muted, #3893c0);
351
+ outline-offset: 2px;
352
+ }
353
+
354
+ .vvp-ui-pill-group__option:disabled {
355
+ opacity: 0.6;
356
+ cursor: not-allowed;
357
+ }
358
+
359
+ .vvp-ui-pill-group__option[aria-checked="true"] {
360
+ background: var(--vvp-primary, #1b7194);
361
+ border-color: var(--vvp-primary, #1b7194);
362
+ color: var(--vvp-on-primary, #fff);
363
+ }
364
+
284
365
  .vvp-ui-progress {
285
366
  width: 100%;
286
367
  text-align: center;
@@ -430,6 +511,15 @@
430
511
  top: var(--vvp-spacing-lg, 16px);
431
512
  right: var(--vvp-spacing-lg, 16px);
432
513
  z-index: 50;
514
+ /* Ghost buttons default to a transparent background, which disappears
515
+ against busy page backgrounds (e.g. crowdfunding.volksverpetzer.de).
516
+ The toggle floats over arbitrary host content, so it needs an
517
+ explicit, opaque background rather than inheriting the ghost variant's. */
518
+ background: var(--vvp-background, #fff);
519
+ }
520
+
521
+ .vvp-ui-theme-toggle:hover:not(:disabled) {
522
+ background: var(--vvp-surface, #e2f0f5);
433
523
  }
434
524
 
435
525
  .vvp-ui-tool-page {
@@ -556,7 +646,7 @@
556
646
  .vvp-ui-tool-page__icon {
557
647
  display: inline-flex;
558
648
  align-items: center;
559
- color: var(--vvp-primary, #1b7194);
649
+ color: var(--vvp-link-text, #1b7194);
560
650
  }
561
651
 
562
652
  .vvp-ui-tool-page__tags {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volksverpetzer/ui-web",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Shared brand-visible React components (Button, Badge, Card, ProgressBar, Input, Alert, Slider, ThemeToggle, InputButton) 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",
@@ -27,8 +27,8 @@
27
27
  "react": ">=19"
28
28
  },
29
29
  "devDependencies": {
30
- "@types/react": "^19.2.18",
31
- "react": "^19.2.3",
30
+ "@types/react": "^19.3.0",
31
+ "react": "^19.3.0",
32
32
  "typescript": "^6.0.3"
33
33
  },
34
34
  "scripts": {