@volksverpetzer/ui-web 0.8.0 → 0.10.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/Button.css CHANGED
@@ -9,6 +9,7 @@
9
9
  cursor: pointer;
10
10
  font-family: inherit;
11
11
  line-height: 1;
12
+ text-decoration: none;
12
13
  transition:
13
14
  background-color 0.15s ease,
14
15
  color 0.15s ease,
@@ -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,37 @@
1
+ /*
2
+ * LinkButton's fixed look: a subtle bordered "pill card" with a soft
3
+ * drop shadow, distinct from Button's ghost variant (see LinkButton.tsx
4
+ * for why). Colors are expressed via the same --vvp-* tokens Button.css
5
+ * uses, so this stays in sync with the shared palette automatically.
6
+ *
7
+ * The shadow comes from @volksverpetzer/design-tokens'
8
+ * --vvp-elevation-accent-* — the one brand-tinted exception to that
9
+ * package's otherwise-neutral elevation scale (see elevation-accent.json).
10
+ * The literal color-mix() here is only a fallback for apps that haven't
11
+ * loaded elevation-accent.css; it must stay in sync with that token.
12
+ */
13
+ .vvp-ui-linkbtn {
14
+ border-radius: var(--vvp-radius-md, 12px);
15
+ background: transparent;
16
+ color: var(--vvp-primary, #1b7194);
17
+ border: 1.5px solid var(--vvp-surface, #e2f0f5);
18
+ box-shadow: var(
19
+ --vvp-elevation-accent-rest,
20
+ 0px 8px 20px color-mix(in srgb, var(--vvp-primary-muted, #3893c0) 6%, transparent)
21
+ );
22
+ transition:
23
+ transform 120ms ease,
24
+ box-shadow 120ms ease,
25
+ border-color 120ms ease,
26
+ background 120ms ease;
27
+ }
28
+
29
+ .vvp-ui-linkbtn:hover {
30
+ transform: translateY(-1px);
31
+ border-color: var(--vvp-primary-muted, #3893c0);
32
+ background: var(--vvp-surface, #e2f0f5);
33
+ box-shadow: var(
34
+ --vvp-elevation-accent-hover,
35
+ 0px 8px 20px color-mix(in srgb, var(--vvp-primary-muted, #3893c0) 18%, transparent)
36
+ );
37
+ }
@@ -0,0 +1,21 @@
1
+ import type { AnchorHTMLAttributes, ReactNode } from "react";
2
+ import type { ButtonSize } from "./Button";
3
+ import "./Button.css";
4
+ import "./LinkButton.css";
5
+ export interface LinkButtonProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
6
+ href: string;
7
+ size?: ButtonSize;
8
+ children: ReactNode;
9
+ }
10
+ /**
11
+ * Anchor-based counterpart to `Button` — for navigation (external links,
12
+ * downloads), where an actual link is the correct element, not a button
13
+ * with an onClick that changes location.
14
+ *
15
+ * Unlike `Button`, this has no `variant` choice: a link is inherently a
16
+ * secondary/tertiary action, never a page's primary CTA, so it always
17
+ * renders the one bordered, low-emphasis look (see LinkButton.css) rather
18
+ * than reusing `Button`'s ghost variant directly — `Button`'s ghost shape
19
+ * is shared with `ThemeToggle`, which needs its own pill/circle geometry.
20
+ */
21
+ export declare const LinkButton: import("react").ForwardRefExoticComponent<LinkButtonProps & import("react").RefAttributes<HTMLAnchorElement>>;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import "./Button.css";
4
+ import "./LinkButton.css";
5
+ /**
6
+ * Anchor-based counterpart to `Button` — for navigation (external links,
7
+ * downloads), where an actual link is the correct element, not a button
8
+ * with an onClick that changes location.
9
+ *
10
+ * Unlike `Button`, this has no `variant` choice: a link is inherently a
11
+ * secondary/tertiary action, never a page's primary CTA, so it always
12
+ * renders the one bordered, low-emphasis look (see LinkButton.css) rather
13
+ * than reusing `Button`'s ghost variant directly — `Button`'s ghost shape
14
+ * is shared with `ThemeToggle`, which needs its own pill/circle geometry.
15
+ */
16
+ export const LinkButton = forwardRef(function LinkButton({ size = "md", className, children, ...rest }, ref) {
17
+ const classes = [
18
+ "vvp-ui-btn",
19
+ "vvp-ui-linkbtn",
20
+ `vvp-ui-btn--${size}`,
21
+ className,
22
+ ]
23
+ .filter(Boolean)
24
+ .join(" ");
25
+ return (_jsx("a", { ref: ref, className: classes, ...rest, children: children }));
26
+ });
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Button, type ButtonProps, type ButtonVariant, type ButtonSize, } from "./Button";
2
+ export { LinkButton, type LinkButtonProps } from "./LinkButton";
2
3
  export { Badge, type BadgeProps, type BadgeVariant } from "./Badge";
3
4
  export { Card, type CardProps } from "./Card";
4
5
  export { ProgressBar, type ProgressBarProps, type ProgressBarMilestone, } from "./ProgressBar";
@@ -6,3 +7,4 @@ export { Input, type InputProps } from "./Input";
6
7
  export { Alert, type AlertProps, type AlertVariant } from "./Alert";
7
8
  export { Slider, type SliderProps } from "./Slider";
8
9
  export { ThemeToggle } from "./ThemeToggle";
10
+ export { InputButton, type InputButtonProps } from "./InputButton";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Button, } from "./Button";
2
+ export { LinkButton } from "./LinkButton";
2
3
  export { Badge } from "./Badge";
3
4
  export { Card } from "./Card";
4
5
  export { ProgressBar, } from "./ProgressBar";
@@ -6,3 +7,4 @@ export { Input } from "./Input";
6
7
  export { Alert } from "./Alert";
7
8
  export { Slider } from "./Slider";
8
9
  export { ThemeToggle } from "./ThemeToggle";
10
+ export { InputButton } from "./InputButton";
package/dist/styles.css CHANGED
@@ -85,6 +85,7 @@
85
85
  cursor: pointer;
86
86
  font-family: inherit;
87
87
  line-height: 1;
88
+ text-decoration: none;
88
89
  transition:
89
90
  background-color 0.15s ease,
90
91
  color 0.15s ease,
@@ -216,6 +217,70 @@
216
217
  outline-color: var(--vvp-error, #c62828);
217
218
  }
218
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
+
246
+ /*
247
+ * LinkButton's fixed look: a subtle bordered "pill card" with a soft
248
+ * drop shadow, distinct from Button's ghost variant (see LinkButton.tsx
249
+ * for why). Colors are expressed via the same --vvp-* tokens Button.css
250
+ * uses, so this stays in sync with the shared palette automatically.
251
+ *
252
+ * The shadow comes from @volksverpetzer/design-tokens'
253
+ * --vvp-elevation-accent-* — the one brand-tinted exception to that
254
+ * package's otherwise-neutral elevation scale (see elevation-accent.json).
255
+ * The literal color-mix() here is only a fallback for apps that haven't
256
+ * loaded elevation-accent.css; it must stay in sync with that token.
257
+ */
258
+ .vvp-ui-linkbtn {
259
+ border-radius: var(--vvp-radius-md, 12px);
260
+ background: transparent;
261
+ color: var(--vvp-primary, #1b7194);
262
+ border: 1.5px solid var(--vvp-surface, #e2f0f5);
263
+ box-shadow: var(
264
+ --vvp-elevation-accent-rest,
265
+ 0px 8px 20px color-mix(in srgb, var(--vvp-primary-muted, #3893c0) 6%, transparent)
266
+ );
267
+ transition:
268
+ transform 120ms ease,
269
+ box-shadow 120ms ease,
270
+ border-color 120ms ease,
271
+ background 120ms ease;
272
+ }
273
+
274
+ .vvp-ui-linkbtn:hover {
275
+ transform: translateY(-1px);
276
+ border-color: var(--vvp-primary-muted, #3893c0);
277
+ background: var(--vvp-surface, #e2f0f5);
278
+ box-shadow: var(
279
+ --vvp-elevation-accent-hover,
280
+ 0px 8px 20px color-mix(in srgb, var(--vvp-primary-muted, #3893c0) 18%, transparent)
281
+ );
282
+ }
283
+
219
284
  .vvp-ui-progress {
220
285
  width: 100%;
221
286
  text-align: center;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@volksverpetzer/ui-web",
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.",
3
+ "version": "0.10.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": {