@gtivr4/a1-design-system-react 0.15.0 → 0.19.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.css +6 -0
- 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/code/Code.d.ts +4 -0
- package/src/components/code/Code.jsx +45 -2
- package/src/components/code/code.css +23 -0
- 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 +7 -0
- package/src/components/figure/Figure.jsx +23 -2
- package/src/components/figure/figure.css +25 -0
- package/src/components/grid/Grid.d.ts +3 -1
- package/src/components/grid/Grid.jsx +10 -0
- package/src/components/grid/grid.css +11 -0
- 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/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.css +10 -2
- package/src/components/split-button/SplitButton.jsx +3 -1
- package/src/components/tabs/tabs.css +3 -0
- package/src/components/toolbar/Toolbar.d.ts +7 -0
- package/src/components/toolbar/Toolbar.jsx +13 -5
- 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 +2 -0
- package/src/themes.css +293 -0
- package/src/tokens.css +22 -1
|
@@ -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
|
+
}
|
|
@@ -13,6 +13,10 @@ export interface CodeProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
13
13
|
editable?: boolean;
|
|
14
14
|
/** Called with the current string value whenever the editable textarea changes. */
|
|
15
15
|
onChangeValue?: (value: string) => void;
|
|
16
|
+
/** Cap a long read-only block to `collapsedLines` with a fade + Show more/less toggle (the toggle appears only when the content overflows). Block, non-editable only. Default: false */
|
|
17
|
+
collapsible?: boolean;
|
|
18
|
+
/** Approximate number of lines shown when collapsed. Default: 14 */
|
|
19
|
+
collapsedLines?: number;
|
|
16
20
|
children?: React.ReactNode;
|
|
17
21
|
}
|
|
18
22
|
|
|
@@ -56,12 +56,17 @@ export function Code({
|
|
|
56
56
|
copyText,
|
|
57
57
|
editable = false,
|
|
58
58
|
onChangeValue,
|
|
59
|
+
collapsible = false,
|
|
60
|
+
collapsedLines = 14,
|
|
59
61
|
className = "",
|
|
60
62
|
children,
|
|
61
63
|
...props
|
|
62
64
|
}) {
|
|
63
65
|
const resolvedVariant = variants.includes(variant) ? variant : "inline";
|
|
64
66
|
const [copied, setCopied] = useState(false);
|
|
67
|
+
const [expanded, setExpanded] = useState(false);
|
|
68
|
+
const [overflows, setOverflows] = useState(false);
|
|
69
|
+
const preRef = useRef(null);
|
|
65
70
|
const [editableValue, setEditableValue] = useState(() =>
|
|
66
71
|
textFromChildren(Children.toArray(children))
|
|
67
72
|
);
|
|
@@ -77,11 +82,16 @@ export function Code({
|
|
|
77
82
|
|
|
78
83
|
const copyLabel = useLabel("code.copyCode", "Copy code");
|
|
79
84
|
const copiedLabel = useLabel("code.copied", "Copied");
|
|
85
|
+
const editLabel = useLabel("code.editCode", "Edit code");
|
|
86
|
+
const showMoreLabel = useLabel("code.showMore", "Show more");
|
|
87
|
+
const showLessLabel = useLabel("code.showLess", "Show less");
|
|
80
88
|
const textToCopy = useMemo(
|
|
81
89
|
() => copyText || (editable ? editableValue : textFromChildren(Children.toArray(children))),
|
|
82
90
|
[children, copyText, editable, editableValue],
|
|
83
91
|
);
|
|
84
92
|
const shouldRenderBlock = resolvedVariant === "block" || copyCode || editable;
|
|
93
|
+
// Collapsible only applies to a read-only block (not the editable textarea).
|
|
94
|
+
const collapses = collapsible && !editable && shouldRenderBlock;
|
|
85
95
|
|
|
86
96
|
useEffect(() => {
|
|
87
97
|
return () => {
|
|
@@ -89,6 +99,16 @@ export function Code({
|
|
|
89
99
|
};
|
|
90
100
|
}, []);
|
|
91
101
|
|
|
102
|
+
// Detect whether the (collapsed) content actually overflows the cap, so the
|
|
103
|
+
// toggle only appears when it's needed. scrollHeight reports the full content
|
|
104
|
+
// height even while clipped, so this is accurate in either state.
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!collapses) { setOverflows(false); return; }
|
|
107
|
+
if (expanded) return; // measured while collapsed; keep so "Show less" stays
|
|
108
|
+
const el = preRef.current;
|
|
109
|
+
if (el) setOverflows(el.scrollHeight - el.clientHeight > 4);
|
|
110
|
+
}, [collapses, expanded, children, collapsedLines]);
|
|
111
|
+
|
|
92
112
|
function handleTextareaChange(e) {
|
|
93
113
|
setEditableValue(e.target.value);
|
|
94
114
|
onChangeValue?.(e.target.value);
|
|
@@ -113,6 +133,10 @@ export function Code({
|
|
|
113
133
|
]
|
|
114
134
|
.filter(Boolean)
|
|
115
135
|
.join(" ");
|
|
136
|
+
const editableProps = {
|
|
137
|
+
...props,
|
|
138
|
+
...(!props["aria-label"] && !props["aria-labelledby"] ? { "aria-label": editLabel } : null),
|
|
139
|
+
};
|
|
116
140
|
|
|
117
141
|
if (!shouldRenderBlock) {
|
|
118
142
|
return (
|
|
@@ -122,12 +146,15 @@ export function Code({
|
|
|
122
146
|
);
|
|
123
147
|
}
|
|
124
148
|
|
|
149
|
+
const collapsed = collapses && overflows && !expanded;
|
|
150
|
+
|
|
125
151
|
return (
|
|
126
152
|
<div
|
|
127
153
|
className={[
|
|
128
154
|
"a1-code-block",
|
|
129
155
|
copyCode && "a1-code-block--copyable",
|
|
130
156
|
editable && "a1-code-block--editable",
|
|
157
|
+
collapsed && "a1-code-block--collapsed",
|
|
131
158
|
className,
|
|
132
159
|
]
|
|
133
160
|
.filter(Boolean)
|
|
@@ -145,10 +172,14 @@ export function Code({
|
|
|
145
172
|
value={editableValue}
|
|
146
173
|
onChange={handleTextareaChange}
|
|
147
174
|
spellCheck={false}
|
|
148
|
-
{...
|
|
175
|
+
{...editableProps}
|
|
149
176
|
/>
|
|
150
177
|
) : (
|
|
151
|
-
<pre
|
|
178
|
+
<pre
|
|
179
|
+
ref={preRef}
|
|
180
|
+
className="a1-code-block__pre"
|
|
181
|
+
style={collapses ? { "--a1-code-collapsed-max": `${collapsedLines * 1.6}em` } : undefined}
|
|
182
|
+
>
|
|
152
183
|
<code className={codeClasses} {...props}>
|
|
153
184
|
{children}
|
|
154
185
|
</code>
|
|
@@ -166,6 +197,18 @@ export function Code({
|
|
|
166
197
|
{copied ? copiedLabel : copyLabel}
|
|
167
198
|
</Button>
|
|
168
199
|
)}
|
|
200
|
+
{collapses && overflows && (
|
|
201
|
+
<Button
|
|
202
|
+
className="a1-code-block__toggle"
|
|
203
|
+
icon={expanded ? "expand_less" : "expand_more"}
|
|
204
|
+
size="sm"
|
|
205
|
+
variant="tertiary"
|
|
206
|
+
onClick={() => setExpanded((v) => !v)}
|
|
207
|
+
type="button"
|
|
208
|
+
>
|
|
209
|
+
{expanded ? showLessLabel : showMoreLabel}
|
|
210
|
+
</Button>
|
|
211
|
+
)}
|
|
169
212
|
</div>
|
|
170
213
|
);
|
|
171
214
|
}
|
|
@@ -87,3 +87,26 @@
|
|
|
87
87
|
.a1-code-block__copy {
|
|
88
88
|
margin: 0;
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
/* Collapsible block: cap the height with a fade and an Expand/Collapse toggle. */
|
|
92
|
+
.a1-code-block--collapsed .a1-code-block__pre {
|
|
93
|
+
position: relative;
|
|
94
|
+
max-block-size: var(--a1-code-collapsed-max, 22rem);
|
|
95
|
+
overflow-y: hidden;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.a1-code-block--collapsed .a1-code-block__pre::after {
|
|
99
|
+
content: "";
|
|
100
|
+
position: absolute;
|
|
101
|
+
inset-inline: 0;
|
|
102
|
+
inset-block-end: 0;
|
|
103
|
+
block-size: var(--base-spacing-48, 3rem);
|
|
104
|
+
background: linear-gradient(to top, var(--semantic-color-surface-panel), transparent);
|
|
105
|
+
pointer-events: none;
|
|
106
|
+
border-end-start-radius: var(--base-radius-md);
|
|
107
|
+
border-end-end-radius: var(--base-radius-md);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.a1-code-block__toggle {
|
|
111
|
+
margin: 0;
|
|
112
|
+
}
|
|
@@ -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;
|
|
@@ -54,6 +54,13 @@ export interface FigureProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
54
54
|
* Pass `true` for symmetric bleed or a numeric spacing token for inline-only.
|
|
55
55
|
*/
|
|
56
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;
|
|
57
64
|
/** Extra class names on the `<figure>` element */
|
|
58
65
|
className?: string;
|
|
59
66
|
/** Extra class names on the `<img>` element */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "./figure.css";
|
|
2
|
-
import { useState } from "react";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
3
|
import { Bleed } from "../bleed/Bleed.jsx";
|
|
4
|
+
import { Icon } from "../icon/Icon.jsx";
|
|
4
5
|
|
|
5
6
|
function isCropRect(rect) {
|
|
6
7
|
return (
|
|
@@ -35,6 +36,8 @@ export function Figure({
|
|
|
35
36
|
marginTop,
|
|
36
37
|
marginBottom,
|
|
37
38
|
bleed,
|
|
39
|
+
placeholder = true,
|
|
40
|
+
placeholderIcon = "image",
|
|
38
41
|
className = "",
|
|
39
42
|
imgClassName = "",
|
|
40
43
|
style,
|
|
@@ -46,6 +49,12 @@ export function Figure({
|
|
|
46
49
|
const cropped = isCropRect(cropRect);
|
|
47
50
|
const [naturalRatio, setNaturalRatio] = useState(null);
|
|
48
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
|
+
|
|
49
58
|
const classes = [
|
|
50
59
|
"a1-figure",
|
|
51
60
|
radius != null && rounded.includes(radius) && `a1-figure--rounded-${radius}`,
|
|
@@ -70,6 +79,7 @@ export function Figure({
|
|
|
70
79
|
alt={alt}
|
|
71
80
|
className={["a1-figure__img", imgClassName].filter(Boolean).join(" ")}
|
|
72
81
|
style={imgStyle}
|
|
82
|
+
onError={() => setErrored(true)}
|
|
73
83
|
onLoad={cropped ? (e) => {
|
|
74
84
|
const { naturalWidth, naturalHeight } = e.currentTarget;
|
|
75
85
|
if (naturalWidth && naturalHeight) setNaturalRatio(naturalWidth / naturalHeight);
|
|
@@ -77,7 +87,18 @@ export function Figure({
|
|
|
77
87
|
/>
|
|
78
88
|
);
|
|
79
89
|
|
|
80
|
-
const
|
|
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 ? (
|
|
81
102
|
<div
|
|
82
103
|
className="a1-figure__crop"
|
|
83
104
|
style={{
|
|
@@ -167,3 +167,28 @@
|
|
|
167
167
|
.a1-figure--caption-center .a1-figure__caption {
|
|
168
168
|
text-align: center;
|
|
169
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> {
|
|
@@ -20,6 +20,8 @@ export interface GridProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
20
20
|
layout?: "default" | "bento";
|
|
21
21
|
/** CSS value for `grid-auto-rows` */
|
|
22
22
|
autoRows?: string;
|
|
23
|
+
/** Cross-axis (vertical) alignment of items within their row. Omit to inherit the grid default ("stretch" = equal-height items filling the row height). */
|
|
24
|
+
alignItems?: "start" | "center" | "end" | "stretch";
|
|
23
25
|
children?: React.ReactNode;
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -11,9 +11,12 @@ const gapSizes = {
|
|
|
11
11
|
};
|
|
12
12
|
const layouts = ["default", "bento"];
|
|
13
13
|
const breakpoints = ["xs", "sm", "md", "lg", "xl"];
|
|
14
|
+
const alignments = ["start", "center", "end", "stretch"];
|
|
14
15
|
|
|
15
16
|
function resolveGap(key) {
|
|
16
17
|
if (key == null) return undefined;
|
|
18
|
+
// "none" removes the gap (matches the shared resolveSpacing convention).
|
|
19
|
+
if (key === "none") return "0";
|
|
17
20
|
if (gapSizes[key]) return gapSizes[key];
|
|
18
21
|
|
|
19
22
|
const n = Number(key);
|
|
@@ -27,6 +30,7 @@ export function Grid({
|
|
|
27
30
|
columnGap,
|
|
28
31
|
layout = "default",
|
|
29
32
|
autoRows,
|
|
33
|
+
alignItems,
|
|
30
34
|
className = "",
|
|
31
35
|
children,
|
|
32
36
|
...props
|
|
@@ -38,6 +42,12 @@ export function Grid({
|
|
|
38
42
|
classes.push(`a1-grid--${resolvedLayout}`);
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
// Cross-axis (vertical) alignment of items in their row. Omit to inherit the
|
|
46
|
+
// grid default (stretch = equal-height items filling the row).
|
|
47
|
+
if (alignments.includes(alignItems)) {
|
|
48
|
+
classes.push(`a1-grid--align-${alignItems}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
let inlineCols;
|
|
42
52
|
if (typeof columns === "number") {
|
|
43
53
|
inlineCols = columns;
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
align-items: stretch;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/* Cross-axis (vertical) alignment of items within their row. */
|
|
13
|
+
.a1-grid--align-start { align-items: start; }
|
|
14
|
+
.a1-grid--align-center { align-items: center; }
|
|
15
|
+
.a1-grid--align-end { align-items: end; }
|
|
16
|
+
.a1-grid--align-stretch { align-items: stretch; }
|
|
17
|
+
|
|
12
18
|
.a1-grid--bento > .a1-grid-item {
|
|
13
19
|
min-height: 0;
|
|
14
20
|
}
|
|
@@ -34,6 +40,7 @@
|
|
|
34
40
|
.a1-grid--xs-3 { --a1-grid-cols: 3; }
|
|
35
41
|
.a1-grid--xs-4 { --a1-grid-cols: 4; }
|
|
36
42
|
.a1-grid--xs-6 { --a1-grid-cols: 6; }
|
|
43
|
+
.a1-grid--xs-8 { --a1-grid-cols: 8; }
|
|
37
44
|
.a1-grid--xs-12 { --a1-grid-cols: 12; }
|
|
38
45
|
|
|
39
46
|
@media (--bp-sm-up) {
|
|
@@ -42,6 +49,7 @@
|
|
|
42
49
|
.a1-grid--sm-3 { --a1-grid-cols: 3; }
|
|
43
50
|
.a1-grid--sm-4 { --a1-grid-cols: 4; }
|
|
44
51
|
.a1-grid--sm-6 { --a1-grid-cols: 6; }
|
|
52
|
+
.a1-grid--sm-8 { --a1-grid-cols: 8; }
|
|
45
53
|
.a1-grid--sm-12 { --a1-grid-cols: 12; }
|
|
46
54
|
}
|
|
47
55
|
|
|
@@ -51,6 +59,7 @@
|
|
|
51
59
|
.a1-grid--md-3 { --a1-grid-cols: 3; }
|
|
52
60
|
.a1-grid--md-4 { --a1-grid-cols: 4; }
|
|
53
61
|
.a1-grid--md-6 { --a1-grid-cols: 6; }
|
|
62
|
+
.a1-grid--md-8 { --a1-grid-cols: 8; }
|
|
54
63
|
.a1-grid--md-12 { --a1-grid-cols: 12; }
|
|
55
64
|
}
|
|
56
65
|
|
|
@@ -60,6 +69,7 @@
|
|
|
60
69
|
.a1-grid--lg-3 { --a1-grid-cols: 3; }
|
|
61
70
|
.a1-grid--lg-4 { --a1-grid-cols: 4; }
|
|
62
71
|
.a1-grid--lg-6 { --a1-grid-cols: 6; }
|
|
72
|
+
.a1-grid--lg-8 { --a1-grid-cols: 8; }
|
|
63
73
|
.a1-grid--lg-12 { --a1-grid-cols: 12; }
|
|
64
74
|
}
|
|
65
75
|
|
|
@@ -69,6 +79,7 @@
|
|
|
69
79
|
.a1-grid--xl-3 { --a1-grid-cols: 3; }
|
|
70
80
|
.a1-grid--xl-4 { --a1-grid-cols: 4; }
|
|
71
81
|
.a1-grid--xl-6 { --a1-grid-cols: 6; }
|
|
82
|
+
.a1-grid--xl-8 { --a1-grid-cols: 8; }
|
|
72
83
|
.a1-grid--xl-12 { --a1-grid-cols: 12; }
|
|
73
84
|
}
|
|
74
85
|
|
|
@@ -117,10 +117,16 @@
|
|
|
117
117
|
overflow-y: hidden;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
/* Only at lg+ is the sidebar persistent and in flow; pin the SideNav to fill the
|
|
121
|
+
sidebar height so its footer stays anchored. At md and below the SideNav must
|
|
122
|
+
keep its fixed-overlay behaviour (see side-nav.css) so the sidebar slot
|
|
123
|
+
collapses and the main column expands to fill the freed space. */
|
|
124
|
+
@media (--bp-lg-up) {
|
|
125
|
+
.a1-page-layout--viewport-height .a1-page-layout__sidebar .a1-side-nav {
|
|
126
|
+
position: relative;
|
|
127
|
+
top: auto;
|
|
128
|
+
height: 100%;
|
|
129
|
+
}
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
.a1-page-layout--viewport-height .a1-page-layout__content {
|
|
@@ -2,6 +2,22 @@ import { useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { Card } from "../card/Card.jsx";
|
|
3
3
|
import "./page-nav.css";
|
|
4
4
|
|
|
5
|
+
// Find the nearest scrollable ancestor of a node, or null when the page scrolls
|
|
6
|
+
// on the document/window itself. Needed because the host may scroll a nested
|
|
7
|
+
// container (e.g. a viewport-height PageLayout) rather than the window — in which
|
|
8
|
+
// case window scroll never fires and the progress/active state would freeze.
|
|
9
|
+
function getScrollParent(node) {
|
|
10
|
+
let el = node?.parentElement;
|
|
11
|
+
while (el && el !== document.body && el !== document.documentElement) {
|
|
12
|
+
const overflowY = getComputedStyle(el).overflowY;
|
|
13
|
+
if ((overflowY === "auto" || overflowY === "scroll") && el.scrollHeight > el.clientHeight) {
|
|
14
|
+
return el;
|
|
15
|
+
}
|
|
16
|
+
el = el.parentElement;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
export function PageNav({
|
|
6
22
|
sections = [],
|
|
7
23
|
label = "On this page",
|
|
@@ -12,22 +28,27 @@ export function PageNav({
|
|
|
12
28
|
const [progress, setProgress] = useState(0);
|
|
13
29
|
const intersectingIds = useRef(new Set());
|
|
14
30
|
|
|
15
|
-
// Reading progress: track
|
|
31
|
+
// Reading progress: track the scroll position of the actual scroll container
|
|
32
|
+
// (the window, or a nested scrollable ancestor like a viewport-height layout).
|
|
16
33
|
useEffect(() => {
|
|
34
|
+
const scroller = getScrollParent(sections.length ? document.getElementById(sections[0].id) : null);
|
|
35
|
+
const target = scroller ?? window;
|
|
17
36
|
function update() {
|
|
18
|
-
const el = document.documentElement;
|
|
37
|
+
const el = scroller ?? document.documentElement;
|
|
19
38
|
const total = el.scrollHeight - el.clientHeight;
|
|
20
39
|
setProgress(total > 0 ? (el.scrollTop / total) * 100 : 0);
|
|
21
40
|
}
|
|
22
|
-
|
|
41
|
+
target.addEventListener("scroll", update, { passive: true });
|
|
23
42
|
update();
|
|
24
|
-
return () =>
|
|
25
|
-
}, []);
|
|
43
|
+
return () => target.removeEventListener("scroll", update);
|
|
44
|
+
}, [sections]);
|
|
26
45
|
|
|
27
|
-
// Active section: observe each section element entering/leaving the
|
|
46
|
+
// Active section: observe each section element entering/leaving the scroll
|
|
47
|
+
// container (root = the nested scroller, or the viewport when null).
|
|
28
48
|
useEffect(() => {
|
|
29
49
|
if (!sections.length) return;
|
|
30
50
|
|
|
51
|
+
const scroller = getScrollParent(document.getElementById(sections[0].id));
|
|
31
52
|
const observer = new IntersectionObserver(
|
|
32
53
|
(entries) => {
|
|
33
54
|
entries.forEach((entry) => {
|
|
@@ -43,9 +64,9 @@ export function PageNav({
|
|
|
43
64
|
if (first) setActiveId(first.id);
|
|
44
65
|
},
|
|
45
66
|
// -8% top offset keeps the active section stable once it clears the
|
|
46
|
-
// header; -88% bottom offset means only the top 12% of the
|
|
67
|
+
// header; -88% bottom offset means only the top 12% of the scroll area
|
|
47
68
|
// is treated as "current".
|
|
48
|
-
{ rootMargin: "-8% 0px -88% 0px", threshold: 0 }
|
|
69
|
+
{ root: scroller ?? null, rootMargin: "-8% 0px -88% 0px", threshold: 0 }
|
|
49
70
|
);
|
|
50
71
|
|
|
51
72
|
sections.forEach(({ id }) => {
|
|
@@ -163,3 +163,16 @@
|
|
|
163
163
|
border-color: var(--semantic-color-action-background);
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
+
|
|
167
|
+
/* ── Desktop: stick within its column as the reader scrolls ────────────────── */
|
|
168
|
+
/* `--a1-page-nav-top` lets a consumer offset for a sticky header (default 16px);
|
|
169
|
+
a long nav scrolls internally rather than running past the viewport. */
|
|
170
|
+
@media (min-width: 769px) {
|
|
171
|
+
.a1-page-nav.a1-card {
|
|
172
|
+
position: sticky;
|
|
173
|
+
top: var(--a1-page-nav-top, var(--base-spacing-16));
|
|
174
|
+
align-self: start;
|
|
175
|
+
max-block-size: calc(100vh - var(--a1-page-nav-top, var(--base-spacing-16)) - var(--base-spacing-16));
|
|
176
|
+
overflow: hidden auto;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -17,6 +17,8 @@ export interface ParagraphProps extends React.HTMLAttributes<HTMLParagraphElemen
|
|
|
17
17
|
textWrap?: "balance";
|
|
18
18
|
/** Horizontal text alignment. "start"/"end" are logical aliases for LTR/RTL-safe alignment. */
|
|
19
19
|
align?: "left" | "center" | "right" | "start" | "end";
|
|
20
|
+
/** Font weight. Omit to inherit the body default. */
|
|
21
|
+
weight?: "regular" | "medium" | "semibold" | "bold";
|
|
20
22
|
children?: React.ReactNode;
|
|
21
23
|
}
|
|
22
24
|
|