@volksverpetzer/ui-web 0.9.0 → 0.11.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.
@@ -0,0 +1,25 @@
1
+ .vvp-ui-input-button {
2
+ position: relative;
3
+ display: block;
4
+ width: 100%;
5
+ }
6
+
7
+ /*
8
+ * Compound selectors (rather than relying on file load order) so these
9
+ * reliably win over Input's and Button's own same-specificity base rules
10
+ * regardless of how the package's CSS files get concatenated.
11
+ */
12
+ .vvp-ui-input-button__field.vvp-ui-input {
13
+ padding-right: 7rem;
14
+ }
15
+
16
+ .vvp-ui-input-button__action.vvp-ui-btn {
17
+ position: absolute;
18
+ top: 2px;
19
+ right: 2px;
20
+ bottom: 2px;
21
+ border-top-left-radius: 0;
22
+ border-bottom-left-radius: 0;
23
+ border-top-right-radius: var(--vvp-radius-md, 12px);
24
+ border-bottom-right-radius: var(--vvp-radius-md, 12px);
25
+ }
@@ -0,0 +1,22 @@
1
+ import type { InputHTMLAttributes, ReactNode } from "react";
2
+ import "./InputButton.css";
3
+ export interface InputButtonProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "children"> {
4
+ /** Rendered inside the embedded action button, e.g. "Shorten". */
5
+ buttonLabel: ReactNode;
6
+ /** Disables just the action button — the input stays interactive (e.g. while a submission triggered by it is in flight). */
7
+ buttonDisabled?: boolean;
8
+ /** aria-label for the action button, for icon-only labels. */
9
+ buttonAriaLabel?: string;
10
+ }
11
+ /**
12
+ * A text input with a primary action button embedded in its right edge —
13
+ * the "paste a value and go" pattern (a URL shortener, an email signup
14
+ * field). Composes `Input` and `Button` (`variant="primary"`, `size="sm"`,
15
+ * `type="submit"`) rather than inventing new styling.
16
+ *
17
+ * The button's own rendered width is measured and applied as the input's
18
+ * right padding, so a longer label (e.g. a "Shortening…" loading state)
19
+ * never overlaps typed text. A fixed fallback covers the brief window
20
+ * before that measurement lands (first paint, or SSR).
21
+ */
22
+ export declare const InputButton: import("react").ForwardRefExoticComponent<InputButtonProps & import("react").RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useLayoutEffect, useRef, useState, } from "react";
3
+ import "./InputButton.css";
4
+ import { Button } from "./Button";
5
+ import { Input } from "./Input";
6
+ // useLayoutEffect warns when it runs during SSR (no DOM to measure yet);
7
+ // useEffect is a no-op there and picks up the real measurement on mount.
8
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
9
+ /**
10
+ * A text input with a primary action button embedded in its right edge —
11
+ * the "paste a value and go" pattern (a URL shortener, an email signup
12
+ * field). Composes `Input` and `Button` (`variant="primary"`, `size="sm"`,
13
+ * `type="submit"`) rather than inventing new styling.
14
+ *
15
+ * The button's own rendered width is measured and applied as the input's
16
+ * right padding, so a longer label (e.g. a "Shortening…" loading state)
17
+ * never overlaps typed text. A fixed fallback covers the brief window
18
+ * before that measurement lands (first paint, or SSR).
19
+ */
20
+ export const InputButton = forwardRef(function InputButton({ buttonLabel, buttonDisabled, buttonAriaLabel, style, ...rest }, ref) {
21
+ const buttonRef = useRef(null);
22
+ const [buttonWidth, setButtonWidth] = useState(0);
23
+ useIsomorphicLayoutEffect(() => {
24
+ const button = buttonRef.current;
25
+ if (!button)
26
+ return;
27
+ const observer = new ResizeObserver(([entry]) => {
28
+ setButtonWidth(entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width);
29
+ });
30
+ observer.observe(button);
31
+ return () => observer.disconnect();
32
+ }, []);
33
+ return (_jsxs("div", { className: "vvp-ui-input-button", children: [_jsx(Input, { ref: ref, className: "vvp-ui-input-button__field", style: {
34
+ ...style,
35
+ paddingRight: buttonWidth ? buttonWidth + 8 : undefined,
36
+ }, ...rest }), _jsx(Button, { ref: buttonRef, type: "submit", size: "sm", disabled: buttonDisabled, "aria-label": buttonAriaLabel, className: "vvp-ui-input-button__action", children: buttonLabel })] }));
37
+ });
@@ -0,0 +1,113 @@
1
+ .vvp-ui-tool-page {
2
+ position: relative;
3
+ min-height: 100vh;
4
+ background: var(--vvp-background, #fff);
5
+ overflow: hidden;
6
+ }
7
+
8
+ .vvp-ui-tool-page__background {
9
+ position: absolute;
10
+ top: 0;
11
+ left: 0;
12
+ right: 0;
13
+ height: 220px;
14
+ overflow: hidden;
15
+ opacity: 0.1;
16
+ pointer-events: none;
17
+ z-index: 0;
18
+ mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
19
+ -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
20
+ }
21
+
22
+ .vvp-ui-tool-page__background svg {
23
+ width: 100%;
24
+ height: 100%;
25
+ display: block;
26
+ }
27
+
28
+ .vvp-ui-tool-page__topbar {
29
+ position: fixed;
30
+ top: var(--vvp-spacing-lg, 16px);
31
+ right: var(--vvp-spacing-lg, 16px);
32
+ z-index: 50;
33
+ display: flex;
34
+ align-items: center;
35
+ gap: var(--vvp-spacing-sm, 8px);
36
+ }
37
+
38
+ /* ThemeToggle self-positions fixed top-right; inside our own topbar it
39
+ should lay out with the GitHub link instead, so it's un-fixed here. */
40
+ .vvp-ui-tool-page__topbar .vvp-ui-theme-toggle {
41
+ position: static;
42
+ top: auto;
43
+ right: auto;
44
+ }
45
+
46
+ .vvp-ui-tool-page__icon-link {
47
+ display: inline-flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ }
51
+
52
+ .vvp-ui-tool-page__inner {
53
+ position: relative;
54
+ z-index: 1;
55
+ max-width: 960px;
56
+ margin: 0 auto;
57
+ padding: var(--vvp-spacing-huge, 40px) var(--vvp-spacing-lg, 16px);
58
+ display: flex;
59
+ flex-direction: column;
60
+ gap: var(--vvp-spacing-xl, 20px);
61
+ }
62
+
63
+ /* Matches ai.volksverpetzer.de's pre-results state: the header/content/
64
+ tags/help block sits in the vertical middle of the viewport rather than
65
+ pinned to the top. */
66
+ .vvp-ui-tool-page__inner--centered {
67
+ min-height: 70vh;
68
+ justify-content: center;
69
+ }
70
+
71
+ .vvp-ui-tool-page__header {
72
+ display: flex;
73
+ flex-direction: column;
74
+ align-items: center;
75
+ gap: var(--vvp-spacing-sm, 8px);
76
+ text-align: center;
77
+ }
78
+
79
+ .vvp-ui-tool-page__title {
80
+ margin: 0;
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ gap: var(--vvp-spacing-sm, 8px);
85
+ font-size: var(--vvp-font-size-xxxl, 30px);
86
+ font-weight: 700;
87
+ line-height: 1.2;
88
+ color: var(--vvp-text, #111);
89
+ }
90
+
91
+ .vvp-ui-tool-page__icon {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ color: var(--vvp-primary, #1b7194);
95
+ }
96
+
97
+ .vvp-ui-tool-page__tags {
98
+ display: flex;
99
+ flex-wrap: wrap;
100
+ justify-content: center;
101
+ gap: var(--vvp-spacing-xs, 4px);
102
+ }
103
+
104
+ .vvp-ui-tool-page__content {
105
+ width: 100%;
106
+ }
107
+
108
+ .vvp-ui-tool-page__help {
109
+ margin: 0;
110
+ text-align: center;
111
+ font-size: var(--vvp-font-size-sm, 14px);
112
+ color: var(--vvp-text-muted, #666);
113
+ }
@@ -0,0 +1,39 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import type { BadgeVariant } from "./Badge";
3
+ import "./ToolPage.css";
4
+ export interface ToolPageTag {
5
+ label: ReactNode;
6
+ variant?: BadgeVariant;
7
+ }
8
+ export interface ToolPageProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
9
+ icon?: ReactNode;
10
+ title: ReactNode;
11
+ /** Small badges rendered below the title, e.g. feature/category tags. */
12
+ tags?: ToolPageTag[];
13
+ /** The page's actual content — a search input, an audio list, a form. */
14
+ children: ReactNode;
15
+ /** Muted note rendered below `children` — usage hints, empty-state help. */
16
+ helpText?: ReactNode;
17
+ /** Renders a GitHub icon link in the top-right corner, next to the theme toggle. */
18
+ githubUrl?: string;
19
+ /** @default true */
20
+ background?: boolean;
21
+ /**
22
+ * Vertically centers the icon/title/tags/content/helpText block in the
23
+ * middle of the viewport — the pre-results look on ai.volksverpetzer.de.
24
+ * Turn off once there's enough content that top-aligning it reads better
25
+ * (e.g. a long list) — this is a static flag, not a scroll-based one, so
26
+ * the caller decides when that switch happens.
27
+ * @default false
28
+ */
29
+ centered?: boolean;
30
+ }
31
+ /**
32
+ * Full-page shell for a single-purpose tool page — the pattern shared by
33
+ * ai.volksverpetzer.de (icon + centered title, a search input, tag pills)
34
+ * and this app's audio converter list. Centered icon/title/tags header,
35
+ * full-width content slot, optional help text below it, a fixed top-right
36
+ * corner with `ThemeToggle` and an optional GitHub link, and (by default)
37
+ * a decorative jagged brand-blue shape along the top edge.
38
+ */
39
+ export declare function ToolPage({ icon, title, tags, children, helpText, githubUrl, background, centered, className, ...rest }: ToolPageProps): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Badge } from "./Badge";
3
+ import { ThemeToggle } from "./ThemeToggle";
4
+ import "./ToolPage.css";
5
+ const GithubIcon = () => (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.57.1.79-.25.79-.55 0-.27-.01-1.16-.02-2.11-3.2.7-3.87-1.36-3.87-1.36-.53-1.33-1.29-1.69-1.29-1.69-1.05-.72.08-.7.08-.7 1.16.08 1.78 1.19 1.78 1.19 1.03 1.77 2.71 1.26 3.37.96.1-.75.4-1.26.73-1.55-2.55-.29-5.23-1.28-5.23-5.69 0-1.26.45-2.29 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11 11 0 0 1 5.79 0c2.2-1.49 3.17-1.18 3.17-1.18.64 1.59.24 2.76.12 3.05.74.8 1.18 1.83 1.18 3.09 0 4.42-2.69 5.39-5.25 5.68.41.36.78 1.07.78 2.15 0 1.55-.01 2.81-.01 3.19 0 .3.21.66.79.55A10.52 10.52 0 0 0 23.5 12c0-6.35-5.15-11.5-11.5-11.5Z" }) }));
6
+ /**
7
+ * Full-page shell for a single-purpose tool page — the pattern shared by
8
+ * ai.volksverpetzer.de (icon + centered title, a search input, tag pills)
9
+ * and this app's audio converter list. Centered icon/title/tags header,
10
+ * full-width content slot, optional help text below it, a fixed top-right
11
+ * corner with `ThemeToggle` and an optional GitHub link, and (by default)
12
+ * a decorative jagged brand-blue shape along the top edge.
13
+ */
14
+ export function ToolPage({ icon, title, tags, children, helpText, githubUrl, background = true, centered = false, className, ...rest }) {
15
+ const classes = ["vvp-ui-tool-page", className].filter(Boolean).join(" ");
16
+ const innerClasses = [
17
+ "vvp-ui-tool-page__inner",
18
+ centered && "vvp-ui-tool-page__inner--centered",
19
+ ]
20
+ .filter(Boolean)
21
+ .join(" ");
22
+ return (_jsxs("div", { className: classes, ...rest, children: [background ? (_jsx("div", { className: "vvp-ui-tool-page__background", "aria-hidden": "true", children: _jsx("svg", { viewBox: "0 0 1440 320", preserveAspectRatio: "none", fill: "var(--vvp-primary, #1b7194)", children: _jsx("path", { d: "M0,0 L0,140 L75,70 L155,160 L235,50 L310,170 L390,80 L465,175 L545,60 L620,150 L700,40 L775,165 L855,75 L935,175 L1010,55 L1090,145 L1165,65 L1245,165 L1320,85 L1440,130 L1440,0 Z" }) }) })) : null, _jsxs("div", { className: "vvp-ui-tool-page__topbar", children: [githubUrl ? (_jsx("a", { href: githubUrl, target: "_blank", rel: "noopener noreferrer", "aria-label": "GitHub", className: "vvp-ui-btn vvp-ui-btn--ghost vvp-ui-btn--sm vvp-ui-tool-page__icon-link", children: _jsx(GithubIcon, {}) })) : null, _jsx(ThemeToggle, {})] }), _jsxs("div", { className: innerClasses, children: [_jsxs("div", { className: "vvp-ui-tool-page__header", children: [_jsxs("h1", { className: "vvp-ui-tool-page__title", children: [icon ? (_jsx("span", { className: "vvp-ui-tool-page__icon", "aria-hidden": "true", children: icon })) : null, title] }), tags && tags.length > 0 ? (_jsx("div", { className: "vvp-ui-tool-page__tags", children: tags.map((tag, index) => (_jsx(Badge, { variant: tag.variant ?? "neutral", size: "sm", children: tag.label }, index))) })) : null] }), _jsx("div", { className: "vvp-ui-tool-page__content", children: children }), helpText ? _jsx("p", { className: "vvp-ui-tool-page__help", children: helpText }) : null] })] }));
23
+ }
package/dist/index.d.ts CHANGED
@@ -7,3 +7,5 @@ export { Input, type InputProps } from "./Input";
7
7
  export { Alert, type AlertProps, type AlertVariant } from "./Alert";
8
8
  export { Slider, type SliderProps } from "./Slider";
9
9
  export { ThemeToggle } from "./ThemeToggle";
10
+ export { InputButton, type InputButtonProps } from "./InputButton";
11
+ export { ToolPage, type ToolPageProps, type ToolPageTag } from "./ToolPage";
package/dist/index.js CHANGED
@@ -7,3 +7,5 @@ export { Input } from "./Input";
7
7
  export { Alert } from "./Alert";
8
8
  export { Slider } from "./Slider";
9
9
  export { ThemeToggle } from "./ThemeToggle";
10
+ export { InputButton } from "./InputButton";
11
+ export { ToolPage } from "./ToolPage";
package/dist/styles.css CHANGED
@@ -217,6 +217,32 @@
217
217
  outline-color: var(--vvp-error, #c62828);
218
218
  }
219
219
 
220
+ .vvp-ui-input-button {
221
+ position: relative;
222
+ display: block;
223
+ width: 100%;
224
+ }
225
+
226
+ /*
227
+ * Compound selectors (rather than relying on file load order) so these
228
+ * reliably win over Input's and Button's own same-specificity base rules
229
+ * regardless of how the package's CSS files get concatenated.
230
+ */
231
+ .vvp-ui-input-button__field.vvp-ui-input {
232
+ padding-right: 7rem;
233
+ }
234
+
235
+ .vvp-ui-input-button__action.vvp-ui-btn {
236
+ position: absolute;
237
+ top: 2px;
238
+ right: 2px;
239
+ bottom: 2px;
240
+ border-top-left-radius: 0;
241
+ border-bottom-left-radius: 0;
242
+ border-top-right-radius: var(--vvp-radius-md, 12px);
243
+ border-bottom-right-radius: var(--vvp-radius-md, 12px);
244
+ }
245
+
220
246
  /*
221
247
  * LinkButton's fixed look: a subtle bordered "pill card" with a soft
222
248
  * drop shadow, distinct from Button's ghost variant (see LinkButton.tsx
@@ -406,3 +432,117 @@
406
432
  z-index: 50;
407
433
  }
408
434
 
435
+ .vvp-ui-tool-page {
436
+ position: relative;
437
+ min-height: 100vh;
438
+ background: var(--vvp-background, #fff);
439
+ overflow: hidden;
440
+ }
441
+
442
+ .vvp-ui-tool-page__background {
443
+ position: absolute;
444
+ top: 0;
445
+ left: 0;
446
+ right: 0;
447
+ height: 220px;
448
+ overflow: hidden;
449
+ opacity: 0.1;
450
+ pointer-events: none;
451
+ z-index: 0;
452
+ mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
453
+ -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
454
+ }
455
+
456
+ .vvp-ui-tool-page__background svg {
457
+ width: 100%;
458
+ height: 100%;
459
+ display: block;
460
+ }
461
+
462
+ .vvp-ui-tool-page__topbar {
463
+ position: fixed;
464
+ top: var(--vvp-spacing-lg, 16px);
465
+ right: var(--vvp-spacing-lg, 16px);
466
+ z-index: 50;
467
+ display: flex;
468
+ align-items: center;
469
+ gap: var(--vvp-spacing-sm, 8px);
470
+ }
471
+
472
+ /* ThemeToggle self-positions fixed top-right; inside our own topbar it
473
+ should lay out with the GitHub link instead, so it's un-fixed here. */
474
+ .vvp-ui-tool-page__topbar .vvp-ui-theme-toggle {
475
+ position: static;
476
+ top: auto;
477
+ right: auto;
478
+ }
479
+
480
+ .vvp-ui-tool-page__icon-link {
481
+ display: inline-flex;
482
+ align-items: center;
483
+ justify-content: center;
484
+ }
485
+
486
+ .vvp-ui-tool-page__inner {
487
+ position: relative;
488
+ z-index: 1;
489
+ max-width: 960px;
490
+ margin: 0 auto;
491
+ padding: var(--vvp-spacing-huge, 40px) var(--vvp-spacing-lg, 16px);
492
+ display: flex;
493
+ flex-direction: column;
494
+ gap: var(--vvp-spacing-xl, 20px);
495
+ }
496
+
497
+ /* Matches ai.volksverpetzer.de's pre-results state: the header/content/
498
+ tags/help block sits in the vertical middle of the viewport rather than
499
+ pinned to the top. */
500
+ .vvp-ui-tool-page__inner--centered {
501
+ min-height: 70vh;
502
+ justify-content: center;
503
+ }
504
+
505
+ .vvp-ui-tool-page__header {
506
+ display: flex;
507
+ flex-direction: column;
508
+ align-items: center;
509
+ gap: var(--vvp-spacing-sm, 8px);
510
+ text-align: center;
511
+ }
512
+
513
+ .vvp-ui-tool-page__title {
514
+ margin: 0;
515
+ display: flex;
516
+ align-items: center;
517
+ justify-content: center;
518
+ gap: var(--vvp-spacing-sm, 8px);
519
+ font-size: var(--vvp-font-size-xxxl, 30px);
520
+ font-weight: 700;
521
+ line-height: 1.2;
522
+ color: var(--vvp-text, #111);
523
+ }
524
+
525
+ .vvp-ui-tool-page__icon {
526
+ display: inline-flex;
527
+ align-items: center;
528
+ color: var(--vvp-primary, #1b7194);
529
+ }
530
+
531
+ .vvp-ui-tool-page__tags {
532
+ display: flex;
533
+ flex-wrap: wrap;
534
+ justify-content: center;
535
+ gap: var(--vvp-spacing-xs, 4px);
536
+ }
537
+
538
+ .vvp-ui-tool-page__content {
539
+ width: 100%;
540
+ }
541
+
542
+ .vvp-ui-tool-page__help {
543
+ margin: 0;
544
+ text-align: center;
545
+ font-size: var(--vvp-font-size-sm, 14px);
546
+ color: var(--vvp-text-muted, #666);
547
+ }
548
+
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@volksverpetzer/ui-web",
3
- "version": "0.9.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.",
3
+ "version": "0.11.0",
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",
7
7
  "repository": {