@tonycodes-ui/mortar 0.11.0 → 0.13.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/AGENTS.md +1 -1
- package/CHANGELOG.md +16 -47
- package/CONTRIBUTING.md +14 -2
- package/DESIGN.md +1 -1
- package/README.md +1 -1
- package/catalog.json +610 -377
- package/cli/audit.mjs +152 -0
- package/dist/agent-data.json +1 -1
- package/dist/application.d.ts +30 -5
- package/dist/application.js +20 -8
- package/dist/illustration.d.ts +47 -0
- package/dist/illustration.js +47 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/inspection.js +17 -1
- package/dist/internal.d.ts +39 -3
- package/dist/internal.js +74 -4
- package/dist/marketing.d.ts +33 -9
- package/dist/marketing.js +23 -14
- package/dist/primitives.js +20 -11
- package/docs/agents.md +21 -18
- package/docs/theming.md +72 -1
- package/package.json +1 -1
- package/skills/mortar/SKILL.md +1 -1
- package/src/application.css +95 -1
- package/src/application.tsx +88 -19
- package/src/data.css +58 -2
- package/src/foundation.css +10 -0
- package/src/illustration.tsx +169 -0
- package/src/index.ts +6 -0
- package/src/inspection.tsx +15 -0
- package/src/internal.tsx +180 -3
- package/src/marketing.css +181 -2
- package/src/marketing.tsx +103 -94
- package/src/primitives.tsx +38 -19
package/src/application.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { AuthHeader, ProviderButtons, type SignInProvider } from "./auth.js";
|
|
|
13
13
|
import { ArrowDown, ArrowUp, Check, SortArrows } from "./icons.js";
|
|
14
14
|
import { headingTag, type HeadingLevel } from "./heading.js";
|
|
15
15
|
import {
|
|
16
|
+
Flyout,
|
|
16
17
|
LinkContent,
|
|
17
18
|
NavGroups,
|
|
18
19
|
NavTreeLinks,
|
|
@@ -21,7 +22,9 @@ import {
|
|
|
21
22
|
cx,
|
|
22
23
|
componentRoot,
|
|
23
24
|
classedIntroSlots,
|
|
25
|
+
sectionTone,
|
|
24
26
|
useDomId,
|
|
27
|
+
type FlyoutItem,
|
|
25
28
|
} from "./internal.js";
|
|
26
29
|
/**
|
|
27
30
|
* A navigation link; `icon` replaces the default marker in the sidebar. With
|
|
@@ -189,11 +192,16 @@ export interface StackedShellProps {
|
|
|
189
192
|
brand: string;
|
|
190
193
|
logo?: ReactNode;
|
|
191
194
|
homeHref?: string;
|
|
192
|
-
|
|
195
|
+
/** Top-level links, or labelled groups that open a dropdown panel. */
|
|
196
|
+
navigation: NavbarItem[];
|
|
193
197
|
actions?: ReactNode;
|
|
194
198
|
/** Page heading shown above the content, e.g. a PageHeading. */
|
|
195
199
|
header?: ReactNode;
|
|
196
200
|
width?: "md" | "lg" | "xl" | "full";
|
|
201
|
+
/** The bar's surface: default, inverse (the other appearance) or accent (the brand's accent fill). */
|
|
202
|
+
tone?: NavbarTone;
|
|
203
|
+
/** A second row of page tabs under the bar, rendered as a TabNav named by `label`. */
|
|
204
|
+
tabs?: ItemGroup<TabNavItem>;
|
|
197
205
|
children: ReactNode;
|
|
198
206
|
className?: string;
|
|
199
207
|
}
|
|
@@ -206,6 +214,8 @@ export function StackedShell({
|
|
|
206
214
|
actions,
|
|
207
215
|
header,
|
|
208
216
|
width = "lg",
|
|
217
|
+
tone,
|
|
218
|
+
tabs,
|
|
209
219
|
children,
|
|
210
220
|
className,
|
|
211
221
|
}: StackedShellProps) {
|
|
@@ -224,7 +234,13 @@ export function StackedShell({
|
|
|
224
234
|
homeHref={homeHref}
|
|
225
235
|
items={navigation}
|
|
226
236
|
actions={actions}
|
|
237
|
+
tone={tone}
|
|
227
238
|
/>
|
|
239
|
+
{tabs && (
|
|
240
|
+
<div className="m-stacked-tabs">
|
|
241
|
+
<TabNav label={tabs.label} items={tabs.items} />
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
228
244
|
<main id={contentId} tabIndex={-1} data-width={width}>
|
|
229
245
|
{header && <div className="m-stacked-header">{header}</div>}
|
|
230
246
|
{children}
|
|
@@ -232,13 +248,20 @@ export function StackedShell({
|
|
|
232
248
|
</div>
|
|
233
249
|
);
|
|
234
250
|
}
|
|
251
|
+
/** A labelled group of links that opens a dropdown panel from the bar. */
|
|
252
|
+
export type NavbarGroup = ItemGroup<FlyoutItem>;
|
|
253
|
+
/** A top-level link, or a group; a group holding the current page is marked current. */
|
|
254
|
+
export type NavbarItem = LinkItem | NavbarGroup;
|
|
255
|
+
export type NavbarTone = "default" | "inverse" | "accent";
|
|
235
256
|
export interface NavbarProps {
|
|
236
257
|
brand: string;
|
|
237
258
|
logo?: ReactNode;
|
|
238
259
|
homeHref?: string;
|
|
239
|
-
/** Top-level links
|
|
240
|
-
items:
|
|
260
|
+
/** Top-level links, or labelled groups that open a dropdown panel. */
|
|
261
|
+
items: NavbarItem[];
|
|
241
262
|
actions?: ReactNode;
|
|
263
|
+
/** The bar's surface: default, inverse (the other appearance) or accent (the brand's accent fill). */
|
|
264
|
+
tone?: NavbarTone;
|
|
242
265
|
/** Accessible name of the navigation landmark. */
|
|
243
266
|
label?: string;
|
|
244
267
|
className?: string;
|
|
@@ -250,13 +273,19 @@ export function Navbar({
|
|
|
250
273
|
homeHref = "/",
|
|
251
274
|
items,
|
|
252
275
|
actions,
|
|
276
|
+
tone,
|
|
253
277
|
label = "Primary",
|
|
254
278
|
className,
|
|
255
279
|
}: NavbarProps) {
|
|
256
280
|
const [open, setOpen] = useState(false);
|
|
257
281
|
const id = useId();
|
|
282
|
+
const close = () => setOpen(false);
|
|
258
283
|
return (
|
|
259
|
-
<header
|
|
284
|
+
<header
|
|
285
|
+
{...componentRoot("Navbar")}
|
|
286
|
+
className={cx("m-navbar", className)}
|
|
287
|
+
data-tone={sectionTone(tone)}
|
|
288
|
+
>
|
|
260
289
|
<KitLink className="m-brand" href={homeHref}>
|
|
261
290
|
<BrandMark logo={logo} />
|
|
262
291
|
{brand}
|
|
@@ -271,21 +300,30 @@ export function Navbar({
|
|
|
271
300
|
Menu
|
|
272
301
|
</Button>
|
|
273
302
|
<nav id={id} aria-label={label} data-open={open || undefined}>
|
|
274
|
-
{items.map((item, index) =>
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
aria-current={item.current ? "page" : undefined}
|
|
279
|
-
onClick={() => setOpen(false)}
|
|
280
|
-
>
|
|
281
|
-
<LinkContent
|
|
282
|
-
prefix="nav"
|
|
283
|
-
icon={item.icon}
|
|
303
|
+
{items.map((item, index) =>
|
|
304
|
+
"items" in item ? (
|
|
305
|
+
<Flyout
|
|
306
|
+
key={`${item.label}:group:${index}`}
|
|
284
307
|
label={item.label}
|
|
285
|
-
|
|
308
|
+
items={item.items}
|
|
309
|
+
onNavigate={close}
|
|
286
310
|
/>
|
|
287
|
-
|
|
288
|
-
|
|
311
|
+
) : (
|
|
312
|
+
<KitLink
|
|
313
|
+
key={`${item.label}:${item.href}:${index}`}
|
|
314
|
+
href={item.href}
|
|
315
|
+
aria-current={item.current ? "page" : undefined}
|
|
316
|
+
onClick={close}
|
|
317
|
+
>
|
|
318
|
+
<LinkContent
|
|
319
|
+
prefix="nav"
|
|
320
|
+
icon={item.icon}
|
|
321
|
+
label={item.label}
|
|
322
|
+
count={item.count}
|
|
323
|
+
/>
|
|
324
|
+
</KitLink>
|
|
325
|
+
),
|
|
326
|
+
)}
|
|
289
327
|
</nav>
|
|
290
328
|
{actions && <div className="m-actions">{actions}</div>}
|
|
291
329
|
</header>
|
|
@@ -689,6 +727,28 @@ export interface DataTableColumn<T> {
|
|
|
689
727
|
width?: string;
|
|
690
728
|
/** Clip long values to one line with an ellipsis. */
|
|
691
729
|
truncate?: boolean;
|
|
730
|
+
/**
|
|
731
|
+
* How long the column stays as the table narrows (its own width, not the
|
|
732
|
+
* screen's): `medium` hides below 480px, `low` below 768px. The first
|
|
733
|
+
* column always shows. Hidden columns leave the accessibility tree too, so
|
|
734
|
+
* keep what people need in visible columns or on the detail page.
|
|
735
|
+
*/
|
|
736
|
+
priority?: "high" | "medium" | "low";
|
|
737
|
+
/**
|
|
738
|
+
* `end` pins the column to the trailing edge while the table scrolls
|
|
739
|
+
* sideways, so a row-actions or status column stays reachable.
|
|
740
|
+
*/
|
|
741
|
+
pin?: "end";
|
|
742
|
+
}
|
|
743
|
+
/** Responsive attributes shared by a column's `col`, header and cells. */
|
|
744
|
+
function columnLayout<T>(col: DataTableColumn<T>, index: number) {
|
|
745
|
+
return {
|
|
746
|
+
"data-priority":
|
|
747
|
+
index > 0 && col.priority && col.priority !== "high"
|
|
748
|
+
? col.priority
|
|
749
|
+
: undefined,
|
|
750
|
+
"data-pin": col.pin,
|
|
751
|
+
};
|
|
692
752
|
}
|
|
693
753
|
export interface DataTableProps<T extends { id: string }> {
|
|
694
754
|
caption: string;
|
|
@@ -894,6 +954,7 @@ export function DataTable<T extends { id: string }>({
|
|
|
894
954
|
<th
|
|
895
955
|
scope="row"
|
|
896
956
|
key={col.key}
|
|
957
|
+
{...columnLayout(col, index)}
|
|
897
958
|
data-align={columnAlign(col.align)}
|
|
898
959
|
data-truncate={col.truncate || undefined}
|
|
899
960
|
>
|
|
@@ -908,6 +969,7 @@ export function DataTable<T extends { id: string }>({
|
|
|
908
969
|
) : (
|
|
909
970
|
<td
|
|
910
971
|
key={col.key}
|
|
972
|
+
{...columnLayout(col, index)}
|
|
911
973
|
data-align={columnAlign(col.align)}
|
|
912
974
|
data-truncate={col.truncate || undefined}
|
|
913
975
|
>
|
|
@@ -926,6 +988,11 @@ export function DataTable<T extends { id: string }>({
|
|
|
926
988
|
aria-label={caption}
|
|
927
989
|
data-density={density === "default" ? undefined : density}
|
|
928
990
|
data-striped={striped || undefined}
|
|
991
|
+
data-prioritised={
|
|
992
|
+
columns.some((col, index) => columnLayout(col, index)["data-priority"])
|
|
993
|
+
? true
|
|
994
|
+
: undefined
|
|
995
|
+
}
|
|
929
996
|
>
|
|
930
997
|
{(toolbar || selectedIds.length > 0) && (
|
|
931
998
|
<div className="m-table-toolbar">
|
|
@@ -977,9 +1044,10 @@ export function DataTable<T extends { id: string }>({
|
|
|
977
1044
|
{columns.some((col) => col.width) && (
|
|
978
1045
|
<colgroup>
|
|
979
1046
|
{onSelectionChange && <col className="m-table-select-col" />}
|
|
980
|
-
{columns.map((col) => (
|
|
1047
|
+
{columns.map((col, index) => (
|
|
981
1048
|
<col
|
|
982
1049
|
key={col.key}
|
|
1050
|
+
{...columnLayout(col, index)}
|
|
983
1051
|
style={col.width ? { width: col.width } : undefined}
|
|
984
1052
|
/>
|
|
985
1053
|
))}
|
|
@@ -996,13 +1064,14 @@ export function DataTable<T extends { id: string }>({
|
|
|
996
1064
|
/>
|
|
997
1065
|
</th>
|
|
998
1066
|
)}
|
|
999
|
-
{columns.map((col) => {
|
|
1067
|
+
{columns.map((col, index) => {
|
|
1000
1068
|
const direction =
|
|
1001
1069
|
sort?.key === col.key ? sort.direction : undefined;
|
|
1002
1070
|
return (
|
|
1003
1071
|
<th
|
|
1004
1072
|
key={col.key}
|
|
1005
1073
|
scope="col"
|
|
1074
|
+
{...columnLayout(col, index)}
|
|
1006
1075
|
data-align={columnAlign(col.align)}
|
|
1007
1076
|
aria-sort={col.sortable ? direction || "none" : undefined}
|
|
1008
1077
|
>
|
package/src/data.css
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
/* One table base for DataTable, Table, PlanComparison and prose tables. Each sets
|
|
10
10
|
its cell padding; DataTable and Table follow density through --m-row-pad. */
|
|
11
11
|
.m-data-table {
|
|
12
|
+
container: m-data-table / inline-size;
|
|
12
13
|
overflow: hidden;
|
|
13
14
|
min-width: 0;
|
|
14
15
|
--m-cell-pad-y: var(--m-row-pad);
|
|
@@ -255,9 +256,64 @@
|
|
|
255
256
|
.m-data-table[data-density="compact"] .m-table-group th {
|
|
256
257
|
padding-block: 4px;
|
|
257
258
|
}
|
|
259
|
+
/* Pinned columns stay at the trailing edge while the table scrolls. Sticky
|
|
260
|
+
cells need an opaque background: the surface, or the row's own state.
|
|
261
|
+
Zero-specificity base, so stripes (after it) and hover and selection win. */
|
|
262
|
+
:where(.m-data-table) [data-pin="end"] {
|
|
263
|
+
position: sticky;
|
|
264
|
+
inset-inline-end: 0;
|
|
265
|
+
z-index: 2;
|
|
266
|
+
background-color: var(--m-surface);
|
|
267
|
+
box-shadow: inset 1px 0 0 var(--m-line);
|
|
268
|
+
}
|
|
269
|
+
:where(.m-data-table:dir(rtl)) [data-pin="end"] {
|
|
270
|
+
box-shadow: inset -1px 0 0 var(--m-line);
|
|
271
|
+
}
|
|
272
|
+
:where(
|
|
273
|
+
.m-data-table[data-striped]
|
|
274
|
+
tbody
|
|
275
|
+
tr:nth-child(even of :not(.m-table-group))
|
|
276
|
+
)
|
|
277
|
+
> [data-pin] {
|
|
278
|
+
background-color: var(--m-sunken);
|
|
279
|
+
}
|
|
280
|
+
.m-data-table thead th[data-pin] {
|
|
281
|
+
background-color: var(--m-paper);
|
|
282
|
+
}
|
|
283
|
+
.m-table-scroll[data-sticky] thead th[data-pin] {
|
|
284
|
+
z-index: 3;
|
|
285
|
+
}
|
|
286
|
+
/* (0,2,1): above the pinned stripe, below forced-colours selection (0,3,0).
|
|
287
|
+
:where(.m-data-table) does that for a selected cell. Hover also zeroes
|
|
288
|
+
[data-linked]; :hover alone would stay at (0,3,1) and keep HighlightText
|
|
289
|
+
on the author's background. */
|
|
290
|
+
:where(.m-data-table) tr:where([data-linked]):hover > [data-pin] {
|
|
291
|
+
background-color: var(--m-sunken);
|
|
292
|
+
}
|
|
293
|
+
:where(.m-data-table) tr[data-selected] > [data-pin] {
|
|
294
|
+
background-color: var(--m-selected);
|
|
295
|
+
}
|
|
296
|
+
/* Column priority follows the table's own width, so a table in a narrow
|
|
297
|
+
panel collapses as it would on a phone. */
|
|
298
|
+
@container m-data-table (max-width: 767px) {
|
|
299
|
+
.m-data-table [data-priority="low"] {
|
|
300
|
+
display: none;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
@container m-data-table (max-width: 479px) {
|
|
304
|
+
.m-data-table [data-priority="medium"] {
|
|
305
|
+
display: none;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/* A table with column priorities is meant to fit, so truncated columns keep
|
|
309
|
+
shrinking on narrow screens, down to a readable floor. */
|
|
310
|
+
.m-data-table[data-prioritised] [data-truncate] .m-truncate {
|
|
311
|
+
min-width: 5rem;
|
|
312
|
+
}
|
|
258
313
|
@media (max-width: 640px) {
|
|
259
|
-
/* Narrow screens scroll the table instead of collapsing truncated columns
|
|
260
|
-
|
|
314
|
+
/* Narrow screens scroll the table instead of collapsing truncated columns,
|
|
315
|
+
unless it has column priorities (above). */
|
|
316
|
+
.m-data-table:not([data-prioritised]) [data-truncate] {
|
|
261
317
|
max-width: none;
|
|
262
318
|
}
|
|
263
319
|
.m-truncate {
|
package/src/foundation.css
CHANGED
|
@@ -1128,9 +1128,13 @@
|
|
|
1128
1128
|
}
|
|
1129
1129
|
|
|
1130
1130
|
/* == SegmentedControl == */
|
|
1131
|
+
/* Never wider than its container: long or many options scroll inside the
|
|
1132
|
+
control, which keeps the selected option in view (inspection.tsx). */
|
|
1131
1133
|
.m-segmented {
|
|
1132
1134
|
display: inline-flex;
|
|
1133
1135
|
width: fit-content;
|
|
1136
|
+
max-width: 100%;
|
|
1137
|
+
overflow-x: auto;
|
|
1134
1138
|
background: var(--m-sunken);
|
|
1135
1139
|
padding: 3px;
|
|
1136
1140
|
gap: 2px;
|
|
@@ -1146,6 +1150,12 @@
|
|
|
1146
1150
|
padding: 5px 10px;
|
|
1147
1151
|
cursor: pointer;
|
|
1148
1152
|
min-height: max(28px, var(--m-touch-target));
|
|
1153
|
+
flex-shrink: 0;
|
|
1154
|
+
white-space: nowrap;
|
|
1155
|
+
}
|
|
1156
|
+
/* Inside the 3px padding, so the scroll container doesn't clip the ring. */
|
|
1157
|
+
.m-segmented button:focus-visible {
|
|
1158
|
+
outline-offset: 1px;
|
|
1149
1159
|
}
|
|
1150
1160
|
.m-segmented [aria-pressed="true"] {
|
|
1151
1161
|
background: var(--m-surface);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { useRef, type CSSProperties } from "react";
|
|
2
|
+
import { componentRoot, cx, useDomId, usePlayback } from "./internal.js";
|
|
3
|
+
|
|
4
|
+
/** A point in the drawing, in the illustration's viewBox units. */
|
|
5
|
+
export interface NetworkNode {
|
|
6
|
+
id: string;
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
/** Short text drawn under the node. */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** accent: the brand's accent fill, to pick out a node. */
|
|
12
|
+
tone?: "default" | "accent";
|
|
13
|
+
}
|
|
14
|
+
/** A straight connection between two node ids. */
|
|
15
|
+
export interface NetworkConnection {
|
|
16
|
+
from: string;
|
|
17
|
+
to: string;
|
|
18
|
+
/** Whether a signal travels from `from` to `to`. Default true. */
|
|
19
|
+
signal?: boolean;
|
|
20
|
+
/** Seconds for this signal to cross; defaults to the illustration's duration. */
|
|
21
|
+
duration?: number;
|
|
22
|
+
/** Seconds before this signal starts; defaults to an even stagger. */
|
|
23
|
+
delay?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const NODE_RADIUS = 8;
|
|
27
|
+
const SIGNAL_RADIUS = 4;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A lightweight network drawing: nodes and connections supplied as data, with
|
|
31
|
+
* optional signals travelling along the connections. Decorative unless it has
|
|
32
|
+
* a label. The animation is illustrative: it never represents live activity.
|
|
33
|
+
*/
|
|
34
|
+
export function NetworkIllustration({
|
|
35
|
+
nodes,
|
|
36
|
+
connections,
|
|
37
|
+
width = 480,
|
|
38
|
+
height = 320,
|
|
39
|
+
label,
|
|
40
|
+
description,
|
|
41
|
+
motion = "auto",
|
|
42
|
+
duration = 3,
|
|
43
|
+
paused,
|
|
44
|
+
onPausedChange,
|
|
45
|
+
id,
|
|
46
|
+
className,
|
|
47
|
+
}: {
|
|
48
|
+
nodes: NetworkNode[];
|
|
49
|
+
connections: NetworkConnection[];
|
|
50
|
+
/** Width of the viewBox; the drawing scales to its container. */
|
|
51
|
+
width?: number;
|
|
52
|
+
/** Height of the viewBox. */
|
|
53
|
+
height?: number;
|
|
54
|
+
/** Makes the drawing informative: an image with this accessible name. Omitted, it is decorative and hidden from assistive technology. */
|
|
55
|
+
label?: string;
|
|
56
|
+
/** With a label, a longer description of what the drawing shows. */
|
|
57
|
+
description?: string;
|
|
58
|
+
/** auto: signals travel where connections carry one, with a pause control. static: the same drawing without motion. */
|
|
59
|
+
motion?: "auto" | "static";
|
|
60
|
+
/** Seconds for a signal to cross a connection. */
|
|
61
|
+
duration?: number;
|
|
62
|
+
/** Whether the user has paused the animation; pass with onPausedChange to control it or share it between illustrations. */
|
|
63
|
+
paused?: boolean;
|
|
64
|
+
onPausedChange?: (paused: boolean) => void;
|
|
65
|
+
id?: string;
|
|
66
|
+
className?: string;
|
|
67
|
+
}) {
|
|
68
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
69
|
+
const titleId = useDomId("m-network-title");
|
|
70
|
+
const descId = useDomId("m-network-desc");
|
|
71
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
72
|
+
const links = connections.flatMap((connection) => {
|
|
73
|
+
const from = byId.get(connection.from);
|
|
74
|
+
const to = byId.get(connection.to);
|
|
75
|
+
return from && to ? [{ ...connection, a: from, b: to }] : [];
|
|
76
|
+
});
|
|
77
|
+
const carriers = links.filter((link) => link.signal !== false);
|
|
78
|
+
const animated = motion === "auto" && carriers.length > 0;
|
|
79
|
+
const { reason, reduced, userPaused, toggle } = usePlayback(ref, {
|
|
80
|
+
paused,
|
|
81
|
+
onPausedChange,
|
|
82
|
+
enabled: animated,
|
|
83
|
+
});
|
|
84
|
+
const informative = Boolean(label);
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
ref={ref}
|
|
88
|
+
id={id}
|
|
89
|
+
className={cx("m-network", className)}
|
|
90
|
+
{...componentRoot("NetworkIllustration")}
|
|
91
|
+
data-motion={motion === "static" ? "static" : undefined}
|
|
92
|
+
data-paused={animated ? (reason ?? undefined) : undefined}
|
|
93
|
+
>
|
|
94
|
+
<svg
|
|
95
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
96
|
+
{...(informative
|
|
97
|
+
? {
|
|
98
|
+
role: "img",
|
|
99
|
+
"aria-labelledby": titleId,
|
|
100
|
+
"aria-describedby": description ? descId : undefined,
|
|
101
|
+
}
|
|
102
|
+
: { "aria-hidden": true, focusable: false })}
|
|
103
|
+
>
|
|
104
|
+
{informative && <title id={titleId}>{label}</title>}
|
|
105
|
+
{informative && description && <desc id={descId}>{description}</desc>}
|
|
106
|
+
{links.map((link, index) => (
|
|
107
|
+
<line
|
|
108
|
+
key={`${link.from}:${link.to}:${index}`}
|
|
109
|
+
className="m-network-link"
|
|
110
|
+
x1={link.a.x}
|
|
111
|
+
y1={link.a.y}
|
|
112
|
+
x2={link.b.x}
|
|
113
|
+
y2={link.b.y}
|
|
114
|
+
/>
|
|
115
|
+
))}
|
|
116
|
+
{animated &&
|
|
117
|
+
!reduced &&
|
|
118
|
+
carriers.map((link, index) => {
|
|
119
|
+
const time = link.duration ?? duration;
|
|
120
|
+
const delay = link.delay ?? -((index * time) / carriers.length);
|
|
121
|
+
return (
|
|
122
|
+
<circle
|
|
123
|
+
key={`${link.from}:${link.to}:${index}`}
|
|
124
|
+
className="m-network-signal"
|
|
125
|
+
r={SIGNAL_RADIUS}
|
|
126
|
+
style={
|
|
127
|
+
{
|
|
128
|
+
"--m-signal-from": `${link.a.x}px ${link.a.y}px`,
|
|
129
|
+
"--m-signal-to": `${link.b.x}px ${link.b.y}px`,
|
|
130
|
+
animationDuration: `${time}s`,
|
|
131
|
+
animationDelay: delay ? `${delay}s` : undefined,
|
|
132
|
+
} as CSSProperties
|
|
133
|
+
}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
})}
|
|
137
|
+
{nodes.map((node) => (
|
|
138
|
+
<circle
|
|
139
|
+
key={node.id}
|
|
140
|
+
className="m-network-node"
|
|
141
|
+
cx={node.x}
|
|
142
|
+
cy={node.y}
|
|
143
|
+
r={NODE_RADIUS}
|
|
144
|
+
data-tone={node.tone === "accent" ? "accent" : undefined}
|
|
145
|
+
/>
|
|
146
|
+
))}
|
|
147
|
+
{nodes.map(
|
|
148
|
+
(node) =>
|
|
149
|
+
node.label && (
|
|
150
|
+
<text
|
|
151
|
+
key={node.id}
|
|
152
|
+
className="m-network-label"
|
|
153
|
+
x={node.x}
|
|
154
|
+
y={node.y + NODE_RADIUS + 16}
|
|
155
|
+
textAnchor="middle"
|
|
156
|
+
>
|
|
157
|
+
{node.label}
|
|
158
|
+
</text>
|
|
159
|
+
),
|
|
160
|
+
)}
|
|
161
|
+
</svg>
|
|
162
|
+
{animated && !reduced && (
|
|
163
|
+
<button type="button" className="m-network-toggle" onClick={toggle}>
|
|
164
|
+
{userPaused ? "Play animation" : "Pause animation"}
|
|
165
|
+
</button>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -66,6 +66,9 @@ export type {
|
|
|
66
66
|
TabNavItem,
|
|
67
67
|
StackedShellProps,
|
|
68
68
|
NavbarProps,
|
|
69
|
+
NavbarItem,
|
|
70
|
+
NavbarGroup,
|
|
71
|
+
NavbarTone,
|
|
69
72
|
BreadcrumbsProps,
|
|
70
73
|
TabNavProps,
|
|
71
74
|
ProgressStepsProps,
|
|
@@ -94,6 +97,7 @@ export type {
|
|
|
94
97
|
} from "./auth.js";
|
|
95
98
|
export {
|
|
96
99
|
MarketingHeader,
|
|
100
|
+
MarketingPage,
|
|
97
101
|
Hero,
|
|
98
102
|
FeatureGrid,
|
|
99
103
|
FeatureSection,
|
|
@@ -228,3 +232,5 @@ export type {
|
|
|
228
232
|
export { CommentComposer } from "./composer.js";
|
|
229
233
|
export type { LinkItem, ItemGroup } from "./navigation.js";
|
|
230
234
|
export type { HeadingLevel } from "./heading.js";
|
|
235
|
+
export { NetworkIllustration } from "./illustration.js";
|
|
236
|
+
export type { NetworkNode, NetworkConnection } from "./illustration.js";
|
package/src/inspection.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from "react";
|
|
1
2
|
import { KitLink } from "./navigation.js";
|
|
2
3
|
import { componentRoot, cx, useComponentName } from "./internal.js";
|
|
3
4
|
export function SegmentedControl({
|
|
@@ -14,8 +15,22 @@ export function SegmentedControl({
|
|
|
14
15
|
className?: string;
|
|
15
16
|
}) {
|
|
16
17
|
const name = useComponentName("SegmentedControl");
|
|
18
|
+
const group = useRef<HTMLDivElement>(null);
|
|
19
|
+
// When the options scroll (narrow screens), keep the selected one in view,
|
|
20
|
+
// on mount and on change. Only the control scrolls, never the page.
|
|
21
|
+
useLayoutEffect(() => {
|
|
22
|
+
const root = group.current;
|
|
23
|
+
const selected = root?.querySelector<HTMLElement>('[aria-pressed="true"]');
|
|
24
|
+
if (!root || !selected || root.scrollWidth <= root.clientWidth) return;
|
|
25
|
+
const outer = root.getBoundingClientRect();
|
|
26
|
+
const inner = selected.getBoundingClientRect();
|
|
27
|
+
if (inner.left < outer.left) root.scrollLeft -= outer.left - inner.left;
|
|
28
|
+
else if (inner.right > outer.right)
|
|
29
|
+
root.scrollLeft += inner.right - outer.right;
|
|
30
|
+
}, [value]);
|
|
17
31
|
return (
|
|
18
32
|
<div
|
|
33
|
+
ref={group}
|
|
19
34
|
{...componentRoot(name)}
|
|
20
35
|
className={cx("m-segmented", className)}
|
|
21
36
|
role="group"
|