@gtivr4/a1-design-system-react 0.14.0 → 0.18.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 +3 -2
- package/src/color-scheme.css +2 -0
- package/src/components/accordion/Accordion.d.ts +8 -0
- package/src/components/accordion/Accordion.jsx +9 -1
- package/src/components/accordion/accordion.css +46 -6
- package/src/components/autocomplete/Autocomplete.d.ts +53 -0
- package/src/components/autocomplete/Autocomplete.jsx +380 -0
- package/src/components/autocomplete/autocomplete.css +346 -0
- package/src/components/banner/Banner.d.ts +9 -2
- package/src/components/banner/Banner.jsx +32 -6
- package/src/components/banner/banner.css +81 -0
- package/src/components/bottom-sheet/BottomSheet.d.ts +22 -0
- package/src/components/bottom-sheet/BottomSheet.jsx +154 -0
- package/src/components/bottom-sheet/bottom-sheet.css +113 -0
- package/src/components/button/button.css +7 -3
- package/src/components/code/Code.jsx +6 -1
- package/src/components/data-table/DataTable.jsx +11 -1
- package/src/components/data-table/data-table.css +19 -0
- package/src/components/figure/Figure.d.ts +37 -4
- package/src/components/figure/Figure.jsx +78 -9
- package/src/components/figure/figure.css +105 -8
- package/src/components/grid/Grid.d.ts +1 -1
- package/src/components/grid/Grid.jsx +2 -0
- package/src/components/grid/grid.css +5 -0
- 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/page-layout/page-layout.css +10 -4
- package/src/components/page-nav/PageNav.jsx +29 -8
- package/src/components/page-nav/page-nav.css +13 -0
- package/src/components/paragraph/Paragraph.d.ts +2 -0
- package/src/components/paragraph/Paragraph.jsx +4 -0
- package/src/components/paragraph/paragraph.css +6 -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/segmented-control/SegmentedControl.d.ts +8 -0
- package/src/components/segmented-control/SegmentedControl.jsx +16 -3
- package/src/components/segmented-control/segmented.css +31 -1
- package/src/components/slider/Slider.d.ts +71 -0
- package/src/components/slider/Slider.jsx +243 -0
- package/src/components/slider/slider.css +238 -0
- package/src/components/split-button/SplitButton.d.ts +39 -0
- package/src/components/split-button/SplitButton.jsx +94 -0
- package/src/components/split-button/split-button.css +40 -0
- package/src/components/tabs/tabs.css +3 -0
- package/src/components/toolbar/Toolbar.d.ts +131 -0
- package/src/components/toolbar/Toolbar.jsx +335 -0
- package/src/components/toolbar/toolbar.css +229 -0
- package/src/components/top-header/top-header.css +2 -0
- package/src/components/tree-menu/TreeMenu.jsx +11 -7
- package/src/index.d.ts +71 -0
- package/src/index.js +15 -1
- package/src/themes.css +293 -0
- package/src/tokens.css +26 -3
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/* BottomSheet — fixed bottom panel, no scrim, separation via shadow. Drag handle
|
|
2
|
+
resizes between detents; content scrolls internally. xs + sm only. */
|
|
3
|
+
|
|
4
|
+
.a1-bottom-sheet {
|
|
5
|
+
position: fixed;
|
|
6
|
+
inset-inline: 0;
|
|
7
|
+
inset-block-end: 0;
|
|
8
|
+
z-index: var(--component-bottom-sheet-z-index, 200);
|
|
9
|
+
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
block-size: var(--a1-bottom-sheet-height, var(--component-bottom-sheet-header-height, 3.5rem));
|
|
13
|
+
max-block-size: 96dvh;
|
|
14
|
+
min-block-size: var(--component-bottom-sheet-header-height, 3.5rem);
|
|
15
|
+
|
|
16
|
+
background: var(--component-bottom-sheet-background, var(--semantic-color-surface-card));
|
|
17
|
+
border-start-start-radius: var(--component-bottom-sheet-border-radius, var(--base-radius-lg));
|
|
18
|
+
border-start-end-radius: var(--component-bottom-sheet-border-radius, var(--base-radius-lg));
|
|
19
|
+
/* Strong 1px hairline along the top edge (and up the rounded corners). */
|
|
20
|
+
border-block-start: var(--component-divider-size-sm) solid var(--semantic-color-border-strong);
|
|
21
|
+
border-inline: var(--component-divider-size-sm) solid var(--semantic-color-border-strong);
|
|
22
|
+
/* Upward shadow only — separates the sheet from page content without a scrim. */
|
|
23
|
+
box-shadow:
|
|
24
|
+
0 calc(-1 * var(--base-spacing-2)) var(--base-spacing-8)
|
|
25
|
+
color-mix(in srgb, var(--semantic-color-text-default) 12%, transparent),
|
|
26
|
+
var(--semantic-shadow-xl);
|
|
27
|
+
|
|
28
|
+
padding-block-end: env(safe-area-inset-bottom, 0px);
|
|
29
|
+
transition: block-size var(--semantic-motion-duration-fast) var(--semantic-motion-easing-standard);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.a1-bottom-sheet--dragging {
|
|
33
|
+
transition: none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ── Header (drag handle + title) ──────────────────────────────────────────── */
|
|
37
|
+
|
|
38
|
+
.a1-bottom-sheet__header {
|
|
39
|
+
flex: none;
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
align-items: stretch;
|
|
43
|
+
gap: var(--base-spacing-8);
|
|
44
|
+
padding: var(--base-spacing-8) var(--component-bottom-sheet-padding, var(--base-spacing-16));
|
|
45
|
+
min-block-size: var(--component-bottom-sheet-header-height, 3.5rem);
|
|
46
|
+
cursor: grab;
|
|
47
|
+
touch-action: none;
|
|
48
|
+
user-select: none;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.a1-bottom-sheet--dragging .a1-bottom-sheet__header {
|
|
52
|
+
cursor: grabbing;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.a1-bottom-sheet__handle {
|
|
56
|
+
align-self: center;
|
|
57
|
+
flex: none;
|
|
58
|
+
inline-size: var(--component-bottom-sheet-handle-width, var(--base-spacing-40));
|
|
59
|
+
block-size: var(--component-bottom-sheet-handle-height, var(--base-spacing-4));
|
|
60
|
+
padding: 0;
|
|
61
|
+
border: none;
|
|
62
|
+
border-radius: 999px;
|
|
63
|
+
background: var(--semantic-color-border-strong);
|
|
64
|
+
cursor: grab;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.a1-bottom-sheet__handle:focus-visible {
|
|
68
|
+
outline: var(--component-button-focus-ring-width, 2px) solid var(--semantic-color-text-accent);
|
|
69
|
+
outline-offset: var(--base-spacing-4);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.a1-bottom-sheet__title {
|
|
73
|
+
font-size: var(--semantic-font-size-body-md);
|
|
74
|
+
font-weight: var(--semantic-font-weight-heading);
|
|
75
|
+
color: var(--semantic-color-text-default);
|
|
76
|
+
white-space: nowrap;
|
|
77
|
+
overflow: hidden;
|
|
78
|
+
text-overflow: ellipsis;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ── Scrollable content ────────────────────────────────────────────────────── */
|
|
82
|
+
|
|
83
|
+
.a1-bottom-sheet__content {
|
|
84
|
+
flex: 1 1 auto;
|
|
85
|
+
min-block-size: 0;
|
|
86
|
+
overflow-y: auto;
|
|
87
|
+
overscroll-behavior: contain;
|
|
88
|
+
-webkit-overflow-scrolling: touch;
|
|
89
|
+
padding:
|
|
90
|
+
0
|
|
91
|
+
var(--component-bottom-sheet-padding, var(--base-spacing-16))
|
|
92
|
+
var(--base-spacing-16);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* When collapsed the content area has no height — it's clipped to the header. */
|
|
96
|
+
.a1-bottom-sheet--collapsed .a1-bottom-sheet__content {
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ── Spacer: reserves the collapsed footprint in document flow ─────────────── */
|
|
101
|
+
|
|
102
|
+
.a1-bottom-sheet__spacer {
|
|
103
|
+
block-size: var(--component-bottom-sheet-header-height, 3.5rem);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ── xs + sm only ──────────────────────────────────────────────────────────── */
|
|
107
|
+
|
|
108
|
+
@media (--bp-md-up) {
|
|
109
|
+
.a1-bottom-sheet,
|
|
110
|
+
.a1-bottom-sheet__spacer {
|
|
111
|
+
display: none;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -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);
|
|
@@ -77,6 +77,7 @@ export function Code({
|
|
|
77
77
|
|
|
78
78
|
const copyLabel = useLabel("code.copyCode", "Copy code");
|
|
79
79
|
const copiedLabel = useLabel("code.copied", "Copied");
|
|
80
|
+
const editLabel = useLabel("code.editCode", "Edit code");
|
|
80
81
|
const textToCopy = useMemo(
|
|
81
82
|
() => copyText || (editable ? editableValue : textFromChildren(Children.toArray(children))),
|
|
82
83
|
[children, copyText, editable, editableValue],
|
|
@@ -113,6 +114,10 @@ export function Code({
|
|
|
113
114
|
]
|
|
114
115
|
.filter(Boolean)
|
|
115
116
|
.join(" ");
|
|
117
|
+
const editableProps = {
|
|
118
|
+
...props,
|
|
119
|
+
...(!props["aria-label"] && !props["aria-labelledby"] ? { "aria-label": editLabel } : null),
|
|
120
|
+
};
|
|
116
121
|
|
|
117
122
|
if (!shouldRenderBlock) {
|
|
118
123
|
return (
|
|
@@ -145,7 +150,7 @@ export function Code({
|
|
|
145
150
|
value={editableValue}
|
|
146
151
|
onChange={handleTextareaChange}
|
|
147
152
|
spellCheck={false}
|
|
148
|
-
{...
|
|
153
|
+
{...editableProps}
|
|
149
154
|
/>
|
|
150
155
|
) : (
|
|
151
156
|
<pre className="a1-code-block__pre">
|
|
@@ -12,7 +12,7 @@ import "./data-table.css";
|
|
|
12
12
|
* columns: Array<{
|
|
13
13
|
* key: string,
|
|
14
14
|
* label: string,
|
|
15
|
-
* type?: "text" | "number" | "currency" | "date" | "badge" | "avatar" | "link" | "actions",
|
|
15
|
+
* type?: "text" | "number" | "currency" | "date" | "badge" | "avatar" | "image" | "link" | "actions",
|
|
16
16
|
* align?: "start" | "center" | "end",
|
|
17
17
|
* width?: string,
|
|
18
18
|
* sortable?: boolean,
|
|
@@ -29,6 +29,7 @@ import "./data-table.css";
|
|
|
29
29
|
// Estimated minimum content width per column type at a "neutral" padding level
|
|
30
30
|
const COL_BASE_WIDTH = {
|
|
31
31
|
avatar: 160, // avatar circle + name text
|
|
32
|
+
image: 72, // small thumbnail
|
|
32
33
|
date: 110, // "Jan 12, 2026"
|
|
33
34
|
actions: 120, // one or two compact buttons
|
|
34
35
|
link: 120, // linked text
|
|
@@ -455,6 +456,15 @@ export function DataTable({
|
|
|
455
456
|
);
|
|
456
457
|
}
|
|
457
458
|
|
|
459
|
+
case "image": {
|
|
460
|
+
// value is an image URL, or `{ src, alt }`.
|
|
461
|
+
const src = value && typeof value === "object" ? value.src : value;
|
|
462
|
+
const alt = value && typeof value === "object" ? (value.alt ?? "") : "";
|
|
463
|
+
return src
|
|
464
|
+
? <img className="a1-data-table__thumb" src={src} alt={alt} loading="lazy" />
|
|
465
|
+
: <span className="a1-data-table__thumb a1-data-table__thumb--empty" aria-hidden="true" />;
|
|
466
|
+
}
|
|
467
|
+
|
|
458
468
|
case "badge": {
|
|
459
469
|
const status = col.statusMap?.[value] ?? "neutral";
|
|
460
470
|
const compact = activeDensity === "compact";
|
|
@@ -286,6 +286,25 @@
|
|
|
286
286
|
font-size: var(--component-notification-font-size);
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
/* Image (thumbnail) cell type. */
|
|
290
|
+
.a1-data-table__thumb {
|
|
291
|
+
display: block;
|
|
292
|
+
inline-size: 2.5rem;
|
|
293
|
+
block-size: 2.5rem;
|
|
294
|
+
object-fit: cover;
|
|
295
|
+
border-radius: var(--base-radius-sm);
|
|
296
|
+
background: var(--semantic-color-surface-raised);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.a1-data-table--compact .a1-data-table__thumb {
|
|
300
|
+
inline-size: 2rem;
|
|
301
|
+
block-size: 2rem;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.a1-data-table__thumb--empty {
|
|
305
|
+
border: var(--component-divider-size-sm) dashed var(--semantic-color-border-subtle);
|
|
306
|
+
}
|
|
307
|
+
|
|
289
308
|
.a1-data-table__actions {
|
|
290
309
|
display: inline-flex;
|
|
291
310
|
align-items: center;
|
|
@@ -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. */
|
|
@@ -28,6 +54,13 @@ export interface FigureProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
28
54
|
* Pass `true` for symmetric bleed or a numeric spacing token for inline-only.
|
|
29
55
|
*/
|
|
30
56
|
bleed?: boolean | SpacingToken;
|
|
57
|
+
/**
|
|
58
|
+
* Show a tokenized placeholder pattern when `src` is missing or fails to load
|
|
59
|
+
* (e.g. a deleted image). Default: true. Set false to render the bare `<img>`.
|
|
60
|
+
*/
|
|
61
|
+
placeholder?: boolean;
|
|
62
|
+
/** Material Symbols icon shown in the placeholder. Default: "image" */
|
|
63
|
+
placeholderIcon?: string;
|
|
31
64
|
/** Extra class names on the `<figure>` element */
|
|
32
65
|
className?: string;
|
|
33
66
|
/** Extra class names on the `<img>` element */
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import "./figure.css";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
2
3
|
import { Bleed } from "../bleed/Bleed.jsx";
|
|
4
|
+
import { Icon } from "../icon/Icon.jsx";
|
|
5
|
+
|
|
6
|
+
function isCropRect(rect) {
|
|
7
|
+
return (
|
|
8
|
+
rect != null &&
|
|
9
|
+
typeof rect === "object" &&
|
|
10
|
+
["x", "y", "width", "height"].every((k) => typeof rect[k] === "number") &&
|
|
11
|
+
rect.width > 0 &&
|
|
12
|
+
rect.height > 0
|
|
13
|
+
);
|
|
14
|
+
}
|
|
3
15
|
|
|
4
16
|
const rounded = ["none", "sm", "md", "lg"];
|
|
5
17
|
const captionPositions = ["start", "center"];
|
|
6
18
|
const spacings = ["sm", "md", "lg"];
|
|
7
|
-
const sizes = ["xs", "sm", "md", "lg"];
|
|
8
|
-
const alignments = ["start", "center", "end"];
|
|
19
|
+
const sizes = ["3xs", "2xs", "xs", "sm", "md", "lg", "xl", "xxl"];
|
|
20
|
+
const alignments = ["none", "start", "center", "end"];
|
|
21
|
+
const aspectRatios = ["16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16", "21:9"];
|
|
22
|
+
const crops = ["center", "top", "bottom", "left", "right", "top-left", "top-right", "bottom-left", "bottom-right"];
|
|
9
23
|
|
|
10
24
|
export function Figure({
|
|
11
25
|
src,
|
|
@@ -16,21 +30,40 @@ export function Figure({
|
|
|
16
30
|
radius,
|
|
17
31
|
size,
|
|
18
32
|
align,
|
|
33
|
+
aspectRatio,
|
|
34
|
+
crop,
|
|
35
|
+
cropRect,
|
|
19
36
|
marginTop,
|
|
20
37
|
marginBottom,
|
|
21
38
|
bleed,
|
|
39
|
+
placeholder = true,
|
|
40
|
+
placeholderIcon = "image",
|
|
22
41
|
className = "",
|
|
23
42
|
imgClassName = "",
|
|
24
43
|
style,
|
|
25
44
|
imgStyle,
|
|
26
45
|
...props
|
|
27
46
|
}) {
|
|
47
|
+
// Freeform crop: a sub-rectangle of the image (fractions 0–1) shown
|
|
48
|
+
// non-destructively. We measure the natural ratio to size the crop box.
|
|
49
|
+
const cropped = isCropRect(cropRect);
|
|
50
|
+
const [naturalRatio, setNaturalRatio] = useState(null);
|
|
51
|
+
|
|
52
|
+
// Show a tokenized placeholder pattern when there's no source or it fails to
|
|
53
|
+
// load (e.g. a deleted library image). Reset the error flag when src changes.
|
|
54
|
+
const [errored, setErrored] = useState(false);
|
|
55
|
+
useEffect(() => { setErrored(false); }, [src]);
|
|
56
|
+
const showPlaceholder = placeholder && (!src || errored);
|
|
57
|
+
|
|
28
58
|
const classes = [
|
|
29
59
|
"a1-figure",
|
|
30
60
|
radius != null && rounded.includes(radius) && `a1-figure--rounded-${radius}`,
|
|
31
61
|
captionPositions.includes(captionPosition) && captionPosition !== "start" && `a1-figure--caption-${captionPosition}`,
|
|
32
62
|
sizes.includes(size) && `a1-figure--${size}`,
|
|
33
|
-
alignments.includes(align) && align !== "
|
|
63
|
+
alignments.includes(align) && align !== "none" && `a1-figure--align-${align}`,
|
|
64
|
+
!cropped && aspectRatios.includes(aspectRatio) && `a1-figure--ratio-${aspectRatio.replace(":", "-")}`,
|
|
65
|
+
!cropped && crops.includes(crop) && crop !== "center" && `a1-figure--crop-${crop}`,
|
|
66
|
+
cropped && "a1-figure--cropped",
|
|
34
67
|
spacings.includes(marginTop) && `a1-figure--mt-${marginTop}`,
|
|
35
68
|
spacings.includes(marginBottom) && `a1-figure--mb-${marginBottom}`,
|
|
36
69
|
className,
|
|
@@ -40,14 +73,50 @@ export function Figure({
|
|
|
40
73
|
captionSrOnly ? "a1-sr-only" : "a1-figure__caption",
|
|
41
74
|
].join(" ");
|
|
42
75
|
|
|
76
|
+
const img = (
|
|
77
|
+
<img
|
|
78
|
+
src={src}
|
|
79
|
+
alt={alt}
|
|
80
|
+
className={["a1-figure__img", imgClassName].filter(Boolean).join(" ")}
|
|
81
|
+
style={imgStyle}
|
|
82
|
+
onError={() => setErrored(true)}
|
|
83
|
+
onLoad={cropped ? (e) => {
|
|
84
|
+
const { naturalWidth, naturalHeight } = e.currentTarget;
|
|
85
|
+
if (naturalWidth && naturalHeight) setNaturalRatio(naturalWidth / naturalHeight);
|
|
86
|
+
} : undefined}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const placeholderEl = (
|
|
91
|
+
<div
|
|
92
|
+
className={["a1-figure__img", "a1-figure__placeholder", imgClassName].filter(Boolean).join(" ")}
|
|
93
|
+
style={imgStyle}
|
|
94
|
+
role="img"
|
|
95
|
+
aria-label={alt || undefined}
|
|
96
|
+
>
|
|
97
|
+
<Icon name={placeholderIcon} className="a1-figure__placeholder-icon" aria-hidden="true" />
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const media = showPlaceholder ? placeholderEl : cropped ? (
|
|
102
|
+
<div
|
|
103
|
+
className="a1-figure__crop"
|
|
104
|
+
style={{
|
|
105
|
+
"--a1-figure-crop-x": cropRect.x,
|
|
106
|
+
"--a1-figure-crop-y": cropRect.y,
|
|
107
|
+
"--a1-figure-crop-w": cropRect.width,
|
|
108
|
+
"--a1-figure-crop-h": cropRect.height,
|
|
109
|
+
// Box aspect ratio = (cropW · imgW) / (cropH · imgH).
|
|
110
|
+
...(naturalRatio ? { aspectRatio: `${(cropRect.width / cropRect.height) * naturalRatio}` } : {}),
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{img}
|
|
114
|
+
</div>
|
|
115
|
+
) : img;
|
|
116
|
+
|
|
43
117
|
const figure = (
|
|
44
118
|
<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
|
-
/>
|
|
119
|
+
{media}
|
|
51
120
|
{caption && (
|
|
52
121
|
<figcaption className={captionClasses}>{caption}</figcaption>
|
|
53
122
|
)}
|
|
@@ -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,8 +131,64 @@
|
|
|
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 {
|
|
96
168
|
text-align: center;
|
|
97
169
|
}
|
|
170
|
+
|
|
171
|
+
/* ─── Placeholder ─────────────────────────────────────────────────────────────
|
|
172
|
+
Shown when the image is missing or fails to load: a tokenized diagonal-stripe
|
|
173
|
+
pattern with a centered icon. Shares .a1-figure__img sizing, so aspect-ratio
|
|
174
|
+
and size variants apply; a 4/3 fallback box is used when no ratio is set. */
|
|
175
|
+
.a1-figure__placeholder {
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
justify-content: center;
|
|
179
|
+
aspect-ratio: 4 / 3;
|
|
180
|
+
background-color: var(--semantic-color-surface-raised);
|
|
181
|
+
background-image: repeating-linear-gradient(
|
|
182
|
+
45deg,
|
|
183
|
+
var(--semantic-color-border-subtle) 0,
|
|
184
|
+
var(--semantic-color-border-subtle) 1px,
|
|
185
|
+
transparent 1px,
|
|
186
|
+
transparent 10px
|
|
187
|
+
);
|
|
188
|
+
color: var(--semantic-color-text-muted);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.a1-figure__placeholder-icon {
|
|
192
|
+
font-size: var(--base-spacing-48);
|
|
193
|
+
opacity: 0.6;
|
|
194
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
3
|
type Breakpoints = "xs" | "sm" | "md" | "lg" | "xl";
|
|
4
|
-
type GapKey = "xs" | "sm" | "md" | "lg" | "xl" | "xxl" | 1 | 2 | 4 | 6 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 64 | 96 | 128;
|
|
4
|
+
type GapKey = "none" | "xs" | "sm" | "md" | "lg" | "xl" | "xxl" | 1 | 2 | 4 | 6 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 64 | 96 | 128;
|
|
5
5
|
type ColSpan = number | "full";
|
|
6
6
|
|
|
7
7
|
export interface GridProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -14,6 +14,8 @@ const breakpoints = ["xs", "sm", "md", "lg", "xl"];
|
|
|
14
14
|
|
|
15
15
|
function resolveGap(key) {
|
|
16
16
|
if (key == null) return undefined;
|
|
17
|
+
// "none" removes the gap (matches the shared resolveSpacing convention).
|
|
18
|
+
if (key === "none") return "0";
|
|
17
19
|
if (gapSizes[key]) return gapSizes[key];
|
|
18
20
|
|
|
19
21
|
const n = Number(key);
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
.a1-grid--xs-3 { --a1-grid-cols: 3; }
|
|
35
35
|
.a1-grid--xs-4 { --a1-grid-cols: 4; }
|
|
36
36
|
.a1-grid--xs-6 { --a1-grid-cols: 6; }
|
|
37
|
+
.a1-grid--xs-8 { --a1-grid-cols: 8; }
|
|
37
38
|
.a1-grid--xs-12 { --a1-grid-cols: 12; }
|
|
38
39
|
|
|
39
40
|
@media (--bp-sm-up) {
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
.a1-grid--sm-3 { --a1-grid-cols: 3; }
|
|
43
44
|
.a1-grid--sm-4 { --a1-grid-cols: 4; }
|
|
44
45
|
.a1-grid--sm-6 { --a1-grid-cols: 6; }
|
|
46
|
+
.a1-grid--sm-8 { --a1-grid-cols: 8; }
|
|
45
47
|
.a1-grid--sm-12 { --a1-grid-cols: 12; }
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -51,6 +53,7 @@
|
|
|
51
53
|
.a1-grid--md-3 { --a1-grid-cols: 3; }
|
|
52
54
|
.a1-grid--md-4 { --a1-grid-cols: 4; }
|
|
53
55
|
.a1-grid--md-6 { --a1-grid-cols: 6; }
|
|
56
|
+
.a1-grid--md-8 { --a1-grid-cols: 8; }
|
|
54
57
|
.a1-grid--md-12 { --a1-grid-cols: 12; }
|
|
55
58
|
}
|
|
56
59
|
|
|
@@ -60,6 +63,7 @@
|
|
|
60
63
|
.a1-grid--lg-3 { --a1-grid-cols: 3; }
|
|
61
64
|
.a1-grid--lg-4 { --a1-grid-cols: 4; }
|
|
62
65
|
.a1-grid--lg-6 { --a1-grid-cols: 6; }
|
|
66
|
+
.a1-grid--lg-8 { --a1-grid-cols: 8; }
|
|
63
67
|
.a1-grid--lg-12 { --a1-grid-cols: 12; }
|
|
64
68
|
}
|
|
65
69
|
|
|
@@ -69,6 +73,7 @@
|
|
|
69
73
|
.a1-grid--xl-3 { --a1-grid-cols: 3; }
|
|
70
74
|
.a1-grid--xl-4 { --a1-grid-cols: 4; }
|
|
71
75
|
.a1-grid--xl-6 { --a1-grid-cols: 6; }
|
|
76
|
+
.a1-grid--xl-8 { --a1-grid-cols: 8; }
|
|
72
77
|
.a1-grid--xl-12 { --a1-grid-cols: 12; }
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -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
|
|