@robr0/design-system 0.12.0 → 0.14.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.
Files changed (56) hide show
  1. package/README.md +9 -3
  2. package/components/AvatarGroup/AvatarGroup.css +61 -0
  3. package/components/AvatarGroup/AvatarGroup.d.ts +32 -0
  4. package/components/AvatarGroup/AvatarGroup.js +31 -0
  5. package/components/Banner/Banner.css +110 -0
  6. package/components/Banner/Banner.d.ts +36 -0
  7. package/components/Banner/Banner.js +59 -0
  8. package/components/ContributionGraph/ContributionGraph.css +7 -1
  9. package/components/ContributionGraph/ContributionGraph.d.ts +9 -1
  10. package/components/ContributionGraph/ContributionGraph.js +59 -43
  11. package/components/EmptyState/EmptyState.css +5 -0
  12. package/components/FilterBar/FilterBar.css +202 -0
  13. package/components/FilterBar/FilterBar.d.ts +55 -0
  14. package/components/FilterBar/FilterBar.js +249 -0
  15. package/components/FunnelChart/FunnelChart.css +3 -2
  16. package/components/FunnelChart/FunnelChart.d.ts +12 -4
  17. package/components/FunnelChart/FunnelChart.js +48 -28
  18. package/components/Gauge/Gauge.css +110 -0
  19. package/components/Gauge/Gauge.d.ts +64 -0
  20. package/components/Gauge/Gauge.js +111 -0
  21. package/components/HoverCard/HoverCard.css +97 -0
  22. package/components/HoverCard/HoverCard.d.ts +29 -0
  23. package/components/HoverCard/HoverCard.js +83 -0
  24. package/components/ImageCompare/ImageCompare.css +112 -0
  25. package/components/ImageCompare/ImageCompare.d.ts +40 -0
  26. package/components/ImageCompare/ImageCompare.js +134 -0
  27. package/components/LinkList/LinkList.d.ts +3 -1
  28. package/components/LinkList/LinkList.js +4 -3
  29. package/components/Meter/Meter.css +89 -0
  30. package/components/Meter/Meter.d.ts +36 -0
  31. package/components/Meter/Meter.js +48 -0
  32. package/components/NotificationCenter/NotificationCenter.css +11 -3
  33. package/components/Rating/Rating.css +74 -0
  34. package/components/Rating/Rating.d.ts +41 -0
  35. package/components/Rating/Rating.js +124 -0
  36. package/components/Sparkline/Sparkline.d.ts +5 -3
  37. package/components/Sparkline/Sparkline.js +30 -1
  38. package/components/SplitButton/SplitButton.css +93 -0
  39. package/components/SplitButton/SplitButton.d.ts +43 -0
  40. package/components/SplitButton/SplitButton.js +66 -0
  41. package/components/SplitPane/SplitPane.css +106 -0
  42. package/components/SplitPane/SplitPane.d.ts +36 -0
  43. package/components/SplitPane/SplitPane.js +130 -0
  44. package/components/StreamingText/StreamingText.css +40 -0
  45. package/components/StreamingText/StreamingText.d.ts +62 -0
  46. package/components/StreamingText/StreamingText.js +49 -0
  47. package/components/StreamingText/useStreamReveal.d.ts +70 -0
  48. package/components/StreamingText/useStreamReveal.js +121 -0
  49. package/components/registry.json +88 -0
  50. package/components/registry.json.d.ts +88 -0
  51. package/components/registry.json.js +1 -1
  52. package/index.d.ts +11 -0
  53. package/index.js +25 -0
  54. package/package.json +5 -1
  55. package/tokens/motion.d.ts +12 -3
  56. package/tokens/motion.js +7 -1
@@ -0,0 +1,110 @@
1
+ /* ============================================
2
+ GAUGE COMPONENT
3
+ Radial dial for a single bounded reading.
4
+ Card chrome, header, and padding come from
5
+ the shared chart styles (Chart.css) — this
6
+ file owns the dial alone. Colour flows
7
+ through `color`: the value arc draws with
8
+ currentColor, so a tone is one custom
9
+ property swap.
10
+ ============================================ */
11
+
12
+ /* Base — the dial centres in the shared chart body */
13
+
14
+ .ds-gauge__body {
15
+ display: flex;
16
+ justify-content: center;
17
+ }
18
+
19
+ /* Unlike the fluid recharts bodies, the dial has intrinsic size — a bare
20
+ gauge shrink-wraps it so it sits inline in a Stat row or tile. */
21
+ .ds-gauge.ds-chart--bare {
22
+ width: auto;
23
+ display: inline-flex;
24
+ }
25
+
26
+ .ds-gauge__dial {
27
+ position: relative;
28
+ display: inline-flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ flex-shrink: 0;
32
+ }
33
+
34
+ .ds-gauge__svg {
35
+ display: block;
36
+ }
37
+
38
+ /* Parts */
39
+
40
+ .ds-gauge__track {
41
+ fill: none;
42
+ stroke: var(--color-bg-container-secondary);
43
+ stroke-linecap: round;
44
+ }
45
+
46
+ .ds-gauge__arc {
47
+ fill: none;
48
+ stroke: currentColor;
49
+ stroke-linecap: round;
50
+ transition: stroke-dasharray var(--motion-duration-slow) var(--motion-ease-emphasized);
51
+ }
52
+
53
+ .ds-gauge__center {
54
+ position: absolute;
55
+ inset: 0;
56
+ display: flex;
57
+ flex-direction: column;
58
+ align-items: center;
59
+ justify-content: center;
60
+ gap: var(--gap-xxs);
61
+ text-align: center;
62
+ pointer-events: none;
63
+ }
64
+
65
+ /* The centre pair mirrors the chart family's radial centre label:
66
+ a light sub-display numeral over a caption. */
67
+
68
+ .ds-gauge__value {
69
+ font-family: var(--font-sub-display-family);
70
+ font-size: var(--font-sub-display-size);
71
+ font-weight: var(--font-sub-display-weight);
72
+ line-height: var(--font-sub-display-line-height);
73
+ letter-spacing: var(--font-sub-display-letter-spacing);
74
+ color: var(--color-text-primary);
75
+ }
76
+
77
+ .ds-gauge__label {
78
+ font-family: var(--font-caption-family);
79
+ font-size: var(--font-caption-size);
80
+ font-weight: var(--font-caption-weight);
81
+ line-height: var(--font-caption-line-height);
82
+ letter-spacing: var(--font-caption-letter-spacing);
83
+ color: var(--color-text-secondary);
84
+ }
85
+
86
+ /* Tones */
87
+
88
+ .ds-gauge--accent {
89
+ color: var(--color-chart-series-1);
90
+ }
91
+
92
+ /* The status hues come from the border tokens, not the text tokens: the
93
+ border set is stable across themes, while the dark-mode text tints are
94
+ near-white and would wash three tones into one. */
95
+
96
+ .ds-gauge--positive {
97
+ color: var(--color-status-positive-border);
98
+ }
99
+
100
+ .ds-gauge--warning {
101
+ color: var(--color-status-warning-border);
102
+ }
103
+
104
+ .ds-gauge--error {
105
+ color: var(--color-status-error-border);
106
+ }
107
+
108
+ .ds-gauge--neutral {
109
+ color: var(--color-text-secondary);
110
+ }
@@ -0,0 +1,64 @@
1
+ import { default as React } from 'react';
2
+ /** One colour switch point: at or above `value`, the dial takes `tone`. */
3
+ export interface GaugeThreshold {
4
+ /** Reading at which this tone takes over. */
5
+ value: number;
6
+ /** Tone applied from this reading upward. */
7
+ tone: 'accent' | 'positive' | 'warning' | 'error' | 'neutral';
8
+ }
9
+ /** Props owned by Gauge itself — everything else falls through to the root div. */
10
+ type GaugeOwnProps = {
11
+ /** Current reading. Clamped into the `min`–`max` range for drawing. */
12
+ value: number;
13
+ /** Lower bound of the dial. */
14
+ min?: number;
15
+ /** Upper bound of the dial. */
16
+ max?: number;
17
+ /**
18
+ * Colour role for the value arc. `accent` (the default) follows the chart
19
+ * palette's lead colour. Ignored while a threshold matches — thresholds
20
+ * exist so the dial recolours itself as the reading crosses them.
21
+ */
22
+ tone?: 'accent' | 'positive' | 'warning' | 'error' | 'neutral';
23
+ /**
24
+ * Colour switch points, e.g. warning at 70 and error at 90. The highest
25
+ * threshold at or below the current reading wins; below them all, the
26
+ * `tone` prop applies.
27
+ */
28
+ thresholds?: GaugeThreshold[];
29
+ /** Shows the reading in the centre of the dial. */
30
+ showValue?: boolean;
31
+ /** Formats the centre reading — for units, precision, or locale. */
32
+ formatValue?: (value: number) => string;
33
+ /**
34
+ * What the reading measures, shown as a caption under it and used as the
35
+ * accessible name, e.g. "CPU usage".
36
+ */
37
+ label?: string;
38
+ /** Chart title, in the shared chart header. */
39
+ title?: string;
40
+ /** Description text below the title. */
41
+ subtitle?: string;
42
+ /** Strip the card chrome (border, padding, fill) when the chart sits inside another panel that supplies the surface */
43
+ bare?: boolean;
44
+ /** Rendered dial diameter in pixels. The arc geometry scales with it. */
45
+ size?: number;
46
+ /** Arc thickness in pixels. */
47
+ strokeWidth?: number;
48
+ /** Additional CSS classes */
49
+ className?: string;
50
+ };
51
+ export interface GaugeProps extends GaugeOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof GaugeOwnProps | 'children'> {
52
+ }
53
+ /**
54
+ * Gauge — a radial dial for a single bounded reading: capacity, usage, a
55
+ * score against a target. Pure SVG computed from props — no charting library,
56
+ * no hooks — so it renders from a Server Component. It wears the chart
57
+ * family's card chrome (title, subtitle, padding) and takes `bare` to drop
58
+ * it inside a Panel, like every other chart. Thresholds recolour the dial
59
+ * through the status roles as the reading crosses them, and the arc animates
60
+ * between readings via a CSS transition. The dial is announced as a `meter`
61
+ * with the label as its accessible name.
62
+ */
63
+ export declare const Gauge: React.ForwardRefExoticComponent<GaugeProps & React.RefAttributes<HTMLDivElement>>;
64
+ export {};
@@ -0,0 +1,111 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import "../Chart/Chart.css";
4
+ import "./Gauge.css";
5
+ const SWEEP_UNITS = 75;
6
+ const Gauge = React.forwardRef(
7
+ ({
8
+ value,
9
+ min = 0,
10
+ max = 100,
11
+ tone = "accent",
12
+ thresholds,
13
+ showValue = true,
14
+ formatValue,
15
+ label,
16
+ title,
17
+ subtitle,
18
+ bare = false,
19
+ size = 120,
20
+ strokeWidth = 12,
21
+ className = "",
22
+ ...rest
23
+ }, ref) => {
24
+ const baseClass = "ds-gauge";
25
+ const chartClass = "ds-chart";
26
+ const range = max - min;
27
+ const fraction = range > 0 ? Math.min(Math.max((value - min) / range, 0), 1) : 0;
28
+ const activeThreshold = thresholds?.filter((t) => value >= t.value).sort((a, b) => b.value - a.value)[0];
29
+ const resolvedTone = activeThreshold?.tone ?? tone;
30
+ const classes = [
31
+ chartClass,
32
+ bare && `${chartClass}--bare`,
33
+ baseClass,
34
+ `${baseClass}--${resolvedTone}`,
35
+ className
36
+ ].filter(Boolean).join(" ");
37
+ const radius = (size - strokeWidth) / 2;
38
+ const center = size / 2;
39
+ const valueUnits = Math.round(SWEEP_UNITS * fraction * 100) / 100;
40
+ const displayValue = formatValue ? formatValue(value) : String(Math.round(value));
41
+ const { "aria-label": restAriaLabel, ...restProps } = rest;
42
+ return /* @__PURE__ */ jsxs("div", { ...restProps, ref, className: classes, children: [
43
+ (title || subtitle) && /* @__PURE__ */ jsx("div", { className: `${chartClass}__header`, children: /* @__PURE__ */ jsxs("div", { className: `${chartClass}__header-text`, children: [
44
+ title && /* @__PURE__ */ jsx("h3", { className: `${chartClass}__title`, children: title }),
45
+ subtitle && /* @__PURE__ */ jsx("p", { className: `${chartClass}__subtitle`, children: subtitle })
46
+ ] }) }),
47
+ /* @__PURE__ */ jsx("div", { className: `${chartClass}__body ${baseClass}__body`, children: /* @__PURE__ */ jsxs(
48
+ "div",
49
+ {
50
+ className: `${baseClass}__dial`,
51
+ style: { width: size, height: size },
52
+ role: "meter",
53
+ "aria-valuenow": value,
54
+ "aria-valuemin": min,
55
+ "aria-valuemax": max,
56
+ "aria-valuetext": displayValue,
57
+ "aria-label": label ?? restAriaLabel,
58
+ children: [
59
+ /* @__PURE__ */ jsxs(
60
+ "svg",
61
+ {
62
+ className: `${baseClass}__svg`,
63
+ viewBox: `0 0 ${size} ${size}`,
64
+ width: size,
65
+ height: size,
66
+ xmlns: "http://www.w3.org/2000/svg",
67
+ "aria-hidden": "true",
68
+ children: [
69
+ /* @__PURE__ */ jsx(
70
+ "circle",
71
+ {
72
+ className: `${baseClass}__track`,
73
+ cx: center,
74
+ cy: center,
75
+ r: radius,
76
+ pathLength: 100,
77
+ strokeWidth,
78
+ strokeDasharray: `${SWEEP_UNITS} ${100 - SWEEP_UNITS}`,
79
+ transform: `rotate(135 ${center} ${center})`
80
+ }
81
+ ),
82
+ valueUnits > 0 && /* @__PURE__ */ jsx(
83
+ "circle",
84
+ {
85
+ className: `${baseClass}__arc`,
86
+ cx: center,
87
+ cy: center,
88
+ r: radius,
89
+ pathLength: 100,
90
+ strokeWidth,
91
+ strokeDasharray: `${valueUnits} ${100 - valueUnits}`,
92
+ transform: `rotate(135 ${center} ${center})`
93
+ }
94
+ )
95
+ ]
96
+ }
97
+ ),
98
+ (showValue || label) && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__center`, children: [
99
+ showValue && /* @__PURE__ */ jsx("span", { className: `${baseClass}__value`, children: displayValue }),
100
+ label && /* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: label })
101
+ ] })
102
+ ]
103
+ }
104
+ ) })
105
+ ] });
106
+ }
107
+ );
108
+ Gauge.displayName = "Gauge";
109
+ export {
110
+ Gauge
111
+ };
@@ -0,0 +1,97 @@
1
+ /* ============================================
2
+ HOVER CARD COMPONENT
3
+ Rich preview panel on hover / focus
4
+ ============================================ */
5
+
6
+ .ds-hover-card {
7
+ position: relative;
8
+ display: inline-flex;
9
+ }
10
+
11
+ /* ============================================
12
+ PANEL
13
+ Dresses like the other floating panels:
14
+ page-primary fill, hairline, radius-sm,
15
+ floating shadow (see Popover).
16
+ ============================================ */
17
+
18
+ .ds-hover-card__panel {
19
+ /* Rendered as a <span> so the card survives inside a <p>; absolute
20
+ positioning already computes display to block, stated here anyway. */
21
+ display: block;
22
+ position: absolute;
23
+ z-index: 50;
24
+ min-width: 240px;
25
+ max-width: 320px;
26
+ padding: var(--padding-md);
27
+ border-radius: var(--radius-sm);
28
+ background-color: var(--color-bg-page-primary);
29
+ border: var(--border-xs) solid var(--color-bg-container-border);
30
+ box-shadow: var(--shadow-floating);
31
+ color: var(--color-text-primary);
32
+ font-family: var(--font-paragraph-sm-family);
33
+ font-size: var(--font-paragraph-sm-size);
34
+ font-weight: var(--font-paragraph-sm-weight);
35
+ line-height: var(--font-paragraph-sm-line-height);
36
+ /* visibility keeps the hidden panel out of the accessibility tree and
37
+ tab order; opacity + transform carry the entrance. */
38
+ visibility: hidden;
39
+ opacity: 0;
40
+ transition:
41
+ opacity var(--motion-duration-fast) var(--motion-ease-standard),
42
+ transform var(--motion-duration-fast) var(--motion-ease-standard),
43
+ visibility var(--motion-duration-fast) var(--motion-ease-standard);
44
+ }
45
+
46
+ .ds-hover-card__panel--visible {
47
+ visibility: visible;
48
+ opacity: 1;
49
+ }
50
+
51
+ /* ============================================
52
+ POSITIONS
53
+ ============================================ */
54
+
55
+ .ds-hover-card__panel--top {
56
+ bottom: 100%;
57
+ left: 50%;
58
+ transform: translateX(-50%) translateY(4px);
59
+ margin-bottom: var(--gap-xs);
60
+ }
61
+
62
+ .ds-hover-card__panel--top.ds-hover-card__panel--visible {
63
+ transform: translateX(-50%) translateY(0);
64
+ }
65
+
66
+ .ds-hover-card__panel--bottom {
67
+ top: 100%;
68
+ left: 50%;
69
+ transform: translateX(-50%) translateY(-4px);
70
+ margin-top: var(--gap-xs);
71
+ }
72
+
73
+ .ds-hover-card__panel--bottom.ds-hover-card__panel--visible {
74
+ transform: translateX(-50%) translateY(0);
75
+ }
76
+
77
+ .ds-hover-card__panel--left {
78
+ right: 100%;
79
+ top: 50%;
80
+ transform: translateY(-50%) translateX(4px);
81
+ margin-right: var(--gap-xs);
82
+ }
83
+
84
+ .ds-hover-card__panel--left.ds-hover-card__panel--visible {
85
+ transform: translateY(-50%) translateX(0);
86
+ }
87
+
88
+ .ds-hover-card__panel--right {
89
+ left: 100%;
90
+ top: 50%;
91
+ transform: translateY(-50%) translateX(-4px);
92
+ margin-left: var(--gap-xs);
93
+ }
94
+
95
+ .ds-hover-card__panel--right.ds-hover-card__panel--visible {
96
+ transform: translateY(-50%) translateX(0);
97
+ }
@@ -0,0 +1,29 @@
1
+ import { default as React } from 'react';
2
+ type HoverCardOwnProps = {
3
+ /** Trigger element */
4
+ children?: React.ReactNode;
5
+ /** Panel content — arbitrary elements, unlike Tooltip's plain text. In running text inside a `<p>`, keep it phrasing-level (spans): a block element would end the paragraph mid-parse. */
6
+ content: React.ReactNode;
7
+ /** Preferred position */
8
+ position?: 'top' | 'bottom' | 'left' | 'right';
9
+ /** Delay before showing (in ms) */
10
+ showDelay?: number;
11
+ /** Delay before hiding (in ms) */
12
+ hideDelay?: number;
13
+ /** Additional CSS classes */
14
+ className?: string;
15
+ };
16
+ export interface HoverCardProps extends HoverCardOwnProps, Omit<React.ComponentPropsWithoutRef<'span'>, keyof HoverCardOwnProps> {
17
+ }
18
+ /**
19
+ * Rich preview panel that opens from hover or focus — a profile card behind
20
+ * a username, a component summary behind a link. Where Tooltip names a
21
+ * control in a line of text, HoverCard previews a destination; its panel is
22
+ * interactive, stays open while the pointer is inside it, and dresses like
23
+ * the other floating panels (Popover, the menus).
24
+ *
25
+ * The hidden panel is `visibility: hidden`, so it stays out of the
26
+ * accessibility tree and tab order until shown. Escape dismisses it.
27
+ */
28
+ export declare const HoverCard: React.ForwardRefExoticComponent<HoverCardProps & React.RefAttributes<HTMLSpanElement>>;
29
+ export {};
@@ -0,0 +1,83 @@
1
+ "use client";
2
+ import { jsxs, jsx } from "react/jsx-runtime";
3
+ import React, { useState, useRef, useId, useEffect } from "react";
4
+ import "./HoverCard.css";
5
+ import { MOTION_HOVER_SHOW_DELAY_MS, MOTION_HOVER_HIDE_DELAY_MS } from "../../tokens/motion.js";
6
+ const HoverCard = React.forwardRef(
7
+ ({
8
+ children,
9
+ content,
10
+ position = "bottom",
11
+ showDelay = MOTION_HOVER_SHOW_DELAY_MS,
12
+ hideDelay = MOTION_HOVER_HIDE_DELAY_MS,
13
+ className = "",
14
+ ...rest
15
+ }, ref) => {
16
+ const [visible, setVisible] = useState(false);
17
+ const showTimeoutRef = useRef(void 0);
18
+ const hideTimeoutRef = useRef(void 0);
19
+ const panelId = useId();
20
+ const baseClass = "ds-hover-card";
21
+ const show = () => {
22
+ clearTimeout(hideTimeoutRef.current);
23
+ showTimeoutRef.current = setTimeout(() => setVisible(true), showDelay);
24
+ };
25
+ const hide = () => {
26
+ clearTimeout(showTimeoutRef.current);
27
+ hideTimeoutRef.current = setTimeout(() => setVisible(false), hideDelay);
28
+ };
29
+ useEffect(() => {
30
+ return () => {
31
+ clearTimeout(showTimeoutRef.current);
32
+ clearTimeout(hideTimeoutRef.current);
33
+ };
34
+ }, []);
35
+ useEffect(() => {
36
+ if (!visible) return;
37
+ const onKeyDown = (e) => {
38
+ if (e.key === "Escape") {
39
+ clearTimeout(showTimeoutRef.current);
40
+ clearTimeout(hideTimeoutRef.current);
41
+ setVisible(false);
42
+ }
43
+ };
44
+ document.addEventListener("keydown", onKeyDown);
45
+ return () => document.removeEventListener("keydown", onKeyDown);
46
+ }, [visible]);
47
+ const handleBlur = (event) => {
48
+ if (!event.currentTarget.contains(event.relatedTarget)) {
49
+ hide();
50
+ }
51
+ };
52
+ const trigger = React.isValidElement(children) ? React.cloneElement(children, {
53
+ "aria-expanded": visible,
54
+ "aria-controls": visible ? panelId : void 0
55
+ }) : children;
56
+ const containerClasses = [baseClass, className].filter(Boolean).join(" ");
57
+ const panelClasses = [
58
+ `${baseClass}__panel`,
59
+ `${baseClass}__panel--${position}`,
60
+ visible ? `${baseClass}__panel--visible` : ""
61
+ ].filter(Boolean).join(" ");
62
+ return /* @__PURE__ */ jsxs(
63
+ "span",
64
+ {
65
+ ...rest,
66
+ ref,
67
+ className: containerClasses,
68
+ onMouseEnter: show,
69
+ onMouseLeave: hide,
70
+ onFocus: show,
71
+ onBlur: handleBlur,
72
+ children: [
73
+ trigger,
74
+ /* @__PURE__ */ jsx("span", { className: panelClasses, id: panelId, children: content })
75
+ ]
76
+ }
77
+ );
78
+ }
79
+ );
80
+ HoverCard.displayName = "HoverCard";
81
+ export {
82
+ HoverCard
83
+ };
@@ -0,0 +1,112 @@
1
+ /* ============================================
2
+ IMAGE COMPARE COMPONENT
3
+ Before-and-after images behind one divider
4
+ ============================================ */
5
+
6
+ .ds-image-compare {
7
+ position: relative;
8
+ width: 100%;
9
+ overflow: hidden;
10
+ border-radius: var(--radius-lg);
11
+ border: var(--border-xs) solid var(--color-bg-container-border);
12
+ background-color: var(--color-bg-container-secondary);
13
+ /* pan-y keeps the page scrollable over the frame on touch; horizontal
14
+ drags stay with the slider. */
15
+ touch-action: pan-y;
16
+ user-select: none;
17
+ cursor: col-resize;
18
+ }
19
+
20
+ /* ============================================
21
+ IMAGES
22
+ ============================================ */
23
+
24
+ .ds-image-compare__after,
25
+ .ds-image-compare__before {
26
+ position: absolute;
27
+ inset: 0;
28
+ width: 100%;
29
+ height: 100%;
30
+ object-fit: cover;
31
+ display: block;
32
+ }
33
+
34
+ /* The before layer sits on top at full frame size and is clipped to the
35
+ left of the divider. clip-path leaves the image's own geometry alone,
36
+ so the two layers stay pixel-aligned as the split moves. */
37
+ .ds-image-compare__before-clip {
38
+ position: absolute;
39
+ inset: 0;
40
+ clip-path: inset(0 calc(100% - var(--ds-image-compare-position, 50%)) 0 0);
41
+ }
42
+
43
+ /* ============================================
44
+ LABELS
45
+ ============================================ */
46
+
47
+ .ds-image-compare__label {
48
+ position: absolute;
49
+ top: var(--gap-md);
50
+ z-index: 2;
51
+ padding: var(--padding-xxxs) var(--padding-sm);
52
+ border-radius: var(--radius-full);
53
+ background-color: var(--color-bg-container-inverse);
54
+ color: var(--color-text-on-inverse);
55
+ font-family: var(--font-paragraph-sm-em-family);
56
+ font-size: var(--font-paragraph-sm-em-size);
57
+ font-weight: var(--font-paragraph-sm-em-weight);
58
+ line-height: var(--font-paragraph-sm-em-line-height);
59
+ pointer-events: none;
60
+ }
61
+
62
+ .ds-image-compare__label--before {
63
+ left: var(--gap-md);
64
+ }
65
+
66
+ .ds-image-compare__label--after {
67
+ right: var(--gap-md);
68
+ }
69
+
70
+ /* ============================================
71
+ DIVIDER + HANDLE
72
+ ============================================ */
73
+
74
+ .ds-image-compare__divider {
75
+ position: absolute;
76
+ top: 0;
77
+ bottom: 0;
78
+ left: var(--ds-image-compare-position);
79
+ width: var(--border-md);
80
+ transform: translateX(-50%);
81
+ background-color: var(--color-bg-page-primary);
82
+ z-index: 1;
83
+ pointer-events: none;
84
+ }
85
+
86
+ .ds-image-compare__handle {
87
+ position: absolute;
88
+ top: 50%;
89
+ left: var(--ds-image-compare-position);
90
+ transform: translate(-50%, -50%);
91
+ z-index: 3;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ width: 32px;
96
+ height: 32px;
97
+ border-radius: var(--radius-full);
98
+ background-color: var(--color-bg-page-primary);
99
+ color: var(--color-icon-primary);
100
+ border: var(--border-xs) solid var(--color-bg-container-border);
101
+ box-shadow: var(--shadow-floating);
102
+ cursor: col-resize;
103
+ }
104
+
105
+ .ds-image-compare__handle .material-symbols-rounded {
106
+ --icon-size: var(--icon-size-sm);
107
+ }
108
+
109
+ .ds-image-compare__handle:focus-visible {
110
+ outline: 2px solid var(--color-action-primary-bg);
111
+ outline-offset: 2px;
112
+ }
@@ -0,0 +1,40 @@
1
+ import { default as React } from 'react';
2
+ /** Props owned by ImageCompare itself — everything else falls through to the root node. */
3
+ type ImageCompareOwnProps = {
4
+ /** Source of the image revealed on the left of the divider */
5
+ beforeSrc: string;
6
+ /** Source of the image revealed on the right of the divider */
7
+ afterSrc: string;
8
+ /** Alt text for the before image */
9
+ beforeAlt: string;
10
+ /** Alt text for the after image */
11
+ afterAlt: string;
12
+ /** Corner label over the before side */
13
+ beforeLabel?: string;
14
+ /** Corner label over the after side */
15
+ afterLabel?: string;
16
+ /** Whether the corner labels render */
17
+ showLabels?: boolean;
18
+ /** Divider position as a percentage from the left (controlled) */
19
+ position?: number;
20
+ /** Initial divider position for uncontrolled use */
21
+ defaultPosition?: number;
22
+ /** Convenience callback receiving the divider position on every change */
23
+ onPositionChange?: (position: number) => void;
24
+ /** CSS aspect-ratio of the frame (both images are sized to cover it) */
25
+ aspectRatio?: string;
26
+ /** Additional CSS classes */
27
+ className?: string;
28
+ };
29
+ export interface ImageCompareProps extends ImageCompareOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof ImageCompareOwnProps> {
30
+ }
31
+ /**
32
+ * Before-and-after comparison of two images sharing one frame. Dragging the
33
+ * divider (or clicking anywhere in the frame) sweeps the split; the handle is
34
+ * a `role="slider"` with arrow-key, Home/End and PageUp/PageDown control, so
35
+ * the comparison works without a pointer.
36
+ *
37
+ * Forwards a ref to the root element and spreads unrecognised props onto it.
38
+ */
39
+ export declare const ImageCompare: React.ForwardRefExoticComponent<ImageCompareProps & React.RefAttributes<HTMLDivElement>>;
40
+ export {};