@robr0/design-system 0.13.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.
- package/README.md +9 -3
- package/components/Banner/Banner.css +110 -0
- package/components/Banner/Banner.d.ts +36 -0
- package/components/Banner/Banner.js +59 -0
- package/components/ContributionGraph/ContributionGraph.css +7 -1
- package/components/ContributionGraph/ContributionGraph.d.ts +9 -1
- package/components/ContributionGraph/ContributionGraph.js +59 -43
- package/components/EmptyState/EmptyState.css +5 -0
- package/components/FunnelChart/FunnelChart.css +3 -2
- package/components/FunnelChart/FunnelChart.d.ts +12 -4
- package/components/FunnelChart/FunnelChart.js +48 -28
- package/components/Gauge/Gauge.css +30 -13
- package/components/Gauge/Gauge.d.ts +16 -8
- package/components/Gauge/Gauge.js +78 -62
- package/components/HoverCard/HoverCard.css +97 -0
- package/components/HoverCard/HoverCard.d.ts +29 -0
- package/components/HoverCard/HoverCard.js +83 -0
- package/components/ImageCompare/ImageCompare.css +112 -0
- package/components/ImageCompare/ImageCompare.d.ts +40 -0
- package/components/ImageCompare/ImageCompare.js +134 -0
- package/components/LinkList/LinkList.d.ts +3 -1
- package/components/LinkList/LinkList.js +4 -3
- package/components/Meter/Meter.css +89 -0
- package/components/Meter/Meter.d.ts +36 -0
- package/components/Meter/Meter.js +48 -0
- package/components/NotificationCenter/NotificationCenter.css +11 -3
- package/components/Rating/Rating.css +74 -0
- package/components/Rating/Rating.d.ts +41 -0
- package/components/Rating/Rating.js +124 -0
- package/components/Sparkline/Sparkline.d.ts +5 -3
- package/components/Sparkline/Sparkline.js +30 -1
- package/components/SplitButton/SplitButton.css +93 -0
- package/components/SplitButton/SplitButton.d.ts +43 -0
- package/components/SplitButton/SplitButton.js +66 -0
- package/components/StreamingText/StreamingText.d.ts +27 -9
- package/components/StreamingText/StreamingText.js +17 -29
- package/components/StreamingText/useStreamReveal.d.ts +70 -0
- package/components/StreamingText/useStreamReveal.js +121 -0
- package/components/registry.json +48 -0
- package/components/registry.json.d.ts +48 -0
- package/components/registry.json.js +1 -1
- package/index.d.ts +6 -0
- package/index.js +15 -0
- package/package.json +5 -1
- package/tokens/motion.d.ts +9 -4
- package/tokens/motion.js +5 -1
|
@@ -15,9 +15,9 @@ type GaugeOwnProps = {
|
|
|
15
15
|
/** Upper bound of the dial. */
|
|
16
16
|
max?: number;
|
|
17
17
|
/**
|
|
18
|
-
* Colour role for the value arc
|
|
19
|
-
*
|
|
20
|
-
* crosses them.
|
|
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
21
|
*/
|
|
22
22
|
tone?: 'accent' | 'positive' | 'warning' | 'error' | 'neutral';
|
|
23
23
|
/**
|
|
@@ -35,7 +35,13 @@ type GaugeOwnProps = {
|
|
|
35
35
|
* accessible name, e.g. "CPU usage".
|
|
36
36
|
*/
|
|
37
37
|
label?: string;
|
|
38
|
-
/**
|
|
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. */
|
|
39
45
|
size?: number;
|
|
40
46
|
/** Arc thickness in pixels. */
|
|
41
47
|
strokeWidth?: number;
|
|
@@ -47,10 +53,12 @@ export interface GaugeProps extends GaugeOwnProps, Omit<React.ComponentPropsWith
|
|
|
47
53
|
/**
|
|
48
54
|
* Gauge — a radial dial for a single bounded reading: capacity, usage, a
|
|
49
55
|
* score against a target. Pure SVG computed from props — no charting library,
|
|
50
|
-
* no hooks — so it renders from a Server Component
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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.
|
|
54
62
|
*/
|
|
55
63
|
export declare const Gauge: React.ForwardRefExoticComponent<GaugeProps & React.RefAttributes<HTMLDivElement>>;
|
|
56
64
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
|
+
import "../Chart/Chart.css";
|
|
3
4
|
import "./Gauge.css";
|
|
4
5
|
const SWEEP_UNITS = 75;
|
|
5
6
|
const Gauge = React.forwardRef(
|
|
@@ -12,81 +13,96 @@ const Gauge = React.forwardRef(
|
|
|
12
13
|
showValue = true,
|
|
13
14
|
formatValue,
|
|
14
15
|
label,
|
|
16
|
+
title,
|
|
17
|
+
subtitle,
|
|
18
|
+
bare = false,
|
|
15
19
|
size = 120,
|
|
16
|
-
strokeWidth =
|
|
20
|
+
strokeWidth = 12,
|
|
17
21
|
className = "",
|
|
18
22
|
...rest
|
|
19
23
|
}, ref) => {
|
|
20
24
|
const baseClass = "ds-gauge";
|
|
25
|
+
const chartClass = "ds-chart";
|
|
21
26
|
const range = max - min;
|
|
22
27
|
const fraction = range > 0 ? Math.min(Math.max((value - min) / range, 0), 1) : 0;
|
|
23
28
|
const activeThreshold = thresholds?.filter((t) => value >= t.value).sort((a, b) => b.value - a.value)[0];
|
|
24
29
|
const resolvedTone = activeThreshold?.tone ?? tone;
|
|
25
|
-
const classes = [
|
|
30
|
+
const classes = [
|
|
31
|
+
chartClass,
|
|
32
|
+
bare && `${chartClass}--bare`,
|
|
33
|
+
baseClass,
|
|
34
|
+
`${baseClass}--${resolvedTone}`,
|
|
35
|
+
className
|
|
36
|
+
].filter(Boolean).join(" ");
|
|
26
37
|
const radius = (size - strokeWidth) / 2;
|
|
27
38
|
const center = size / 2;
|
|
28
39
|
const valueUnits = Math.round(SWEEP_UNITS * fraction * 100) / 100;
|
|
29
40
|
const displayValue = formatValue ? formatValue(value) : String(Math.round(value));
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
] });
|
|
90
106
|
}
|
|
91
107
|
);
|
|
92
108
|
Gauge.displayName = "Gauge";
|
|
@@ -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 {};
|