@designtools/docs 0.2.0 → 0.2.2

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/src/swatches.tsx CHANGED
@@ -1,17 +1,19 @@
1
1
  "use client";
2
2
 
3
- import { type ReactNode, useEffect, useRef, useState } from "react";
3
+ import { Check, Square, X } from "lucide-react";
4
+ import { type CSSProperties, type ReactNode, useEffect, useRef, useState } from "react";
4
5
  import { contrastRatio, wcagGrade } from "./lib/contrast";
5
6
  import { cx } from "./lib/cx";
6
7
  import { ramps, semanticColours, valueIn, type Mode } from "./lib/manifest";
7
8
  import type { TokensManifest } from "./lib/manifest-types";
8
9
 
9
10
  /**
10
- * Colour, painted from the tokens rather than restated. Each colour with a
11
- * subdued pair is a card: its fill, subdued, hover and press, in light and dark
12
- * side by side. Then the surfaces, then the utility colours. Every sample sits
13
- * inside its own `data-theme`, so dark is shown as the dark tokens resolve, and
14
- * every pair's contrast is measured on the colours the browser actually paints.
11
+ * Colour, painted from the tokens rather than restated. No panels and no cards around the samples:
12
+ * each sample is its own colour, with one hairline round it, set on the page in a light and a dark
13
+ * column. Each colour with a subdued pair shows its fill large, then subdued, hover and press; then
14
+ * the surfaces and the utility colours, one row each. Every sample sits inside its own `data-theme`,
15
+ * so dark is shown as the dark tokens resolve, and every pair's contrast is measured on the colours
16
+ * the browser actually paints.
15
17
  */
16
18
  export function SemanticSwatches({
17
19
  tokens,
@@ -30,85 +32,107 @@ export function SemanticSwatches({
30
32
  const token = byName.get(name)?.background;
31
33
  return token && pointsAt(valueIn(token, mode));
32
34
  };
35
+ const columns = cx("grid gap-x-8 gap-y-6", modes.length > 1 && "md:grid-cols-2");
36
+ const modeLabel = (mode: Mode) => <p className="text-xs font-medium text-muted-foreground capitalize">{mode}</p>;
37
+ const title = (text: string) => text.charAt(0).toUpperCase() + text.slice(1);
33
38
 
34
- // one level of framing: a panel per mode (the dark one has to be dark), chips inside it, and nothing around the family
35
- const panel = (mode: Mode) =>
36
- cx("flex flex-col gap-3 rounded-2xl bg-canvas p-4 text-canvas-foreground", mode === "light" && "ring-1 ring-border ring-inset");
37
- const modeLabel = (mode: Mode) => <span className="text-xs font-medium text-muted-foreground capitalize">{mode}</span>;
38
- const row = (title: string, meta: ReactNode, body: ReactNode, key?: string) => (
39
- <div key={key ?? title} data-slot="swatch-row" className="grid gap-x-10 gap-y-5 border-t border-border pt-8 first:border-t-0 first:pt-0 lg:grid-cols-[11rem_minmax(0,1fr)]">
40
- <div className="flex flex-col gap-1.5">
41
- <h3 className="text-2xl leading-tight tracking-tight">{title}</h3>
42
- {meta}
39
+ return (
40
+ <div data-slot="swatches" className="flex flex-col gap-16">
41
+ <div className="flex flex-col gap-14">
42
+ {families.map((f) => (
43
+ <section key={f} data-slot="swatch-family" className="flex flex-col gap-5">
44
+ <header className="flex items-baseline gap-3">
45
+ <h3 className="text-2xl leading-tight tracking-tight">{title(f.slice(2).replace(/-/g, " "))}</h3>
46
+ <code className="font-mono text-xs text-muted-foreground">{f}</code>
47
+ </header>
48
+ <div className={columns}>
49
+ {modes.map((mode) => (
50
+ <div key={mode} className="flex flex-col gap-2.5">
51
+ {modeLabel(mode)}
52
+ {/* only the samples take the mode; the label is read on the page */}
53
+ <div data-theme={mode} className="flex flex-col gap-2.5">
54
+ <Chip name={f} label="fill" step={step(f, mode)} size="lg" />
55
+ <div className="grid grid-cols-3 gap-2.5">
56
+ <Chip name={`${f}-subdued`} label="subdued" step={step(`${f}-subdued`, mode)} />
57
+ {byName.has(`${f}-hover`) && <Chip name={`${f}-hover`} foreground={`${f}-foreground`} label="hover" />}
58
+ {byName.has(`${f}-active`) && <Chip name={`${f}-active`} foreground={`${f}-foreground`} label="press" />}
59
+ </div>
60
+ </div>
61
+ </div>
62
+ ))}
63
+ </div>
64
+ </section>
65
+ ))}
43
66
  </div>
44
- {body}
45
- </div>
46
- );
47
67
 
48
- return (
49
- <div data-slot="swatches" className="flex flex-col gap-10">
50
- {families.map((f) =>
51
- row(
52
- (([first, ...rest]) => first.toUpperCase() + rest.join(""))(f.slice(2).replace(/-/g, " ")),
53
- <code className="font-mono text-xs text-muted-foreground">{f}</code>,
54
- <div className={cx("grid gap-4", modes.length > 1 && "md:grid-cols-2")}>
55
- {modes.map((mode) => (
56
- <div key={mode} data-theme={mode} data-slot="swatch-family" className={panel(mode)}>
57
- {modeLabel(mode)}
58
- <Chip name={f} label="fill" step={step(f, mode)} size="lg" />
59
- <div className="grid grid-cols-3 gap-2">
60
- <Chip name={`${f}-subdued`} label="subdued" step={step(`${f}-subdued`, mode)} />
61
- {byName.has(`${f}-hover`) && <Chip name={`${f}-hover`} foreground={`${f}-foreground`} label="hover" />}
62
- {byName.has(`${f}-active`) && <Chip name={`${f}-active`} foreground={`${f}-foreground`} label="press" />}
68
+ {surfaces.length > 0 && (
69
+ <Rows title="Surfaces" lead="The page, panels and popups, and the brand's own backgrounds, each with its text." modes={modes}>
70
+ {surfaces.map((name) => (
71
+ <Row key={name} name={name.slice(2)} modes={modes}>
72
+ {(mode) => (
73
+ <div data-theme={mode}>
74
+ <Chip name={name} label={step(name, mode) ?? ""} size="wide" />
63
75
  </div>
64
- </div>
65
- ))}
66
- </div>,
67
- f,
68
- ),
76
+ )}
77
+ </Row>
78
+ ))}
79
+ </Rows>
69
80
  )}
70
81
 
71
- {surfaces.length > 0 &&
72
- row(
73
- "Surfaces",
74
- <p className="text-sm text-muted-foreground">The page, panels and popups, each with its text.</p>,
75
- <div className={cx("grid gap-4", modes.length > 1 && "md:grid-cols-2")}>
76
- {modes.map((mode) => (
77
- <div key={mode} data-theme={mode} className={panel(mode)}>
78
- {modeLabel(mode)}
79
- <div className="grid grid-cols-2 gap-2 xl:grid-cols-3">
80
- {surfaces.map((name) => (
81
- <Chip key={name} name={name} label={name.slice(2)} step={step(name, mode)} outlined />
82
- ))}
83
- </div>
84
- </div>
85
- ))}
86
- </div>,
87
- )}
82
+ {utility.length > 0 && (
83
+ <Rows title="Lines, focus and shadow" lead="Borders, input edges, the focus ring and the shadow colour." modes={modes}>
84
+ {utility.map((name) => (
85
+ <Row key={name} name={name.slice(2)} modes={modes}>
86
+ {(mode) => (
87
+ <span className="flex h-14 items-center gap-3">
88
+ <span data-theme={mode} aria-hidden className="size-10 shrink-0 rounded-lg ring-1 ring-(--canvas-foreground)/15 ring-inset" style={{ background: `var(${name})` }} />
89
+ <span className="font-mono text-xs text-muted-foreground">{step(name, mode)}</span>
90
+ </span>
91
+ )}
92
+ </Row>
93
+ ))}
94
+ </Rows>
95
+ )}
96
+ </div>
97
+ );
98
+ }
88
99
 
89
- {utility.length > 0 &&
90
- row(
91
- "Lines, focus and shadow",
92
- <p className="text-sm text-muted-foreground">Borders, input edges, the focus ring and the shadow colour.</p>,
93
- <div className={cx("grid gap-4", modes.length > 1 && "md:grid-cols-2")}>
94
- {modes.map((mode) => (
95
- <div key={mode} data-theme={mode} className={panel(mode)}>
96
- {modeLabel(mode)}
97
- <ul className="grid grid-cols-2 gap-x-4 gap-y-3">
98
- {utility.map((name) => (
99
- <li key={name} data-slot="swatch" className="flex items-center gap-3">
100
- <span aria-hidden className="size-9 shrink-0 rounded-lg border border-border" style={{ background: `var(${name})` }} />
101
- <span className="flex min-w-0 flex-col font-mono text-xs">
102
- <span className="truncate font-medium">{name.slice(2)}</span>
103
- <span className="truncate text-muted-foreground">{step(name, mode)}</span>
104
- </span>
105
- </li>
106
- ))}
107
- </ul>
108
- </div>
109
- ))}
110
- </div>,
111
- )}
100
+ /** A titled list of rows, with the mode names once over the columns. */
101
+ function Rows({ title, lead, modes, children }: { title: string; lead: string; modes: Mode[]; children: ReactNode }) {
102
+ return (
103
+ <section className="flex flex-col gap-5">
104
+ <header className="flex flex-col gap-1.5">
105
+ <h3 className="text-2xl leading-tight tracking-tight">{title}</h3>
106
+ <p className="text-sm text-muted-foreground">{lead}</p>
107
+ </header>
108
+ <div>
109
+ <div aria-hidden className="hidden gap-x-8 pb-3 text-xs font-medium text-muted-foreground md:grid md:grid-cols-[9rem_repeat(var(--modes),minmax(0,1fr))]" style={{ "--modes": modes.length } as CSSProperties}>
110
+ <span />
111
+ {modes.map((m) => (
112
+ <span key={m} className="capitalize">
113
+ {m}
114
+ </span>
115
+ ))}
116
+ </div>
117
+ {children}
118
+ </div>
119
+ </section>
120
+ );
121
+ }
122
+
123
+ function Row({ name, modes, children }: { name: string; modes: Mode[]; children: (mode: Mode) => ReactNode }) {
124
+ return (
125
+ <div
126
+ className="grid items-center gap-x-8 gap-y-2 border-t border-border py-3 md:grid-cols-[9rem_repeat(var(--modes),minmax(0,1fr))]"
127
+ style={{ "--modes": modes.length } as CSSProperties}
128
+ >
129
+ <code className="font-mono text-sm">{name}</code>
130
+ {modes.map((mode) => (
131
+ <div key={mode} className="flex flex-col gap-1">
132
+ <span className="text-xs text-muted-foreground capitalize md:hidden">{mode}</span>
133
+ {children(mode)}
134
+ </div>
135
+ ))}
112
136
  </div>
113
137
  );
114
138
  }
@@ -121,9 +145,9 @@ function pointsAt(value: string | undefined): string | undefined {
121
145
  }
122
146
 
123
147
  /**
124
- * One background with its foreground on it, labelled in that foreground, with the
125
- * contrast measured on what the browser paints. It reads its mode from the nearest
126
- * `data-theme` around it.
148
+ * One background with its foreground on it, labelled in that foreground, with the contrast measured on
149
+ * what the browser paints. It reads its mode from the nearest `data-theme` around it. One hairline round
150
+ * it, so a white sample shows on a white page, and nothing else.
127
151
  */
128
152
  function Chip({
129
153
  name,
@@ -131,14 +155,12 @@ function Chip({
131
155
  label,
132
156
  step,
133
157
  size = "md",
134
- outlined,
135
158
  }: {
136
159
  name: string;
137
160
  foreground?: string;
138
161
  label: string;
139
162
  step?: string;
140
- size?: "md" | "lg";
141
- outlined?: boolean;
163
+ size?: "md" | "lg" | "wide";
142
164
  }) {
143
165
  const chip = useRef<HTMLDivElement>(null);
144
166
  const [ratio, setRatio] = useState<number | null>(null);
@@ -151,40 +173,45 @@ function Chip({
151
173
  const grade = ratio === null ? null : wcagGrade(ratio);
152
174
  const fails = grade === "Fail";
153
175
  const measured = grade && ratio !== null ? `${ratio.toFixed(1)}:1 ${grade}` : undefined;
154
- return (
155
- <div
156
- ref={chip}
157
- data-slot="swatch"
158
- title={`${name} with ${foreground}${step ? ` · ${step}` : ""}${measured ? ` · ${measured}` : ""}`}
159
- className={cx("flex min-w-0 flex-col justify-between gap-3 rounded-lg p-3", size === "lg" ? "min-h-28" : "min-h-20", outlined && "border border-border")}
160
- style={{ background: `var(${name})`, color: `var(${foreground})` }}
161
- >
162
- {size === "lg" ? (
163
- <span className="flex items-start justify-between gap-2">
164
- <span className="text-3xl leading-none tracking-tight">Aa</span>
165
- {measured && (
166
- <span
167
- data-slot="swatch-contrast"
168
- className={cx("rounded-full px-2 py-0.5 font-mono text-xs whitespace-nowrap", fails ? "bg-destructive font-medium text-destructive-foreground" : "ring-1 ring-current/30")}
169
- >
170
- {measured}
171
- </span>
172
- )}
176
+ const contrast = measured && (
177
+ <span data-slot="swatch-contrast" className={cx("font-mono text-xs whitespace-nowrap", fails ? "rounded-sm bg-destructive px-1.5 py-0.5 font-medium text-destructive-foreground" : "opacity-80")}>
178
+ {measured}
179
+ </span>
180
+ );
181
+ const paint = { background: `var(${name})`, color: `var(${foreground})` };
182
+ // a hairline in the sample's own text colour: it shows a white sample on a white page and disappears into a strong fill
183
+ const base = "min-w-0 rounded-xl ring-1 ring-current/15 ring-inset";
184
+ const tip = `${name} with ${foreground}${step ? ` · ${step}` : ""}${measured ? ` · ${measured}` : ""}`;
185
+
186
+ if (size === "wide")
187
+ return (
188
+ <div ref={chip} data-slot="swatch" title={tip} className={cx(base, "flex h-14 items-center justify-between gap-4 px-4")} style={paint}>
189
+ <span className="flex min-w-0 items-baseline gap-3">
190
+ <span className="text-xl leading-none font-medium">Aa</span>
191
+ <span className="font-mono text-xs opacity-80">{label}</span>
173
192
  </span>
174
- ) : (
175
- <span className="text-base leading-none font-medium">Aa</span>
176
- )}
177
- <span className="flex min-w-0 flex-col font-mono text-xs leading-snug">
178
- <span className="truncate font-medium">{label}</span>
179
- {size === "lg" ? (
180
- step && <span className="truncate opacity-75">{step}</span>
181
- ) : (
182
- measured && (
183
- <span data-slot="swatch-contrast" className={cx("truncate", fails ? "w-fit rounded-sm bg-destructive px-1 text-destructive-foreground" : "opacity-75")}>
184
- {measured}
185
- </span>
186
- )
187
- )}
193
+ {contrast}
194
+ </div>
195
+ );
196
+ if (size === "lg")
197
+ return (
198
+ <div ref={chip} data-slot="swatch" title={tip} className={cx(base, "flex min-h-36 flex-col justify-between gap-6 p-5")} style={paint}>
199
+ <span className="flex items-start justify-between gap-3">
200
+ <span className="text-5xl leading-none tracking-tight">Aa</span>
201
+ {contrast}
202
+ </span>
203
+ <span className="flex flex-col gap-0.5 text-sm leading-snug">
204
+ <span className="font-medium">{label}</span>
205
+ {step && <span className="font-mono text-xs opacity-80">{step}</span>}
206
+ </span>
207
+ </div>
208
+ );
209
+ return (
210
+ <div ref={chip} data-slot="swatch" title={tip} className={cx(base, "flex min-h-28 flex-col justify-between gap-4 p-4")} style={paint}>
211
+ <span className="text-2xl leading-none font-medium">Aa</span>
212
+ <span className="flex flex-col gap-1 leading-snug">
213
+ <span className="text-sm font-medium">{label}</span>
214
+ {contrast}
188
215
  </span>
189
216
  </div>
190
217
  );
@@ -194,7 +221,7 @@ function Chip({
194
221
  * Each colour on each background, per mode: a label is measured on its own fill, and this is
195
222
  * everything else, red text on the page, a brand fill on a dark section. Backgrounds are the
196
223
  * canvas, surface and overlay and every named `--surface-<name>`; colours are each semantic fill
197
- * and muted text. T: fine as text (4.5:1); S: fine as a shape only (3:1); none: neither.
224
+ * and muted text. Text: fine as text (4.5:1); shape: fine as a shape only (3:1); none: neither.
198
225
  * Measured on what the browser paints, inside each mode's `data-theme`.
199
226
  */
200
227
  export function PageContrast({ tokens, modes = ["light", "dark"] }: { tokens: TokensManifest; modes?: Mode[] }) {
@@ -202,7 +229,21 @@ export function PageContrast({ tokens, modes = ["light", "dark"] }: { tokens: To
202
229
  const colours = [...names].filter((n) => names.has(`${n}-subdued`)).concat(names.has("--muted") ? ["--muted-foreground"] : []);
203
230
  const grounds = ["--canvas", "--surface", "--overlay", ...[...names].filter((n) => n.startsWith("--surface-") && !n.endsWith("-foreground") && n !== "--surface-foreground")];
204
231
  return (
205
- <div data-slot="page-contrast" className="flex flex-col gap-12">
232
+ <div data-slot="page-contrast" className="flex flex-col gap-14">
233
+ <p className="flex flex-wrap gap-x-6 gap-y-2 text-sm text-muted-foreground">
234
+ {(
235
+ [
236
+ ["text", "fine as text, 4.5:1"],
237
+ ["shape", "fine as a shape only, 3:1"],
238
+ ["none", "neither"],
239
+ ] as const
240
+ ).map(([mark, words]) => (
241
+ <span key={mark} className="inline-flex items-center gap-2">
242
+ <Verdict mark={mark} />
243
+ {words}
244
+ </span>
245
+ ))}
246
+ </p>
206
247
  {modes.map((mode) => (
207
248
  <PageGrid key={mode} mode={mode} colours={colours} grounds={grounds} />
208
249
  ))}
@@ -210,6 +251,23 @@ export function PageContrast({ tokens, modes = ["light", "dark"] }: { tokens: To
210
251
  );
211
252
  }
212
253
 
254
+ const VERDICT = {
255
+ text: { Icon: Check, className: "text-success-subdued-foreground", label: "text" },
256
+ shape: { Icon: Square, className: "text-muted-foreground", label: "shape" },
257
+ none: { Icon: X, className: "text-destructive-subdued-foreground", label: "none" },
258
+ } as const;
259
+
260
+ function Verdict({ mark, ratio }: { mark: keyof typeof VERDICT; ratio?: number }) {
261
+ const { Icon, className, label } = VERDICT[mark];
262
+ return (
263
+ <span className="inline-flex items-center gap-1.5 whitespace-nowrap">
264
+ <Icon aria-hidden className={cx("size-3.5 shrink-0", className)} strokeWidth={2.5} />
265
+ {ratio != null && <span className="font-mono text-xs text-canvas-foreground tabular-nums">{ratio.toFixed(1)}</span>}
266
+ <span className={cx(ratio != null && "text-xs text-muted-foreground")}>{ratio != null ? label : null}</span>
267
+ </span>
268
+ );
269
+ }
270
+
213
271
  function PageGrid({ mode, colours, grounds }: { mode: Mode; colours: string[]; grounds: string[] }) {
214
272
  const probe = useRef<HTMLDivElement>(null);
215
273
  const [ratios, setRatios] = useState<Record<string, number | null>>({});
@@ -226,44 +284,44 @@ function PageGrid({ mode, colours, grounds }: { mode: Mode; colours: string[]; g
226
284
  setRatios(out);
227
285
  }, [colours, grounds]);
228
286
  return (
229
- <div className="-mx-1 overflow-x-auto px-1" tabIndex={0} role="region" aria-label={`Colours on each background, ${mode} mode`}>
287
+ <div className="-mx-1 overflow-x-auto px-1 pb-2" tabIndex={0} role="region" aria-label={`Colours on each background, ${mode} mode`}>
230
288
  <div ref={probe} data-theme={mode} hidden />
231
- <table className="w-full min-w-[40rem] border-collapse text-left text-sm">
232
- <caption className="pb-4 text-left text-lg font-medium capitalize">{mode}</caption>
289
+ <table className="w-full min-w-[46rem] table-fixed border-collapse text-left text-sm">
290
+ <caption className="pb-5 text-left text-lg font-medium capitalize">{mode}</caption>
233
291
  <thead>
234
292
  <tr className="text-xs text-muted-foreground">
235
- <th scope="col" className="pr-4 pb-3 font-medium">Colour</th>
293
+ <th scope="col" className="w-32 pr-4 pb-3 font-medium">
294
+ Colour
295
+ </th>
236
296
  {grounds.map((g) => (
237
- <th key={g} scope="col" className="px-3 pb-3 font-mono font-normal">{g.slice(2)}</th>
297
+ <th key={g} scope="col" className="px-2 pb-3 font-mono font-normal">
298
+ {g.slice(2)}
299
+ </th>
238
300
  ))}
239
301
  </tr>
240
302
  </thead>
241
303
  <tbody>
242
304
  {colours.map((c) => (
243
305
  <tr key={c} className="border-t border-border">
244
- <th scope="row" className="py-3.5 pr-4 font-mono text-xs font-medium">{c.slice(2)}</th>
306
+ <th scope="row" className="py-4 pr-4 align-top font-mono text-xs font-medium break-words">
307
+ {c.slice(2)}
308
+ </th>
245
309
  {grounds.map((g) => {
246
310
  const r = ratios[`${c}|${g}`];
247
- const mark = r == null ? "" : r >= 4.5 ? "text" : r >= 3 ? "shape" : "none";
311
+ const mark = r == null ? undefined : r >= 4.5 ? "text" : r >= 3 ? "shape" : "none";
248
312
  return (
249
- <td key={g} className="px-3 py-3.5">
250
- <div className="flex items-center gap-2.5">
313
+ <td key={g} className="px-2 py-4 align-top">
314
+ <div className="flex flex-col gap-2">
251
315
  {/* only the sample takes the mode; the number is read on the page */}
252
- <span data-theme={mode} className="flex h-10 w-14 items-center justify-center rounded-lg border border-(--border) text-base font-bold" style={{ background: `var(${g})`, color: `var(${c})` }} aria-hidden>
316
+ <span
317
+ data-theme={mode}
318
+ aria-hidden
319
+ className="flex h-12 items-center justify-center rounded-lg text-lg font-bold ring-1 ring-(--border) ring-inset"
320
+ style={{ background: `var(${g})`, color: `var(${c})` }}
321
+ >
253
322
  Aa
254
323
  </span>
255
- {r != null && (
256
- <span
257
- className={cx(
258
- "inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-mono text-xs",
259
- mark === "text" && "bg-success-subdued text-success-subdued-foreground",
260
- mark === "shape" && "bg-muted text-muted-foreground",
261
- mark === "none" && "bg-destructive-subdued text-destructive-subdued-foreground",
262
- )}
263
- >
264
- {r.toFixed(1)} {mark}
265
- </span>
266
- )}
324
+ {mark && r != null && <Verdict mark={mark} ratio={r} />}
267
325
  </div>
268
326
  </td>
269
327
  );
package/src/workbench.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { handlerNames } from "./lib/handlers";
3
4
  import { Live } from "./lib/live";
4
5
  import { Tabs } from "@base-ui/react/tabs";
5
6
  import { Toggle } from "@base-ui/react/toggle";
@@ -133,7 +134,7 @@ export function Workbench({
133
134
  const own = Object.entries(entry.props).filter(([name, p]) => !axisNames.has(name) && !PLUMBING.has(name) && p.deprecated === undefined);
134
135
  // disabled is a pinned state ("Off"), not a prop control
135
136
  const settable = own.filter(([name, p]) => !isFunction(name, p) && !p.from && name !== "disabled");
136
- const handlers = own.filter(([name, p]) => isFunction(name, p)).map(([name]) => name);
137
+ const handlers = handlerNames(Object.fromEntries(own));
137
138
  const canDisable = "disabled" in entry.props;
138
139
 
139
140
  const [variants, setVariants] = useState<Record<string, string>>({});