@gtivr4/a1-design-system-react 0.13.3 → 0.15.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/package.json +1 -1
- package/src/components/accordion/Accordion.d.ts +8 -0
- package/src/components/accordion/Accordion.jsx +9 -1
- package/src/components/accordion/accordion.css +40 -6
- package/src/components/button/button.css +7 -3
- package/src/components/figure/Figure.d.ts +30 -4
- package/src/components/figure/Figure.jsx +57 -9
- package/src/components/figure/figure.css +80 -8
- package/src/components/icon-button/IconButton.d.ts +2 -2
- package/src/components/icon-button/IconButton.jsx +3 -2
- package/src/components/icon-button/icon-button.css +11 -1
- package/src/components/menu/Menu.jsx +12 -0
- package/src/components/menu/menu.css +17 -6
- package/src/components/section/Section.d.ts +6 -0
- package/src/components/section/Section.jsx +19 -0
- package/src/components/section/section.css +33 -10
- package/src/components/slider/Slider.d.ts +71 -0
- package/src/components/slider/Slider.jsx +243 -0
- package/src/components/slider/slider.css +230 -0
- package/src/components/split-button/SplitButton.d.ts +39 -0
- package/src/components/split-button/SplitButton.jsx +92 -0
- package/src/components/split-button/split-button.css +40 -0
- package/src/components/toolbar/Toolbar.d.ts +124 -0
- package/src/components/toolbar/Toolbar.jsx +327 -0
- package/src/components/toolbar/toolbar.css +229 -0
- package/src/index.js +13 -1
- package/src/tokens.css +4 -2
package/package.json
CHANGED
|
@@ -3,6 +3,12 @@ import * as React from "react";
|
|
|
3
3
|
export interface AccordionProps {
|
|
4
4
|
/** Trigger label text */
|
|
5
5
|
label: string;
|
|
6
|
+
/**
|
|
7
|
+
* Secondary information shown in the trigger, **below** the label. It only
|
|
8
|
+
* shows while the accordion is collapsed (a glanceable summary, e.g. the
|
|
9
|
+
* applied settings) and is hidden when open. Truncates with an ellipsis.
|
|
10
|
+
*/
|
|
11
|
+
subtext?: React.ReactNode;
|
|
6
12
|
/** Controlled open state */
|
|
7
13
|
open?: boolean;
|
|
8
14
|
/** Initial open state (uncontrolled). Default: false */
|
|
@@ -11,6 +17,8 @@ export interface AccordionProps {
|
|
|
11
17
|
onChange?: (open: boolean) => void;
|
|
12
18
|
/** Size — affects trigger text size and padding. Default: "md" */
|
|
13
19
|
size?: "sm" | "md" | "lg";
|
|
20
|
+
/** Show a divider under the trigger/header (it stays attached to the header, not the bottom of the open panel). Default: false */
|
|
21
|
+
divider?: boolean;
|
|
14
22
|
/** Prevent the accordion from being toggled. Default: false */
|
|
15
23
|
disabled?: boolean;
|
|
16
24
|
className?: string;
|
|
@@ -6,11 +6,13 @@ const SIZES = ["sm", "md", "lg"];
|
|
|
6
6
|
|
|
7
7
|
export function Accordion({
|
|
8
8
|
label,
|
|
9
|
+
subtext,
|
|
9
10
|
children,
|
|
10
11
|
open: controlledOpen,
|
|
11
12
|
defaultOpen = false,
|
|
12
13
|
onChange,
|
|
13
14
|
size = "md",
|
|
15
|
+
divider = false,
|
|
14
16
|
disabled = false,
|
|
15
17
|
className = "",
|
|
16
18
|
...rest
|
|
@@ -46,6 +48,7 @@ export function Accordion({
|
|
|
46
48
|
"a1-accordion",
|
|
47
49
|
`a1-accordion--${resolvedSize}`,
|
|
48
50
|
open && "a1-accordion--open",
|
|
51
|
+
divider && "a1-accordion--divider",
|
|
49
52
|
disabled && "a1-accordion--disabled",
|
|
50
53
|
className,
|
|
51
54
|
].filter(Boolean).join(" ")}
|
|
@@ -63,7 +66,12 @@ export function Accordion({
|
|
|
63
66
|
<span className="a1-accordion__chevron" aria-hidden="true">
|
|
64
67
|
<Icon name="expand_more" />
|
|
65
68
|
</span>
|
|
66
|
-
<span className="a1-
|
|
69
|
+
<span className="a1-accordion__text">
|
|
70
|
+
<span className="a1-accordion__label">{label}</span>
|
|
71
|
+
{subtext != null && subtext !== "" && (
|
|
72
|
+
<span className="a1-accordion__subtext">{subtext}</span>
|
|
73
|
+
)}
|
|
74
|
+
</span>
|
|
67
75
|
</button>
|
|
68
76
|
|
|
69
77
|
<div
|
|
@@ -3,31 +3,34 @@
|
|
|
3
3
|
.a1-accordion {
|
|
4
4
|
/* Size tokens — default (md) */
|
|
5
5
|
--a1-ac-height: var(--component-accordion-trigger-height-md);
|
|
6
|
-
--a1-ac-px: var(--
|
|
6
|
+
--a1-ac-px: var(--base-spacing-8);
|
|
7
7
|
--a1-ac-py: var(--base-spacing-8);
|
|
8
8
|
--a1-ac-icon-size: var(--component-accordion-icon-size-md);
|
|
9
9
|
--a1-ac-font-size: var(--semantic-font-size-body-md);
|
|
10
10
|
--a1-ac-font-weight: var(--base-font-weight-medium);
|
|
11
|
+
--a1-ac-subtext-size: var(--semantic-font-size-body-sm);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/* ─── Sizes ─────────────────────────────────────────────────────────────────── */
|
|
14
15
|
|
|
15
16
|
.a1-accordion--sm {
|
|
16
17
|
--a1-ac-height: var(--component-accordion-trigger-height-sm);
|
|
17
|
-
--a1-ac-px: var(--
|
|
18
|
+
--a1-ac-px: var(--base-spacing-4);
|
|
18
19
|
--a1-ac-py: var(--base-spacing-6);
|
|
19
20
|
--a1-ac-icon-size: var(--component-accordion-icon-size-sm);
|
|
20
21
|
--a1-ac-font-size: var(--semantic-font-size-body-sm);
|
|
21
22
|
--a1-ac-font-weight: var(--base-font-weight-medium);
|
|
23
|
+
--a1-ac-subtext-size: var(--semantic-font-size-body-xs);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
.a1-accordion--lg {
|
|
25
27
|
--a1-ac-height: var(--component-accordion-trigger-height-lg);
|
|
26
|
-
--a1-ac-px: var(--
|
|
28
|
+
--a1-ac-px: var(--base-spacing-12);
|
|
27
29
|
--a1-ac-py: var(--base-spacing-12);
|
|
28
30
|
--a1-ac-icon-size: var(--component-accordion-icon-size-lg);
|
|
29
31
|
--a1-ac-font-size: var(--semantic-font-size-body-lg);
|
|
30
32
|
--a1-ac-font-weight: var(--base-font-weight-bold);
|
|
33
|
+
--a1-ac-subtext-size: var(--semantic-font-size-body-md);
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/* ─── Trigger ───────────────────────────────────────────────────────────────── */
|
|
@@ -41,7 +44,6 @@
|
|
|
41
44
|
padding-inline: var(--a1-ac-px);
|
|
42
45
|
padding-block: var(--a1-ac-py);
|
|
43
46
|
border: none;
|
|
44
|
-
border-radius: var(--component-accordion-border-radius);
|
|
45
47
|
background: transparent;
|
|
46
48
|
cursor: pointer;
|
|
47
49
|
color: var(--semantic-color-text-default);
|
|
@@ -85,13 +87,36 @@
|
|
|
85
87
|
transform: rotate(0deg);
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
/* ─── Label
|
|
90
|
+
/* ─── Label + subtext ───────────────────────────────────────────────────────── */
|
|
91
|
+
|
|
92
|
+
/* Label and subtext stack vertically (subtext below the title). */
|
|
93
|
+
.a1-accordion__text {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex: 1 1 auto;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
min-width: 0;
|
|
98
|
+
}
|
|
89
99
|
|
|
90
100
|
.a1-accordion__label {
|
|
91
|
-
flex: 1;
|
|
92
101
|
min-width: 0;
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
/* Secondary info below the title, shown only while collapsed (e.g. a summary of
|
|
105
|
+
applied settings). Truncates with an ellipsis. */
|
|
106
|
+
.a1-accordion__subtext {
|
|
107
|
+
min-width: 0;
|
|
108
|
+
color: var(--semantic-color-text-muted);
|
|
109
|
+
font-size: var(--a1-ac-subtext-size);
|
|
110
|
+
font-weight: var(--semantic-font-weight-body);
|
|
111
|
+
white-space: nowrap;
|
|
112
|
+
overflow: hidden;
|
|
113
|
+
text-overflow: ellipsis;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.a1-accordion--open .a1-accordion__subtext {
|
|
117
|
+
display: none;
|
|
118
|
+
}
|
|
119
|
+
|
|
95
120
|
/* ─── Body — CSS grid height animation ──────────────────────────────────────── */
|
|
96
121
|
|
|
97
122
|
.a1-accordion__body {
|
|
@@ -109,6 +134,15 @@
|
|
|
109
134
|
min-height: 0;
|
|
110
135
|
}
|
|
111
136
|
|
|
137
|
+
/* ─── Divider (optional) ────────────────────────────────────────────────────── */
|
|
138
|
+
/* Attached to the trigger (the expanding header), so it stays put under the
|
|
139
|
+
header whether collapsed or expanded — never drops to the bottom of the open
|
|
140
|
+
panel. */
|
|
141
|
+
|
|
142
|
+
.a1-accordion--divider .a1-accordion__trigger {
|
|
143
|
+
border-block-end: var(--component-divider-size-xs) solid var(--semantic-color-border-subtle);
|
|
144
|
+
}
|
|
145
|
+
|
|
112
146
|
/* ─── Disabled ──────────────────────────────────────────────────────────────── */
|
|
113
147
|
|
|
114
148
|
.a1-accordion--disabled .a1-accordion__trigger {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
.a1-button {
|
|
2
2
|
box-sizing: border-box;
|
|
3
|
-
|
|
3
|
+
/* min-height keeps the standard target size for a single line; the button
|
|
4
|
+
grows taller when a long label wraps to multiple lines. */
|
|
4
5
|
min-height: var(--a1-button-height, var(--component-button-min-height));
|
|
5
|
-
max-height: var(--a1-button-height, var(--component-button-min-height));
|
|
6
6
|
display: inline-flex;
|
|
7
7
|
align-items: center;
|
|
8
8
|
justify-content: center;
|
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
--a1-icon-weight: 700;
|
|
19
19
|
line-height: var(--component-button-font-line-height);
|
|
20
20
|
text-decoration: none;
|
|
21
|
-
|
|
21
|
+
/* Allow long labels to wrap onto multiple lines (centered), breaking an
|
|
22
|
+
over-long word if needed, instead of overflowing a fixed-height pill. */
|
|
23
|
+
white-space: normal;
|
|
24
|
+
text-align: center;
|
|
25
|
+
overflow-wrap: anywhere;
|
|
22
26
|
overflow: clip;
|
|
23
27
|
cursor: pointer;
|
|
24
28
|
background: var(--a1-button-background);
|
|
@@ -13,12 +13,38 @@ export interface FigureProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
13
13
|
captionSrOnly?: boolean;
|
|
14
14
|
/** Caption alignment. Default: "start" */
|
|
15
15
|
captionPosition?: "start" | "center";
|
|
16
|
-
/** Border radius on the image. */
|
|
16
|
+
/** Border radius on the image. Default (no prop) is square, same as "none". */
|
|
17
17
|
radius?: "none" | "sm" | "md" | "lg";
|
|
18
18
|
/** Constrain figure width. */
|
|
19
|
-
size?: "xs" | "sm" | "md" | "lg";
|
|
20
|
-
/** Horizontal alignment of the figure. Default: "
|
|
21
|
-
align?: "start" | "center" | "end";
|
|
19
|
+
size?: "3xs" | "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
|
|
20
|
+
/** Horizontal alignment of the figure. Default: "none" (normal flow). */
|
|
21
|
+
align?: "none" | "start" | "center" | "end";
|
|
22
|
+
/**
|
|
23
|
+
* Fix the image to a set aspect ratio, cropping to fill via `object-fit: cover`.
|
|
24
|
+
* Omit for the image's natural ratio.
|
|
25
|
+
*/
|
|
26
|
+
aspectRatio?: "16:9" | "4:3" | "3:2" | "1:1" | "2:3" | "3:4" | "9:16" | "21:9";
|
|
27
|
+
/**
|
|
28
|
+
* Crop focal point used when the image is cropped (i.e. when `aspectRatio` or a
|
|
29
|
+
* fixed height applies). Maps to `object-position`. Default: "center"
|
|
30
|
+
*/
|
|
31
|
+
crop?:
|
|
32
|
+
| "center"
|
|
33
|
+
| "top"
|
|
34
|
+
| "bottom"
|
|
35
|
+
| "left"
|
|
36
|
+
| "right"
|
|
37
|
+
| "top-left"
|
|
38
|
+
| "top-right"
|
|
39
|
+
| "bottom-left"
|
|
40
|
+
| "bottom-right";
|
|
41
|
+
/**
|
|
42
|
+
* Freeform crop: a sub-rectangle of the image to show, expressed as fractions
|
|
43
|
+
* (0–1) of the natural image — `{ x, y, width, height }` where x/y is the
|
|
44
|
+
* top-left corner. Applied non-destructively (CSS only); the image is never
|
|
45
|
+
* modified. Takes precedence over `aspectRatio` / `crop` when set.
|
|
46
|
+
*/
|
|
47
|
+
cropRect?: { x: number; y: number; width: number; height: number };
|
|
22
48
|
/** Top margin. */
|
|
23
49
|
marginTop?: "sm" | "md" | "lg";
|
|
24
50
|
/** Bottom margin. */
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import "./figure.css";
|
|
2
|
+
import { useState } from "react";
|
|
2
3
|
import { Bleed } from "../bleed/Bleed.jsx";
|
|
3
4
|
|
|
5
|
+
function isCropRect(rect) {
|
|
6
|
+
return (
|
|
7
|
+
rect != null &&
|
|
8
|
+
typeof rect === "object" &&
|
|
9
|
+
["x", "y", "width", "height"].every((k) => typeof rect[k] === "number") &&
|
|
10
|
+
rect.width > 0 &&
|
|
11
|
+
rect.height > 0
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
const rounded = ["none", "sm", "md", "lg"];
|
|
5
16
|
const captionPositions = ["start", "center"];
|
|
6
17
|
const spacings = ["sm", "md", "lg"];
|
|
7
|
-
const sizes = ["xs", "sm", "md", "lg"];
|
|
8
|
-
const alignments = ["start", "center", "end"];
|
|
18
|
+
const sizes = ["3xs", "2xs", "xs", "sm", "md", "lg", "xl", "xxl"];
|
|
19
|
+
const alignments = ["none", "start", "center", "end"];
|
|
20
|
+
const aspectRatios = ["16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16", "21:9"];
|
|
21
|
+
const crops = ["center", "top", "bottom", "left", "right", "top-left", "top-right", "bottom-left", "bottom-right"];
|
|
9
22
|
|
|
10
23
|
export function Figure({
|
|
11
24
|
src,
|
|
@@ -16,6 +29,9 @@ export function Figure({
|
|
|
16
29
|
radius,
|
|
17
30
|
size,
|
|
18
31
|
align,
|
|
32
|
+
aspectRatio,
|
|
33
|
+
crop,
|
|
34
|
+
cropRect,
|
|
19
35
|
marginTop,
|
|
20
36
|
marginBottom,
|
|
21
37
|
bleed,
|
|
@@ -25,12 +41,20 @@ export function Figure({
|
|
|
25
41
|
imgStyle,
|
|
26
42
|
...props
|
|
27
43
|
}) {
|
|
44
|
+
// Freeform crop: a sub-rectangle of the image (fractions 0–1) shown
|
|
45
|
+
// non-destructively. We measure the natural ratio to size the crop box.
|
|
46
|
+
const cropped = isCropRect(cropRect);
|
|
47
|
+
const [naturalRatio, setNaturalRatio] = useState(null);
|
|
48
|
+
|
|
28
49
|
const classes = [
|
|
29
50
|
"a1-figure",
|
|
30
51
|
radius != null && rounded.includes(radius) && `a1-figure--rounded-${radius}`,
|
|
31
52
|
captionPositions.includes(captionPosition) && captionPosition !== "start" && `a1-figure--caption-${captionPosition}`,
|
|
32
53
|
sizes.includes(size) && `a1-figure--${size}`,
|
|
33
|
-
alignments.includes(align) && align !== "
|
|
54
|
+
alignments.includes(align) && align !== "none" && `a1-figure--align-${align}`,
|
|
55
|
+
!cropped && aspectRatios.includes(aspectRatio) && `a1-figure--ratio-${aspectRatio.replace(":", "-")}`,
|
|
56
|
+
!cropped && crops.includes(crop) && crop !== "center" && `a1-figure--crop-${crop}`,
|
|
57
|
+
cropped && "a1-figure--cropped",
|
|
34
58
|
spacings.includes(marginTop) && `a1-figure--mt-${marginTop}`,
|
|
35
59
|
spacings.includes(marginBottom) && `a1-figure--mb-${marginBottom}`,
|
|
36
60
|
className,
|
|
@@ -40,14 +64,38 @@ export function Figure({
|
|
|
40
64
|
captionSrOnly ? "a1-sr-only" : "a1-figure__caption",
|
|
41
65
|
].join(" ");
|
|
42
66
|
|
|
67
|
+
const img = (
|
|
68
|
+
<img
|
|
69
|
+
src={src}
|
|
70
|
+
alt={alt}
|
|
71
|
+
className={["a1-figure__img", imgClassName].filter(Boolean).join(" ")}
|
|
72
|
+
style={imgStyle}
|
|
73
|
+
onLoad={cropped ? (e) => {
|
|
74
|
+
const { naturalWidth, naturalHeight } = e.currentTarget;
|
|
75
|
+
if (naturalWidth && naturalHeight) setNaturalRatio(naturalWidth / naturalHeight);
|
|
76
|
+
} : undefined}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const media = cropped ? (
|
|
81
|
+
<div
|
|
82
|
+
className="a1-figure__crop"
|
|
83
|
+
style={{
|
|
84
|
+
"--a1-figure-crop-x": cropRect.x,
|
|
85
|
+
"--a1-figure-crop-y": cropRect.y,
|
|
86
|
+
"--a1-figure-crop-w": cropRect.width,
|
|
87
|
+
"--a1-figure-crop-h": cropRect.height,
|
|
88
|
+
// Box aspect ratio = (cropW · imgW) / (cropH · imgH).
|
|
89
|
+
...(naturalRatio ? { aspectRatio: `${(cropRect.width / cropRect.height) * naturalRatio}` } : {}),
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{img}
|
|
93
|
+
</div>
|
|
94
|
+
) : img;
|
|
95
|
+
|
|
43
96
|
const figure = (
|
|
44
97
|
<figure className={classes} style={style} {...props}>
|
|
45
|
-
|
|
46
|
-
src={src}
|
|
47
|
-
alt={alt}
|
|
48
|
-
className={["a1-figure__img", imgClassName].filter(Boolean).join(" ")}
|
|
49
|
-
style={imgStyle}
|
|
50
|
-
/>
|
|
98
|
+
{media}
|
|
51
99
|
{caption && (
|
|
52
100
|
<figcaption className={captionClasses}>{caption}</figcaption>
|
|
53
101
|
)}
|
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
display: block;
|
|
16
16
|
width: 100%;
|
|
17
17
|
height: auto;
|
|
18
|
-
|
|
18
|
+
/* Square by default — this is the same as radius="none". Use the `radius`
|
|
19
|
+
prop to round the corners. */
|
|
20
|
+
border-radius: 0;
|
|
21
|
+
/* Crop point. Harmless on natural-ratio images (the box already matches the
|
|
22
|
+
intrinsic ratio); takes effect once a fixed aspect-ratio or height crops. */
|
|
23
|
+
object-fit: cover;
|
|
24
|
+
object-position: var(--a1-figure-crop, center);
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
.a1-figure__caption {
|
|
@@ -27,25 +33,47 @@
|
|
|
27
33
|
font-style: italic;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
/* ──
|
|
36
|
+
/* ── Freeform crop (cropRect) ────────────────────────────────────────────────
|
|
37
|
+
The crop box clips a sub-rectangle of the image. The image is scaled so the
|
|
38
|
+
crop region fills the box width and shifted so the region's top-left aligns. */
|
|
39
|
+
.a1-figure__crop {
|
|
40
|
+
position: relative;
|
|
41
|
+
inline-size: 100%;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
}
|
|
31
44
|
|
|
32
|
-
.a1-figure--
|
|
33
|
-
|
|
45
|
+
.a1-figure--cropped .a1-figure__crop .a1-figure__img {
|
|
46
|
+
position: absolute;
|
|
47
|
+
inline-size: calc(100% / var(--a1-figure-crop-w));
|
|
48
|
+
max-inline-size: none;
|
|
49
|
+
block-size: auto;
|
|
50
|
+
inset-inline-start: calc(var(--a1-figure-crop-x) / var(--a1-figure-crop-w) * -100%);
|
|
51
|
+
inset-block-start: calc(var(--a1-figure-crop-y) / var(--a1-figure-crop-h) * -100%);
|
|
52
|
+
border-radius: 0;
|
|
34
53
|
}
|
|
35
54
|
|
|
36
|
-
|
|
55
|
+
/* ── Rounded modifier ──────────────────────────────────────────────────────── */
|
|
56
|
+
|
|
57
|
+
/* radius="none" matches the default (square) — kept so a figure can be reset to
|
|
58
|
+
square when a wrapping context sets a radius. The crop box, when present, is
|
|
59
|
+
the clipping element so it carries the radius. */
|
|
60
|
+
.a1-figure--rounded-none .a1-figure__img,
|
|
61
|
+
.a1-figure--rounded-none .a1-figure__crop {
|
|
37
62
|
border-radius: 0;
|
|
38
63
|
}
|
|
39
64
|
|
|
40
|
-
.a1-figure--rounded-sm .a1-figure__img
|
|
65
|
+
.a1-figure--rounded-sm .a1-figure__img,
|
|
66
|
+
.a1-figure--rounded-sm .a1-figure__crop {
|
|
41
67
|
border-radius: var(--base-radius-sm);
|
|
42
68
|
}
|
|
43
69
|
|
|
44
|
-
.a1-figure--rounded-md .a1-figure__img
|
|
70
|
+
.a1-figure--rounded-md .a1-figure__img,
|
|
71
|
+
.a1-figure--rounded-md .a1-figure__crop {
|
|
45
72
|
border-radius: var(--base-radius-md);
|
|
46
73
|
}
|
|
47
74
|
|
|
48
|
-
.a1-figure--rounded-lg .a1-figure__img
|
|
75
|
+
.a1-figure--rounded-lg .a1-figure__img,
|
|
76
|
+
.a1-figure--rounded-lg .a1-figure__crop {
|
|
49
77
|
border-radius: var(--base-radius-lg);
|
|
50
78
|
}
|
|
51
79
|
|
|
@@ -60,8 +88,11 @@
|
|
|
60
88
|
.a1-figure--mb-lg { margin-bottom: var(--base-spacing-24); }
|
|
61
89
|
|
|
62
90
|
/* ── Alignment ───────────────────────────────────────────────────────────── */
|
|
91
|
+
/* Default (align="none"): no alignment — the figure flows at the inline-start in
|
|
92
|
+
normal document flow. start/center/end are explicit alignments. */
|
|
63
93
|
|
|
64
94
|
/* start/end: constrain the figure itself and shift via margin */
|
|
95
|
+
.a1-figure--align-start { margin-inline-end: auto; }
|
|
65
96
|
.a1-figure--align-end { margin-inline-start: auto; }
|
|
66
97
|
|
|
67
98
|
/* center: figure stays full-width; size constrains img + caption */
|
|
@@ -70,14 +101,24 @@
|
|
|
70
101
|
/* ── Size ────────────────────────────────────────────────────────────────── */
|
|
71
102
|
|
|
72
103
|
/* Default: constrain the figure element */
|
|
104
|
+
.a1-figure--3xs { max-width: 5rem; }
|
|
105
|
+
.a1-figure--2xs { max-width: 8rem; }
|
|
73
106
|
.a1-figure--xs { max-width: 12rem; }
|
|
74
107
|
.a1-figure--sm { max-width: 20rem; }
|
|
75
108
|
.a1-figure--md { max-width: 30rem; }
|
|
76
109
|
.a1-figure--lg { max-width: 40rem; }
|
|
110
|
+
.a1-figure--xl { max-width: 50rem; }
|
|
111
|
+
.a1-figure--xxl { max-width: 60rem; }
|
|
77
112
|
|
|
78
113
|
/* Center override: transfer the constraint to img + caption instead */
|
|
79
114
|
.a1-figure--align-center { max-width: none; }
|
|
80
115
|
|
|
116
|
+
.a1-figure--align-center.a1-figure--3xs .a1-figure__img,
|
|
117
|
+
.a1-figure--align-center.a1-figure--3xs .a1-figure__caption { max-width: 5rem; }
|
|
118
|
+
|
|
119
|
+
.a1-figure--align-center.a1-figure--2xs .a1-figure__img,
|
|
120
|
+
.a1-figure--align-center.a1-figure--2xs .a1-figure__caption { max-width: 8rem; }
|
|
121
|
+
|
|
81
122
|
.a1-figure--align-center.a1-figure--xs .a1-figure__img,
|
|
82
123
|
.a1-figure--align-center.a1-figure--xs .a1-figure__caption { max-width: 12rem; }
|
|
83
124
|
|
|
@@ -90,6 +131,37 @@
|
|
|
90
131
|
.a1-figure--align-center.a1-figure--lg .a1-figure__img,
|
|
91
132
|
.a1-figure--align-center.a1-figure--lg .a1-figure__caption { max-width: 40rem; }
|
|
92
133
|
|
|
134
|
+
.a1-figure--align-center.a1-figure--xl .a1-figure__img,
|
|
135
|
+
.a1-figure--align-center.a1-figure--xl .a1-figure__caption { max-width: 50rem; }
|
|
136
|
+
|
|
137
|
+
.a1-figure--align-center.a1-figure--xxl .a1-figure__img,
|
|
138
|
+
.a1-figure--align-center.a1-figure--xxl .a1-figure__caption { max-width: 60rem; }
|
|
139
|
+
|
|
140
|
+
/* ── Aspect ratio ────────────────────────────────────────────────────────── */
|
|
141
|
+
|
|
142
|
+
/* A fixed aspect-ratio box; the image fills it via object-fit: cover, cropping
|
|
143
|
+
to the `crop` point. width:100% + height:auto lets aspect-ratio size the box. */
|
|
144
|
+
.a1-figure--ratio-16-9 .a1-figure__img { aspect-ratio: 16 / 9; }
|
|
145
|
+
.a1-figure--ratio-4-3 .a1-figure__img { aspect-ratio: 4 / 3; }
|
|
146
|
+
.a1-figure--ratio-3-2 .a1-figure__img { aspect-ratio: 3 / 2; }
|
|
147
|
+
.a1-figure--ratio-1-1 .a1-figure__img { aspect-ratio: 1 / 1; }
|
|
148
|
+
.a1-figure--ratio-2-3 .a1-figure__img { aspect-ratio: 2 / 3; }
|
|
149
|
+
.a1-figure--ratio-3-4 .a1-figure__img { aspect-ratio: 3 / 4; }
|
|
150
|
+
.a1-figure--ratio-9-16 .a1-figure__img { aspect-ratio: 9 / 16; }
|
|
151
|
+
.a1-figure--ratio-21-9 .a1-figure__img { aspect-ratio: 21 / 9; }
|
|
152
|
+
|
|
153
|
+
/* ── Crop (object-position) ──────────────────────────────────────────────────
|
|
154
|
+
Each modifier sets --a1-figure-crop, read by .a1-figure__img's
|
|
155
|
+
object-position. Default (center) needs no class. */
|
|
156
|
+
.a1-figure--crop-top { --a1-figure-crop: center top; }
|
|
157
|
+
.a1-figure--crop-bottom { --a1-figure-crop: center bottom; }
|
|
158
|
+
.a1-figure--crop-left { --a1-figure-crop: left center; }
|
|
159
|
+
.a1-figure--crop-right { --a1-figure-crop: right center; }
|
|
160
|
+
.a1-figure--crop-top-left { --a1-figure-crop: left top; }
|
|
161
|
+
.a1-figure--crop-top-right { --a1-figure-crop: right top; }
|
|
162
|
+
.a1-figure--crop-bottom-left { --a1-figure-crop: left bottom; }
|
|
163
|
+
.a1-figure--crop-bottom-right { --a1-figure-crop: right bottom; }
|
|
164
|
+
|
|
93
165
|
/* ── Caption position ──────────────────────────────────────────────────────── */
|
|
94
166
|
|
|
95
167
|
.a1-figure--caption-center .a1-figure__caption {
|
|
@@ -13,8 +13,8 @@ export interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonEl
|
|
|
13
13
|
label: string;
|
|
14
14
|
/** Visual style. Default: "tertiary" */
|
|
15
15
|
variant?: "tertiary" | "secondary" | "destructive" | "success";
|
|
16
|
-
/** Button size. "lg" matches Button's large touch target (3.5rem) and icon size
|
|
17
|
-
size?: "md" | "lg";
|
|
16
|
+
/** Button size. "sm" is a 24×24px target (the WCAG 2.2 AA minimum) for dense toolbars; "lg" matches Button's large touch target (3.5rem) and icon size. Default: "md" */
|
|
17
|
+
size?: "sm" | "md" | "lg";
|
|
18
18
|
/** Link target when rendered with `as="a"`. */
|
|
19
19
|
href?: string;
|
|
20
20
|
disabled?: boolean;
|
|
@@ -2,7 +2,8 @@ import "./icon-button.css";
|
|
|
2
2
|
import { Icon } from "../icon/Icon.jsx";
|
|
3
3
|
|
|
4
4
|
const variants = ["tertiary", "secondary", "destructive", "success"];
|
|
5
|
-
const sizes = ["md", "lg"];
|
|
5
|
+
const sizes = ["sm", "md", "lg"];
|
|
6
|
+
const sizeClass = { sm: "a1-icon-button--small", lg: "a1-icon-button--large" };
|
|
6
7
|
|
|
7
8
|
export function IconButton({
|
|
8
9
|
as: Component = "button",
|
|
@@ -23,7 +24,7 @@ export function IconButton({
|
|
|
23
24
|
const classes = [
|
|
24
25
|
"a1-icon-button",
|
|
25
26
|
`a1-icon-button--${resolvedVariant}`,
|
|
26
|
-
resolvedSize
|
|
27
|
+
resolvedSize && sizeClass[resolvedSize],
|
|
27
28
|
className,
|
|
28
29
|
].filter(Boolean).join(" ");
|
|
29
30
|
|
|
@@ -37,12 +37,22 @@
|
|
|
37
37
|
--a1-icon-opsz: var(--a1-icon-button-icon-opsz, var(--component-icon-button-icon-optical-size));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/* ── Size: small ─────────────────────────────────────────────────────────── */
|
|
41
|
+
/* 24×24px total — the WCAG 2.2 target-size (AA) minimum; for dense toolbars. */
|
|
42
|
+
|
|
43
|
+
.a1-icon-button--small {
|
|
44
|
+
--a1-icon-button-size: var(--base-spacing-24);
|
|
45
|
+
--a1-icon-button-icon-size: var(--base-spacing-16);
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
/* ── Size: large ─────────────────────────────────────────────────────────── */
|
|
41
49
|
/* Touch target matches Button lg (3.5rem); icon matches Button lg's icon. */
|
|
42
50
|
|
|
43
51
|
.a1-icon-button--large {
|
|
44
52
|
--a1-icon-button-size: var(--component-button-large-height);
|
|
45
|
-
|
|
53
|
+
/* Icon is two steps up the icon scale (20 → 24 → 32px) so it reads at the
|
|
54
|
+
larger touch target. */
|
|
55
|
+
--a1-icon-button-icon-size: var(--base-spacing-32);
|
|
46
56
|
--a1-icon-button-icon-opsz: var(--component-button-icon-optical-size);
|
|
47
57
|
}
|
|
48
58
|
|
|
@@ -81,6 +81,18 @@ export function Menu({
|
|
|
81
81
|
el.style.setProperty("--a1-menu-top", `${Math.round(top)}px`);
|
|
82
82
|
el.style.setProperty("--a1-menu-left", `${Math.round(left)}px`);
|
|
83
83
|
el.style.setProperty("--a1-menu-max-height", `${Math.floor(maxHeight)}px`);
|
|
84
|
+
|
|
85
|
+
// `position: fixed` resolves against the nearest transformed/filtered
|
|
86
|
+
// ancestor's box, not the viewport — e.g. an `overlay` Toolbar positioned
|
|
87
|
+
// with a CSS transform. Measure where the menu actually landed and correct
|
|
88
|
+
// by the containing-block offset so it stays anchored to the viewport.
|
|
89
|
+
const placed = el.getBoundingClientRect();
|
|
90
|
+
const dx = left - placed.left;
|
|
91
|
+
const dy = top - placed.top;
|
|
92
|
+
if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
|
|
93
|
+
el.style.setProperty("--a1-menu-left", `${Math.round(left + dx)}px`);
|
|
94
|
+
el.style.setProperty("--a1-menu-top", `${Math.round(top + dy)}px`);
|
|
95
|
+
}
|
|
84
96
|
}, [anchorRef]);
|
|
85
97
|
|
|
86
98
|
const openDialog = useCallback(() => {
|
|
@@ -104,16 +104,27 @@
|
|
|
104
104
|
background: transparent;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/* Active / current-page indicator
|
|
107
|
+
/* Active / current-page indicator — matches the TreeMenu selected pattern:
|
|
108
|
+
a solid action-background fill for a clear, unambiguous highlight. */
|
|
108
109
|
.a1-menu-item--active {
|
|
109
|
-
color: var(--semantic-color-
|
|
110
|
-
background: var(--semantic-color-action-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
color: var(--semantic-color-action-foreground);
|
|
111
|
+
background: var(--semantic-color-action-background);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.a1-menu-item--active:hover {
|
|
115
|
+
background: var(--semantic-color-action-background-hover);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.a1-menu-item--active:active {
|
|
119
|
+
background: var(--semantic-color-action-background-pressed);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.a1-menu-item--active:focus-visible {
|
|
123
|
+
outline-color: var(--semantic-color-action-foreground);
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
.a1-menu-item--active .a1-menu-item__icon {
|
|
116
|
-
color: var(--semantic-color-action-
|
|
127
|
+
color: var(--semantic-color-action-foreground);
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
/* Destructive variant */
|
|
@@ -33,6 +33,12 @@ export interface SectionProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
33
33
|
borderStyle?: "solid" | "dashed" | "dotted";
|
|
34
34
|
/** Border color tone. Uses the same variants as Divider. Default: "subtle" */
|
|
35
35
|
borderVariant?: "subtle" | "strong" | "accent";
|
|
36
|
+
/**
|
|
37
|
+
* Which sides the border is drawn on (requires `borderSize`). `"all"` (default)
|
|
38
|
+
* draws all four sides; pass an array to draw only those sides, e.g.
|
|
39
|
+
* `["top", "bottom"]`. An empty array draws no border.
|
|
40
|
+
*/
|
|
41
|
+
borderSides?: "all" | ("top" | "right" | "bottom" | "left")[];
|
|
36
42
|
/** Border radius scale. */
|
|
37
43
|
radius?: "none" | "sm" | "md" | "lg" | "xl";
|
|
38
44
|
children?: React.ReactNode;
|
|
@@ -23,8 +23,19 @@ const VALID_ALIGNMENTS = ["left", "center", "right"];
|
|
|
23
23
|
const VALID_BORDER_SIZES = ["xs", "sm", "md", "lg"];
|
|
24
24
|
const VALID_BORDER_STYLES = ["solid", "dashed", "dotted"];
|
|
25
25
|
const VALID_BORDER_VARIANTS = ["subtle", "strong", "accent"];
|
|
26
|
+
const VALID_BORDER_SIDES = ["top", "right", "bottom", "left"];
|
|
26
27
|
const VALID_RADII = ["none", "sm", "md", "lg", "xl"];
|
|
27
28
|
|
|
29
|
+
// Resolve which sides get the border. `null` means all sides (the default);
|
|
30
|
+
// an array means only those sides (an empty array means no border).
|
|
31
|
+
function resolveBorderSides(borderSides) {
|
|
32
|
+
if (borderSides == null || borderSides === "all") return null;
|
|
33
|
+
const arr = Array.isArray(borderSides) ? borderSides : [borderSides];
|
|
34
|
+
const sides = VALID_BORDER_SIDES.filter((side) => arr.includes(side));
|
|
35
|
+
if (sides.length === VALID_BORDER_SIDES.length) return null; // all four = all
|
|
36
|
+
return sides;
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
export function Section({
|
|
29
40
|
as: Component = "section",
|
|
30
41
|
padding = "md",
|
|
@@ -39,6 +50,7 @@ export function Section({
|
|
|
39
50
|
borderSize,
|
|
40
51
|
borderStyle = "solid",
|
|
41
52
|
borderVariant = "subtle",
|
|
53
|
+
borderSides,
|
|
42
54
|
radius,
|
|
43
55
|
className = "",
|
|
44
56
|
children,
|
|
@@ -98,6 +110,13 @@ export function Section({
|
|
|
98
110
|
|
|
99
111
|
if (borderSize && VALID_BORDER_SIZES.includes(borderSize)) {
|
|
100
112
|
classes.push(`a1-section--border-${borderSize}`);
|
|
113
|
+
|
|
114
|
+
// Per-side borders. Omitted / "all" draws all four sides (default).
|
|
115
|
+
const sides = resolveBorderSides(borderSides);
|
|
116
|
+
if (sides) {
|
|
117
|
+
classes.push("a1-section--border-sided");
|
|
118
|
+
sides.forEach((side) => classes.push(`a1-section--border-side-${side}`));
|
|
119
|
+
}
|
|
101
120
|
}
|
|
102
121
|
|
|
103
122
|
if (borderStyle && VALID_BORDER_STYLES.includes(borderStyle)) {
|