@tonycodes-ui/mortar 0.0.0-stage → 0.11.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.
Files changed (104) hide show
  1. package/AGENTS.md +10 -0
  2. package/CHANGELOG.md +276 -0
  3. package/CONTRIBUTING.md +48 -0
  4. package/DESIGN.md +51 -0
  5. package/LICENSE +21 -0
  6. package/README.md +155 -2
  7. package/TESTING.md +83 -0
  8. package/baseline-policy.json +16 -0
  9. package/bin/mortar.mjs +258 -0
  10. package/catalog.json +9703 -0
  11. package/cli/audit.mjs +943 -0
  12. package/cli/data.mjs +176 -0
  13. package/cli/mcp.mjs +216 -0
  14. package/cli/migrations.mjs +128 -0
  15. package/cli/packet.mjs +267 -0
  16. package/cli/search.mjs +246 -0
  17. package/cli/tools.mjs +229 -0
  18. package/design-principles.json +30 -0
  19. package/dist/agent-data.json +1 -0
  20. package/dist/application.d.ts +340 -0
  21. package/dist/application.js +240 -0
  22. package/dist/auth.d.ts +121 -0
  23. package/dist/auth.js +97 -0
  24. package/dist/brand.d.ts +69 -0
  25. package/dist/brand.js +266 -0
  26. package/dist/calendar.d.ts +75 -0
  27. package/dist/calendar.js +310 -0
  28. package/dist/choice.d.ts +36 -0
  29. package/dist/choice.js +36 -0
  30. package/dist/composer.d.ts +28 -0
  31. package/dist/composer.js +47 -0
  32. package/dist/display.d.ts +136 -0
  33. package/dist/display.js +75 -0
  34. package/dist/docs.d.ts +145 -0
  35. package/dist/docs.js +141 -0
  36. package/dist/ecommerce.d.ts +261 -0
  37. package/dist/ecommerce.js +177 -0
  38. package/dist/feedback.d.ts +80 -0
  39. package/dist/feedback.js +98 -0
  40. package/dist/foundation.d.ts +42 -0
  41. package/dist/foundation.js +121 -0
  42. package/dist/heading.d.ts +5 -0
  43. package/dist/heading.js +9 -0
  44. package/dist/icons.d.ts +50 -0
  45. package/dist/icons.js +70 -0
  46. package/dist/index.d.ts +34 -0
  47. package/dist/index.js +17 -0
  48. package/dist/inspection.d.ts +24 -0
  49. package/dist/inspection.js +14 -0
  50. package/dist/internal.d.ts +492 -0
  51. package/dist/internal.js +273 -0
  52. package/dist/layout.d.ts +107 -0
  53. package/dist/layout.js +66 -0
  54. package/dist/marketing-sections.d.ts +161 -0
  55. package/dist/marketing-sections.js +58 -0
  56. package/dist/marketing.d.ts +254 -0
  57. package/dist/marketing.js +150 -0
  58. package/dist/navigation.d.ts +35 -0
  59. package/dist/navigation.js +44 -0
  60. package/dist/primitives.d.ts +258 -0
  61. package/dist/primitives.js +244 -0
  62. package/dist/showcase.d.ts +29 -0
  63. package/dist/showcase.js +10 -0
  64. package/dist/workspace.d.ts +80 -0
  65. package/dist/workspace.js +48 -0
  66. package/docs/agents.md +127 -0
  67. package/docs/auth-flows.md +71 -0
  68. package/docs/migrating-to-0.6.md +94 -0
  69. package/docs/theming.md +170 -0
  70. package/package.json +94 -4
  71. package/skills/mortar/SKILL.md +36 -0
  72. package/src/application.css +1487 -0
  73. package/src/application.tsx +1413 -0
  74. package/src/auth.tsx +452 -0
  75. package/src/brand.ts +335 -0
  76. package/src/calendar.css +468 -0
  77. package/src/calendar.tsx +868 -0
  78. package/src/choice.tsx +240 -0
  79. package/src/composer.tsx +158 -0
  80. package/src/data.css +690 -0
  81. package/src/display.tsx +452 -0
  82. package/src/docs.css +704 -0
  83. package/src/docs.tsx +737 -0
  84. package/src/ecommerce.css +810 -0
  85. package/src/ecommerce.tsx +1033 -0
  86. package/src/feedback.tsx +388 -0
  87. package/src/foundation.css +1867 -0
  88. package/src/foundation.tsx +227 -0
  89. package/src/heading.ts +13 -0
  90. package/src/icons.tsx +237 -0
  91. package/src/index.ts +230 -0
  92. package/src/inspection.tsx +91 -0
  93. package/src/internal.tsx +614 -0
  94. package/src/layout.tsx +342 -0
  95. package/src/marketing-sections.tsx +621 -0
  96. package/src/marketing.css +2400 -0
  97. package/src/marketing.tsx +1156 -0
  98. package/src/navigation.tsx +118 -0
  99. package/src/overlays.css +350 -0
  100. package/src/primitives.tsx +1135 -0
  101. package/src/showcase.tsx +120 -0
  102. package/src/tokens.css +449 -0
  103. package/src/workspace.tsx +333 -0
  104. package/styles.css +56 -0
@@ -0,0 +1,621 @@
1
+ import { IconLinks, KitLink, type IconLink } from "./navigation.js";
2
+ import { useId, useState, type FormEvent, type ReactNode } from "react";
3
+ import { ActionLink, Button, Input, type LinkAction } from "./primitives.js";
4
+ import { Avatar } from "./display.js";
5
+ import type { SectionTone } from "./layout.js";
6
+ import { childLevel, headingTag, type HeadingLevel } from "./heading.js";
7
+ import {
8
+ ActionLinks,
9
+ TermList,
10
+ TitledIntro,
11
+ cx,
12
+ componentRoot,
13
+ sectionTone,
14
+ } from "./internal.js";
15
+
16
+ export function BentoGrid({
17
+ id,
18
+ className,
19
+ eyebrow,
20
+ title,
21
+ description,
22
+ items,
23
+ tone = "default",
24
+ headingLevel = 2,
25
+ }: {
26
+ id?: string;
27
+ className?: string;
28
+ eyebrow?: string;
29
+ title: string;
30
+ description?: string;
31
+ items: {
32
+ title: string;
33
+ description: string;
34
+ visual?: ReactNode;
35
+ /** Columns the tile spans. */
36
+ span?: 1 | 2;
37
+ /** Rows the tile spans, e.g. tall tiles at both ends of a three-column grid. */
38
+ rowSpan?: 1 | 2;
39
+ /** The tile's surface; inverse remaps it to the inverse tokens. */
40
+ tone?: SectionTone;
41
+ }[];
42
+ /** The section's band; inverse gives a dark section whose tiles follow the remapped tokens. */
43
+ tone?: SectionTone;
44
+ /** The section title's level; tile titles sit one level below. */
45
+ headingLevel?: HeadingLevel;
46
+ }) {
47
+ const ItemHeading = headingTag(childLevel(headingLevel));
48
+ return (
49
+ <section
50
+ id={id}
51
+ className={cx("m-marketing-section", className)}
52
+ data-tone={sectionTone(tone)}
53
+ {...componentRoot("BentoGrid")}
54
+ >
55
+ <TitledIntro
56
+ eyebrow={eyebrow}
57
+ title={title}
58
+ description={description}
59
+ headingLevel={headingLevel}
60
+ />
61
+ <ul className="m-bento">
62
+ {items.map((item, index) => (
63
+ <li
64
+ key={`${item.title}:${index}`}
65
+ data-span={item.span || 1}
66
+ data-row-span={item.rowSpan === 2 ? 2 : undefined}
67
+ >
68
+ <article data-tone={sectionTone(item.tone)}>
69
+ {item.visual && (
70
+ <div className="m-bento-visual">{item.visual}</div>
71
+ )}
72
+ <ItemHeading>{item.title}</ItemHeading>
73
+ <p>{item.description}</p>
74
+ </article>
75
+ </li>
76
+ ))}
77
+ </ul>
78
+ </section>
79
+ );
80
+ }
81
+
82
+ export function StatGroup({
83
+ id,
84
+ className,
85
+ label,
86
+ items,
87
+ layout = "grid",
88
+ background,
89
+ }: {
90
+ id?: string;
91
+ className?: string;
92
+ label: string;
93
+ /** description: optional supporting sentence under the value. */
94
+ items: { value: string; label: string; description?: string }[];
95
+ /** grid: ruled columns. timeline: a dotted line through the items, for dated milestones (value is the date). stepped: cards that rise in height from first to last. */
96
+ layout?: "grid" | "timeline" | "stepped";
97
+ /** Decorative full-cover image behind the stats. The group switches to the inverse tone with a scrim so text keeps its contrast. */
98
+ background?: ReactNode;
99
+ }) {
100
+ const list = (
101
+ <TermList
102
+ id={background ? undefined : id}
103
+ className={cx("m-stat-group", !background && className)}
104
+ aria-label={label}
105
+ data-layout={layout === "grid" ? undefined : layout}
106
+ {...componentRoot("StatGroup", !background)}
107
+ items={items.map((item) => ({
108
+ term: item.label,
109
+ details: item.value,
110
+ after: item.description && (
111
+ <dd className="m-stat-description">{item.description}</dd>
112
+ ),
113
+ }))}
114
+ />
115
+ );
116
+ if (!background) return list;
117
+ return (
118
+ <div
119
+ id={id}
120
+ className={cx("m-stat-band", className)}
121
+ {...componentRoot("StatGroup")}
122
+ data-background="true"
123
+ data-tone="inverse"
124
+ >
125
+ <div className="m-cover-background" aria-hidden="true">
126
+ {background}
127
+ </div>
128
+ {list}
129
+ </div>
130
+ );
131
+ }
132
+
133
+ export function TestimonialGrid({
134
+ id,
135
+ className,
136
+ title,
137
+ items,
138
+ headingLevel = 2,
139
+ layout = "masonry",
140
+ }: {
141
+ id?: string;
142
+ className?: string;
143
+ title?: string;
144
+ items: {
145
+ quote: string;
146
+ name: string;
147
+ role?: string;
148
+ avatar?: string;
149
+ /** A larger quote that spans the grid's middle columns, or every column in masonry. */
150
+ featured?: boolean;
151
+ }[];
152
+ headingLevel?: HeadingLevel;
153
+ /** masonry: quotes flow down balanced columns at their own heights. grid: rows of equal height. */
154
+ layout?: "masonry" | "grid";
155
+ }) {
156
+ const Heading = headingTag(headingLevel);
157
+ return (
158
+ <section
159
+ id={id}
160
+ className={cx("m-marketing-section", className)}
161
+ aria-label={title ? undefined : "Testimonials"}
162
+ {...componentRoot("TestimonialGrid")}
163
+ >
164
+ {title && <Heading className="m-section-title">{title}</Heading>}
165
+ <div
166
+ className="m-testimonial-grid"
167
+ data-layout={layout === "grid" ? "grid" : undefined}
168
+ >
169
+ {items.map((item) => (
170
+ <figure
171
+ key={item.name}
172
+ data-featured={item.featured ? "true" : undefined}
173
+ >
174
+ <blockquote>{item.quote}</blockquote>
175
+ <figcaption>
176
+ <Avatar decorative name={item.name} src={item.avatar} size="sm" />
177
+ <span>
178
+ <strong>{item.name}</strong>
179
+ {item.role && <span>{item.role}</span>}
180
+ </span>
181
+ </figcaption>
182
+ </figure>
183
+ ))}
184
+ </div>
185
+ </section>
186
+ );
187
+ }
188
+
189
+ export function PlanComparison({
190
+ id,
191
+ className,
192
+ title,
193
+ description,
194
+ plans,
195
+ featuredPlan,
196
+ groups,
197
+ headingLevel = 2,
198
+ }: {
199
+ id?: string;
200
+ className?: string;
201
+ title: string;
202
+ description?: string;
203
+ plans: string[];
204
+ featuredPlan?: string;
205
+ groups: {
206
+ label: string;
207
+ rows: { feature: string; values: (boolean | string)[] }[];
208
+ }[];
209
+ headingLevel?: HeadingLevel;
210
+ }) {
211
+ const headingId = useId();
212
+ return (
213
+ <section
214
+ id={id}
215
+ className={cx("m-marketing-section", className)}
216
+ aria-labelledby={headingId}
217
+ {...componentRoot("PlanComparison")}
218
+ >
219
+ <TitledIntro
220
+ id={headingId}
221
+ title={title}
222
+ description={description}
223
+ headingLevel={headingLevel}
224
+ />
225
+ <div
226
+ className="m-table-scroll m-comparison"
227
+ role="region"
228
+ aria-label={`${title} table`}
229
+ tabIndex={0}
230
+ >
231
+ <table>
232
+ <thead>
233
+ <tr>
234
+ <td />
235
+ {plans.map((plan) => (
236
+ <th
237
+ key={plan}
238
+ scope="col"
239
+ data-featured={plan === featuredPlan || undefined}
240
+ >
241
+ {plan}
242
+ </th>
243
+ ))}
244
+ </tr>
245
+ </thead>
246
+ {groups.map((group) => (
247
+ <tbody key={group.label}>
248
+ <tr className="m-comparison-group">
249
+ <th scope="colgroup" colSpan={plans.length + 1}>
250
+ {group.label}
251
+ </th>
252
+ </tr>
253
+ {group.rows.map((row) => (
254
+ <tr key={row.feature}>
255
+ <th scope="row">{row.feature}</th>
256
+ {row.values.map((value, i) => (
257
+ <td
258
+ key={plans[i]}
259
+ data-featured={plans[i] === featuredPlan || undefined}
260
+ >
261
+ {typeof value === "boolean" ? (
262
+ // The glyph is decoration; the cell's text is the meaning.
263
+ <>
264
+ <span
265
+ aria-hidden="true"
266
+ className={value ? undefined : "m-comparison-no"}
267
+ >
268
+ {value ? "✓" : "—"}
269
+ </span>
270
+ <span className="m-sr-only">
271
+ {value ? "Included" : "Not included"}
272
+ </span>
273
+ </>
274
+ ) : (
275
+ value
276
+ )}
277
+ </td>
278
+ ))}
279
+ </tr>
280
+ ))}
281
+ </tbody>
282
+ ))}
283
+ </table>
284
+ </div>
285
+ </section>
286
+ );
287
+ }
288
+
289
+ export function PostList({
290
+ id,
291
+ className,
292
+ title,
293
+ description,
294
+ posts,
295
+ action,
296
+ headingLevel = 2,
297
+ itemHeadingLevel,
298
+ }: {
299
+ id?: string;
300
+ className?: string;
301
+ title: string;
302
+ description?: string;
303
+ posts: {
304
+ href: string;
305
+ title: string;
306
+ excerpt: string;
307
+ date: string;
308
+ dateLabel: string;
309
+ category?: string;
310
+ image?: ReactNode;
311
+ /** Spans the full row with the image beside a larger title; usually the first post. */
312
+ featured?: boolean;
313
+ }[];
314
+ action?: LinkAction;
315
+ headingLevel?: HeadingLevel;
316
+ /** Level of each post title; defaults to one below `headingLevel`. */
317
+ itemHeadingLevel?: HeadingLevel;
318
+ }) {
319
+ const PostHeading = headingTag(itemHeadingLevel ?? childLevel(headingLevel));
320
+ return (
321
+ <section
322
+ id={id}
323
+ className={cx("m-marketing-section", className)}
324
+ {...componentRoot("PostList")}
325
+ >
326
+ <div className="m-section-heading-row">
327
+ <div>
328
+ <TitledIntro
329
+ title={title}
330
+ description={description}
331
+ headingLevel={headingLevel}
332
+ />
333
+ </div>
334
+ {action && <ActionLink {...action} variant="secondary" />}
335
+ </div>
336
+ <ul className="m-post-list">
337
+ {posts.map((post, index) => (
338
+ <li
339
+ key={`${post.title}:${post.href}:${index}`}
340
+ data-featured={post.featured ? "true" : undefined}
341
+ >
342
+ <article>
343
+ {post.image && <div className="m-post-image">{post.image}</div>}
344
+ <p className="m-post-meta">
345
+ {post.category && <span>{post.category}</span>}
346
+ <time dateTime={post.date}>{post.dateLabel}</time>
347
+ </p>
348
+ <PostHeading className="m-post-title">
349
+ <KitLink href={post.href}>{post.title}</KitLink>
350
+ </PostHeading>
351
+ <p>{post.excerpt}</p>
352
+ </article>
353
+ </li>
354
+ ))}
355
+ </ul>
356
+ </section>
357
+ );
358
+ }
359
+
360
+ export function TeamGrid({
361
+ id,
362
+ className,
363
+ title,
364
+ description,
365
+ members,
366
+ headingLevel = 2,
367
+ layout = "grid",
368
+ }: {
369
+ id?: string;
370
+ className?: string;
371
+ title: string;
372
+ description?: string;
373
+ members: {
374
+ name: string;
375
+ role: string;
376
+ image?: ReactNode;
377
+ bio?: string;
378
+ /** Profile links, e.g. icon-only social links named "Ana on LinkedIn". */
379
+ links?: IconLink[];
380
+ }[];
381
+ headingLevel?: HeadingLevel;
382
+ /** grid: small square portraits. large: fewer columns of tall portraits. list: each portrait beside the name, role and bio. */
383
+ layout?: "grid" | "large" | "list";
384
+ }) {
385
+ return (
386
+ <section
387
+ id={id}
388
+ className={cx("m-marketing-section", className)}
389
+ {...componentRoot("TeamGrid")}
390
+ >
391
+ <TitledIntro
392
+ title={title}
393
+ description={description}
394
+ headingLevel={headingLevel}
395
+ />
396
+ <ul
397
+ className="m-team-grid"
398
+ data-layout={layout === "grid" ? undefined : layout}
399
+ >
400
+ {members.map((member) => (
401
+ <li key={member.name}>
402
+ <div className="m-team-portrait">
403
+ {member.image || (
404
+ <Avatar
405
+ decorative
406
+ name={member.name}
407
+ size="lg"
408
+ shape="square"
409
+ />
410
+ )}
411
+ </div>
412
+ <strong>{member.name}</strong>
413
+ <span>{member.role}</span>
414
+ {member.bio && <p className="m-team-bio">{member.bio}</p>}
415
+ {member.links?.length ? (
416
+ <IconLinks label={`${member.name} links`} links={member.links} />
417
+ ) : null}
418
+ </li>
419
+ ))}
420
+ </ul>
421
+ </section>
422
+ );
423
+ }
424
+
425
+ export function ContactSection({
426
+ id,
427
+ className,
428
+ title,
429
+ description,
430
+ details = [],
431
+ headingLevel = 2,
432
+ media,
433
+ offices = [],
434
+ children,
435
+ }: {
436
+ id?: string;
437
+ className?: string;
438
+ title: string;
439
+ description?: string;
440
+ details?: { label: string; value: ReactNode }[];
441
+ headingLevel?: HeadingLevel;
442
+ /** An image filling one half of a split layout, beside the intro and form; give it alt text or alt="" if decorative. */
443
+ media?: ReactNode;
444
+ /** Office locations in a grid below; each name is a heading one level below the title. */
445
+ offices?: { name: string; address: ReactNode }[];
446
+ /** The consumer's form. Compose with Input, Select, TextArea and Button. Optional when the section lists offices. */
447
+ children?: ReactNode;
448
+ }) {
449
+ const OfficeHeading = headingTag(childLevel(headingLevel));
450
+ const intro = (
451
+ <>
452
+ <TitledIntro
453
+ title={title}
454
+ description={description}
455
+ headingLevel={headingLevel}
456
+ />
457
+ {details.length > 0 && (
458
+ <TermList
459
+ items={details.map((d) => ({ term: d.label, details: d.value }))}
460
+ />
461
+ )}
462
+ </>
463
+ );
464
+ const form = children && <div className="m-contact-form">{children}</div>;
465
+ return (
466
+ <section
467
+ id={id}
468
+ className={cx("m-contact", className)}
469
+ {...componentRoot("ContactSection")}
470
+ data-media={media ? "true" : undefined}
471
+ >
472
+ {media ? (
473
+ <>
474
+ <div className="m-contact-media">{media}</div>
475
+ <div className="m-contact-body">
476
+ <div>{intro}</div>
477
+ {form}
478
+ </div>
479
+ </>
480
+ ) : (
481
+ <>
482
+ <div>{intro}</div>
483
+ {form}
484
+ </>
485
+ )}
486
+ {offices.length > 0 && (
487
+ <ul className="m-contact-offices">
488
+ {offices.map((office, index) => (
489
+ <li key={`${office.name}:${index}`}>
490
+ <OfficeHeading>{office.name}</OfficeHeading>
491
+ <address>{office.address}</address>
492
+ </li>
493
+ ))}
494
+ </ul>
495
+ )}
496
+ </section>
497
+ );
498
+ }
499
+
500
+ export function Newsletter({
501
+ id,
502
+ className,
503
+ title,
504
+ description,
505
+ onSubmit,
506
+ submitLabel = "Subscribe",
507
+ status = "idle",
508
+ message,
509
+ headingLevel = 2,
510
+ layout = "split",
511
+ variant = "plain",
512
+ }: {
513
+ id?: string;
514
+ className?: string;
515
+ title: string;
516
+ description?: string;
517
+ onSubmit: (email: string) => void;
518
+ submitLabel?: string;
519
+ /** submitting: busy and inert. success: announces `message`. error: shows `message` on the field. */
520
+ status?: "idle" | "submitting" | "success" | "error";
521
+ message?: string;
522
+ headingLevel?: HeadingLevel;
523
+ /** split: the copy beside the form. center: the copy above a centred form. */
524
+ layout?: "split" | "center";
525
+ /** plain: a section divided by a top rule. panel: a rounded, sunken card. */
526
+ variant?: "plain" | "panel";
527
+ }) {
528
+ const [email, setEmail] = useState("");
529
+ const submitting = status === "submitting";
530
+ const submit = (e: FormEvent) => {
531
+ e.preventDefault();
532
+ if (!submitting) onSubmit(email);
533
+ };
534
+ return (
535
+ <section
536
+ id={id}
537
+ className={cx("m-newsletter", className)}
538
+ {...componentRoot("Newsletter")}
539
+ data-layout={layout === "split" ? undefined : layout}
540
+ data-variant={variant === "plain" ? undefined : variant}
541
+ >
542
+ <div>
543
+ <TitledIntro
544
+ title={title}
545
+ description={description}
546
+ descriptionClassName={null}
547
+ headingLevel={headingLevel}
548
+ />
549
+ </div>
550
+ <form onSubmit={submit} aria-busy={submitting || undefined}>
551
+ <Input
552
+ label="Email address"
553
+ type="email"
554
+ autoComplete="email"
555
+ required
556
+ value={email}
557
+ onChange={(e) => setEmail(e.target.value)}
558
+ error={status === "error" ? message : undefined}
559
+ disabled={submitting}
560
+ />
561
+ <Button type="submit" loading={submitting}>
562
+ {submitLabel}
563
+ </Button>
564
+ {/* Mounted empty so the success text is announced when it arrives. */}
565
+ <p role="status" className="m-newsletter-status">
566
+ {status === "success" ? message || "Thanks for subscribing." : ""}
567
+ </p>
568
+ </form>
569
+ </section>
570
+ );
571
+ }
572
+
573
+ export function NotFound({
574
+ id,
575
+ className,
576
+ code = "404",
577
+ title,
578
+ description,
579
+ actions,
580
+ illustration,
581
+ background,
582
+ headingLevel = 1,
583
+ }: {
584
+ id?: string;
585
+ className?: string;
586
+ code?: string;
587
+ title: string;
588
+ description: string;
589
+ actions: LinkAction[];
590
+ /** Decorative artwork above the code. */
591
+ illustration?: ReactNode;
592
+ /** Decorative full-cover image behind the message. The section switches to the inverse tone with a scrim so text keeps its contrast. */
593
+ background?: ReactNode;
594
+ headingLevel?: HeadingLevel;
595
+ }) {
596
+ const Heading = headingTag(headingLevel);
597
+ return (
598
+ <section
599
+ id={id}
600
+ className={cx("m-not-found", className)}
601
+ {...componentRoot("NotFound")}
602
+ data-background={background ? true : undefined}
603
+ data-tone={background ? "inverse" : undefined}
604
+ >
605
+ {background && (
606
+ <div className="m-cover-background" aria-hidden="true">
607
+ {background}
608
+ </div>
609
+ )}
610
+ {illustration && (
611
+ <div className="m-not-found-art" aria-hidden="true">
612
+ {illustration}
613
+ </div>
614
+ )}
615
+ <p className="m-eyebrow">{code}</p>
616
+ <Heading className="m-section-title">{title}</Heading>
617
+ <p>{description}</p>
618
+ <ActionLinks actions={actions} />
619
+ </section>
620
+ );
621
+ }