@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.
- package/AGENTS.md +10 -0
- package/CHANGELOG.md +276 -0
- package/CONTRIBUTING.md +48 -0
- package/DESIGN.md +51 -0
- package/LICENSE +21 -0
- package/README.md +155 -2
- package/TESTING.md +83 -0
- package/baseline-policy.json +16 -0
- package/bin/mortar.mjs +258 -0
- package/catalog.json +9703 -0
- package/cli/audit.mjs +943 -0
- package/cli/data.mjs +176 -0
- package/cli/mcp.mjs +216 -0
- package/cli/migrations.mjs +128 -0
- package/cli/packet.mjs +267 -0
- package/cli/search.mjs +246 -0
- package/cli/tools.mjs +229 -0
- package/design-principles.json +30 -0
- package/dist/agent-data.json +1 -0
- package/dist/application.d.ts +340 -0
- package/dist/application.js +240 -0
- package/dist/auth.d.ts +121 -0
- package/dist/auth.js +97 -0
- package/dist/brand.d.ts +69 -0
- package/dist/brand.js +266 -0
- package/dist/calendar.d.ts +75 -0
- package/dist/calendar.js +310 -0
- package/dist/choice.d.ts +36 -0
- package/dist/choice.js +36 -0
- package/dist/composer.d.ts +28 -0
- package/dist/composer.js +47 -0
- package/dist/display.d.ts +136 -0
- package/dist/display.js +75 -0
- package/dist/docs.d.ts +145 -0
- package/dist/docs.js +141 -0
- package/dist/ecommerce.d.ts +261 -0
- package/dist/ecommerce.js +177 -0
- package/dist/feedback.d.ts +80 -0
- package/dist/feedback.js +98 -0
- package/dist/foundation.d.ts +42 -0
- package/dist/foundation.js +121 -0
- package/dist/heading.d.ts +5 -0
- package/dist/heading.js +9 -0
- package/dist/icons.d.ts +50 -0
- package/dist/icons.js +70 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +17 -0
- package/dist/inspection.d.ts +24 -0
- package/dist/inspection.js +14 -0
- package/dist/internal.d.ts +492 -0
- package/dist/internal.js +273 -0
- package/dist/layout.d.ts +107 -0
- package/dist/layout.js +66 -0
- package/dist/marketing-sections.d.ts +161 -0
- package/dist/marketing-sections.js +58 -0
- package/dist/marketing.d.ts +254 -0
- package/dist/marketing.js +150 -0
- package/dist/navigation.d.ts +35 -0
- package/dist/navigation.js +44 -0
- package/dist/primitives.d.ts +258 -0
- package/dist/primitives.js +244 -0
- package/dist/showcase.d.ts +29 -0
- package/dist/showcase.js +10 -0
- package/dist/workspace.d.ts +80 -0
- package/dist/workspace.js +48 -0
- package/docs/agents.md +127 -0
- package/docs/auth-flows.md +71 -0
- package/docs/migrating-to-0.6.md +94 -0
- package/docs/theming.md +170 -0
- package/package.json +94 -4
- package/skills/mortar/SKILL.md +36 -0
- package/src/application.css +1487 -0
- package/src/application.tsx +1413 -0
- package/src/auth.tsx +452 -0
- package/src/brand.ts +335 -0
- package/src/calendar.css +468 -0
- package/src/calendar.tsx +868 -0
- package/src/choice.tsx +240 -0
- package/src/composer.tsx +158 -0
- package/src/data.css +690 -0
- package/src/display.tsx +452 -0
- package/src/docs.css +704 -0
- package/src/docs.tsx +737 -0
- package/src/ecommerce.css +810 -0
- package/src/ecommerce.tsx +1033 -0
- package/src/feedback.tsx +388 -0
- package/src/foundation.css +1867 -0
- package/src/foundation.tsx +227 -0
- package/src/heading.ts +13 -0
- package/src/icons.tsx +237 -0
- package/src/index.ts +230 -0
- package/src/inspection.tsx +91 -0
- package/src/internal.tsx +614 -0
- package/src/layout.tsx +342 -0
- package/src/marketing-sections.tsx +621 -0
- package/src/marketing.css +2400 -0
- package/src/marketing.tsx +1156 -0
- package/src/navigation.tsx +118 -0
- package/src/overlays.css +350 -0
- package/src/primitives.tsx +1135 -0
- package/src/showcase.tsx +120 -0
- package/src/tokens.css +449 -0
- package/src/workspace.tsx +333 -0
- package/styles.css +56 -0
package/src/display.tsx
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { useId, type ReactNode } from "react";
|
|
2
|
+
import { KitLink } from "./navigation.js";
|
|
3
|
+
import type { Tone } from "./primitives.js";
|
|
4
|
+
import { Check, Close, Paperclip, Person } from "./icons.js";
|
|
5
|
+
import { headingTag, type HeadingLevel } from "./heading.js";
|
|
6
|
+
import {
|
|
7
|
+
TermList,
|
|
8
|
+
cx,
|
|
9
|
+
componentRoot,
|
|
10
|
+
focusAfterRemoval,
|
|
11
|
+
classedIntroSlots,
|
|
12
|
+
} from "./internal.js";
|
|
13
|
+
|
|
14
|
+
function initials(name: string) {
|
|
15
|
+
return name
|
|
16
|
+
.split(/\s+/)
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.slice(0, 2)
|
|
19
|
+
.map((part) => part.charAt(0).toUpperCase())
|
|
20
|
+
.join("");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AvatarProps {
|
|
24
|
+
name: string;
|
|
25
|
+
src?: string;
|
|
26
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl";
|
|
27
|
+
shape?: "circle" | "square";
|
|
28
|
+
status?: "online" | "offline";
|
|
29
|
+
/** Corner of the status dot; `top` suits notification indicators. */
|
|
30
|
+
statusPosition?: "bottom" | "top";
|
|
31
|
+
/** Shown when there is no `src`: the name's initials or a generic person icon. */
|
|
32
|
+
placeholder?: "initials" | "icon";
|
|
33
|
+
/** Hide the avatar from assistive technology when the name is already visible beside it. */
|
|
34
|
+
decorative?: boolean;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
export function Avatar({
|
|
38
|
+
name,
|
|
39
|
+
src,
|
|
40
|
+
size = "md",
|
|
41
|
+
shape = "circle",
|
|
42
|
+
status,
|
|
43
|
+
statusPosition = "bottom",
|
|
44
|
+
placeholder = "initials",
|
|
45
|
+
decorative = false,
|
|
46
|
+
className,
|
|
47
|
+
}: AvatarProps) {
|
|
48
|
+
return (
|
|
49
|
+
<span
|
|
50
|
+
{...componentRoot("Avatar")}
|
|
51
|
+
className={cx(`m-avatar m-avatar-${size}`, className)}
|
|
52
|
+
data-shape={shape === "circle" ? undefined : shape}
|
|
53
|
+
{...(decorative
|
|
54
|
+
? { "aria-hidden": true }
|
|
55
|
+
: {
|
|
56
|
+
role: "img",
|
|
57
|
+
"aria-label": status ? `${name}, ${status}` : name,
|
|
58
|
+
})}
|
|
59
|
+
>
|
|
60
|
+
{src ? (
|
|
61
|
+
<img src={src} alt="" />
|
|
62
|
+
) : placeholder === "icon" ? (
|
|
63
|
+
<Person className="m-avatar-placeholder" />
|
|
64
|
+
) : (
|
|
65
|
+
<span aria-hidden="true">{initials(name)}</span>
|
|
66
|
+
)}
|
|
67
|
+
{status && (
|
|
68
|
+
<span
|
|
69
|
+
className="m-avatar-status"
|
|
70
|
+
data-status={status}
|
|
71
|
+
data-position={statusPosition === "top" ? "top" : undefined}
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
</span>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface AvatarGroupProps {
|
|
80
|
+
label: string;
|
|
81
|
+
people: { name: string; src?: string }[];
|
|
82
|
+
max?: number;
|
|
83
|
+
className?: string;
|
|
84
|
+
}
|
|
85
|
+
export function AvatarGroup({
|
|
86
|
+
label,
|
|
87
|
+
people,
|
|
88
|
+
max = 4,
|
|
89
|
+
className,
|
|
90
|
+
}: AvatarGroupProps) {
|
|
91
|
+
const shown = people.slice(0, max),
|
|
92
|
+
rest = people.length - shown.length;
|
|
93
|
+
return (
|
|
94
|
+
<ul
|
|
95
|
+
{...componentRoot("AvatarGroup")}
|
|
96
|
+
className={cx("m-avatar-group", className)}
|
|
97
|
+
aria-label={label}
|
|
98
|
+
>
|
|
99
|
+
{shown.map((person) => (
|
|
100
|
+
<li key={person.name}>
|
|
101
|
+
<Avatar name={person.name} src={person.src} size="sm" />
|
|
102
|
+
</li>
|
|
103
|
+
))}
|
|
104
|
+
{rest > 0 && (
|
|
105
|
+
<li>
|
|
106
|
+
<span
|
|
107
|
+
className="m-avatar m-avatar-sm m-avatar-more"
|
|
108
|
+
role="img"
|
|
109
|
+
aria-label={`${rest} more`}
|
|
110
|
+
>
|
|
111
|
+
+{rest}
|
|
112
|
+
</span>
|
|
113
|
+
</li>
|
|
114
|
+
)}
|
|
115
|
+
</ul>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface KbdProps {
|
|
120
|
+
keys: string[];
|
|
121
|
+
className?: string;
|
|
122
|
+
}
|
|
123
|
+
export function Kbd({ keys, className }: KbdProps) {
|
|
124
|
+
return (
|
|
125
|
+
<span {...componentRoot("Kbd")} className={cx("m-kbd-group", className)}>
|
|
126
|
+
{keys.map((key, i) => (
|
|
127
|
+
<kbd key={i} className="m-kbd">
|
|
128
|
+
{key}
|
|
129
|
+
</kbd>
|
|
130
|
+
))}
|
|
131
|
+
</span>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ProgressBarProps {
|
|
136
|
+
label: string;
|
|
137
|
+
value: number;
|
|
138
|
+
max?: number;
|
|
139
|
+
valueLabel?: string;
|
|
140
|
+
/** Status colour for the bar; omit for the accent colour. */
|
|
141
|
+
tone?: Tone;
|
|
142
|
+
className?: string;
|
|
143
|
+
}
|
|
144
|
+
export function ProgressBar({
|
|
145
|
+
label,
|
|
146
|
+
value,
|
|
147
|
+
max = 100,
|
|
148
|
+
valueLabel,
|
|
149
|
+
tone,
|
|
150
|
+
className,
|
|
151
|
+
}: ProgressBarProps) {
|
|
152
|
+
const labelId = useId();
|
|
153
|
+
const percent = Math.min(100, Math.max(0, (value / max) * 100));
|
|
154
|
+
return (
|
|
155
|
+
<div
|
|
156
|
+
{...componentRoot("ProgressBar")}
|
|
157
|
+
className={cx("m-progress", className)}
|
|
158
|
+
data-tone={tone}
|
|
159
|
+
>
|
|
160
|
+
<div className="m-progress-label">
|
|
161
|
+
<span id={labelId}>{label}</span>
|
|
162
|
+
{valueLabel && <span aria-hidden="true">{valueLabel}</span>}
|
|
163
|
+
</div>
|
|
164
|
+
<div
|
|
165
|
+
className="m-progress-track"
|
|
166
|
+
role="progressbar"
|
|
167
|
+
aria-labelledby={labelId}
|
|
168
|
+
aria-valuemin={0}
|
|
169
|
+
aria-valuemax={max}
|
|
170
|
+
aria-valuenow={value}
|
|
171
|
+
aria-valuetext={valueLabel}
|
|
172
|
+
>
|
|
173
|
+
<span style={{ width: `${percent}%` }} />
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface FilterChipProps {
|
|
180
|
+
label: ReactNode;
|
|
181
|
+
selected?: boolean;
|
|
182
|
+
onClick?: () => void;
|
|
183
|
+
/** Shows a remove button; focus moves to the next chip once the chip is gone. */
|
|
184
|
+
onRemove?: () => void;
|
|
185
|
+
className?: string;
|
|
186
|
+
}
|
|
187
|
+
export function FilterChip({
|
|
188
|
+
label,
|
|
189
|
+
selected = false,
|
|
190
|
+
onClick,
|
|
191
|
+
onRemove,
|
|
192
|
+
className,
|
|
193
|
+
}: FilterChipProps) {
|
|
194
|
+
if (onRemove)
|
|
195
|
+
return (
|
|
196
|
+
<span
|
|
197
|
+
{...componentRoot("FilterChip")}
|
|
198
|
+
className={cx("m-chip", className)}
|
|
199
|
+
data-selected="true"
|
|
200
|
+
>
|
|
201
|
+
{label}
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={(event) => {
|
|
205
|
+
const chip = event.currentTarget.parentElement;
|
|
206
|
+
if (chip) focusAfterRemoval(chip);
|
|
207
|
+
onRemove();
|
|
208
|
+
}}
|
|
209
|
+
aria-label={`Remove ${typeof label === "string" ? label : "filter"}`}
|
|
210
|
+
>
|
|
211
|
+
<Close />
|
|
212
|
+
</button>
|
|
213
|
+
</span>
|
|
214
|
+
);
|
|
215
|
+
return (
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
{...componentRoot("FilterChip")}
|
|
219
|
+
className={cx("m-chip", className)}
|
|
220
|
+
aria-pressed={selected}
|
|
221
|
+
data-selected={selected || undefined}
|
|
222
|
+
onClick={onClick}
|
|
223
|
+
>
|
|
224
|
+
{!selected && <span aria-hidden="true">+</span>}
|
|
225
|
+
{label}
|
|
226
|
+
</button>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface CheckListProps {
|
|
231
|
+
items: ReactNode[];
|
|
232
|
+
label?: string;
|
|
233
|
+
columns?: 1 | 2;
|
|
234
|
+
size?: "sm" | "md";
|
|
235
|
+
className?: string;
|
|
236
|
+
}
|
|
237
|
+
/** Feature or inclusion list with a check marker; replaces ad-hoc list-disc styling. */
|
|
238
|
+
export function CheckList({
|
|
239
|
+
items,
|
|
240
|
+
label,
|
|
241
|
+
columns = 1,
|
|
242
|
+
size = "md",
|
|
243
|
+
className,
|
|
244
|
+
}: CheckListProps) {
|
|
245
|
+
return (
|
|
246
|
+
<ul
|
|
247
|
+
{...componentRoot("CheckList")}
|
|
248
|
+
className={cx("m-check-list", className)}
|
|
249
|
+
aria-label={label}
|
|
250
|
+
data-columns={columns === 1 ? undefined : columns}
|
|
251
|
+
data-size={size === "md" ? undefined : size}
|
|
252
|
+
>
|
|
253
|
+
{items.map((item, i) => (
|
|
254
|
+
<li key={i}>
|
|
255
|
+
<Check variant="list" />
|
|
256
|
+
<span>{item}</span>
|
|
257
|
+
</li>
|
|
258
|
+
))}
|
|
259
|
+
</ul>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface DescriptionListProps {
|
|
264
|
+
title?: string;
|
|
265
|
+
description?: string;
|
|
266
|
+
headingLevel?: HeadingLevel;
|
|
267
|
+
items: { term: string; details: ReactNode; action?: ReactNode }[];
|
|
268
|
+
layout?: "columns" | "stacked";
|
|
269
|
+
/**
|
|
270
|
+
* `striped` shades alternate rows instead of dividing them. To show the
|
|
271
|
+
* list in a card, place it in a Card with the title on the card.
|
|
272
|
+
*/
|
|
273
|
+
variant?: "plain" | "striped";
|
|
274
|
+
id?: string;
|
|
275
|
+
className?: string;
|
|
276
|
+
}
|
|
277
|
+
/** Label/value pairs for detail screens, stacked on narrow screens and in columns when wide. */
|
|
278
|
+
export function DescriptionList({
|
|
279
|
+
title,
|
|
280
|
+
description,
|
|
281
|
+
headingLevel = 2,
|
|
282
|
+
items,
|
|
283
|
+
layout = "columns",
|
|
284
|
+
variant = "plain",
|
|
285
|
+
id,
|
|
286
|
+
className,
|
|
287
|
+
}: DescriptionListProps) {
|
|
288
|
+
return (
|
|
289
|
+
<div
|
|
290
|
+
id={id}
|
|
291
|
+
{...componentRoot("DescriptionList")}
|
|
292
|
+
className={cx("m-description-list", className)}
|
|
293
|
+
data-layout={layout === "columns" ? undefined : layout}
|
|
294
|
+
data-variant={variant === "plain" ? undefined : variant}
|
|
295
|
+
>
|
|
296
|
+
{title && (
|
|
297
|
+
<div className="m-description-heading">
|
|
298
|
+
{...classedIntroSlots(
|
|
299
|
+
{
|
|
300
|
+
title,
|
|
301
|
+
description,
|
|
302
|
+
headingLevel,
|
|
303
|
+
descriptionClassName: null,
|
|
304
|
+
},
|
|
305
|
+
"m-description-title",
|
|
306
|
+
)}
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
<TermList
|
|
310
|
+
items={items.map((item) => ({
|
|
311
|
+
term: item.term,
|
|
312
|
+
details: item.details,
|
|
313
|
+
attributes: { "data-action": item.action ? true : undefined },
|
|
314
|
+
after: item.action && (
|
|
315
|
+
<dd className="m-description-action">{item.action}</dd>
|
|
316
|
+
),
|
|
317
|
+
}))}
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export interface GridListItem {
|
|
324
|
+
id: string;
|
|
325
|
+
title: string;
|
|
326
|
+
description?: string;
|
|
327
|
+
/** Destination for the card; its title link covers the whole card. */
|
|
328
|
+
href?: string;
|
|
329
|
+
/** Avatar or icon shown beside the title. */
|
|
330
|
+
media?: ReactNode;
|
|
331
|
+
/** Image across the top of the card. Use alt="" when the title already describes it. */
|
|
332
|
+
cover?: { src: string; alt: string };
|
|
333
|
+
/** Badges, dates or counts under the description. */
|
|
334
|
+
meta?: ReactNode;
|
|
335
|
+
/** Term/value pairs under the body, e.g. a client card's last invoice and amount. */
|
|
336
|
+
details?: { term: string; details: ReactNode }[];
|
|
337
|
+
actions?: ReactNode;
|
|
338
|
+
}
|
|
339
|
+
export interface GridListProps {
|
|
340
|
+
label: string;
|
|
341
|
+
items: GridListItem[];
|
|
342
|
+
columns?: 2 | 3 | 4;
|
|
343
|
+
/** Level of each card's title. */
|
|
344
|
+
itemHeadingLevel?: HeadingLevel;
|
|
345
|
+
/** `split` gives each action an equal cell divided by shared borders, e.g. Email | Call. */
|
|
346
|
+
actionsLayout?: "inline" | "split";
|
|
347
|
+
className?: string;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Responsive grid of cards for people, projects or files. A card's title link
|
|
351
|
+
* covers the card; links and buttons in `media`, `meta`, `details` and
|
|
352
|
+
* `actions` sit above it and stay clickable.
|
|
353
|
+
*/
|
|
354
|
+
export function GridList({
|
|
355
|
+
label,
|
|
356
|
+
items,
|
|
357
|
+
columns = 3,
|
|
358
|
+
itemHeadingLevel = 3,
|
|
359
|
+
actionsLayout = "inline",
|
|
360
|
+
className,
|
|
361
|
+
}: GridListProps) {
|
|
362
|
+
const Heading = headingTag(itemHeadingLevel);
|
|
363
|
+
return (
|
|
364
|
+
<ul
|
|
365
|
+
{...componentRoot("GridList")}
|
|
366
|
+
className={cx("m-grid-list", className)}
|
|
367
|
+
aria-label={label}
|
|
368
|
+
data-columns={columns === 3 ? undefined : columns}
|
|
369
|
+
data-actions={actionsLayout === "split" ? "split" : undefined}
|
|
370
|
+
>
|
|
371
|
+
{items.map((item) => (
|
|
372
|
+
<li key={item.id}>
|
|
373
|
+
{item.cover && (
|
|
374
|
+
<div className="m-grid-list-cover">
|
|
375
|
+
<img src={item.cover.src} alt={item.cover.alt} loading="lazy" />
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
<div className="m-grid-list-body">
|
|
379
|
+
{item.media && (
|
|
380
|
+
<span className="m-grid-list-media">{item.media}</span>
|
|
381
|
+
)}
|
|
382
|
+
<div className="m-grid-list-text">
|
|
383
|
+
<Heading className="m-grid-list-title">
|
|
384
|
+
{item.href ? (
|
|
385
|
+
<KitLink href={item.href}>{item.title}</KitLink>
|
|
386
|
+
) : (
|
|
387
|
+
item.title
|
|
388
|
+
)}
|
|
389
|
+
</Heading>
|
|
390
|
+
{item.description && <p>{item.description}</p>}
|
|
391
|
+
{item.meta && <div className="m-grid-list-meta">{item.meta}</div>}
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
{item.details && item.details.length > 0 && (
|
|
395
|
+
<TermList className="m-grid-list-details" items={item.details} />
|
|
396
|
+
)}
|
|
397
|
+
{item.actions && (
|
|
398
|
+
<div className="m-grid-list-actions">{item.actions}</div>
|
|
399
|
+
)}
|
|
400
|
+
</li>
|
|
401
|
+
))}
|
|
402
|
+
</ul>
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export interface Attachment {
|
|
407
|
+
id: string;
|
|
408
|
+
name: string;
|
|
409
|
+
href: string;
|
|
410
|
+
/** Preformatted size, e.g. "2.4 MB". */
|
|
411
|
+
size?: string;
|
|
412
|
+
/** Link text; the file name is appended for screen readers. Defaults to "Download". */
|
|
413
|
+
actionLabel?: string;
|
|
414
|
+
icon?: ReactNode;
|
|
415
|
+
}
|
|
416
|
+
export interface AttachmentListProps {
|
|
417
|
+
files: Attachment[];
|
|
418
|
+
label?: string;
|
|
419
|
+
className?: string;
|
|
420
|
+
}
|
|
421
|
+
/** Files attached to a record, each with a size and a named download link. */
|
|
422
|
+
export function AttachmentList({
|
|
423
|
+
files,
|
|
424
|
+
label = "Attachments",
|
|
425
|
+
className,
|
|
426
|
+
}: AttachmentListProps) {
|
|
427
|
+
return (
|
|
428
|
+
<ul
|
|
429
|
+
{...componentRoot("AttachmentList")}
|
|
430
|
+
className={cx("m-attachments", className)}
|
|
431
|
+
aria-label={label}
|
|
432
|
+
>
|
|
433
|
+
{files.map((file) => (
|
|
434
|
+
<li key={file.id}>
|
|
435
|
+
<span className="m-attachment-icon" aria-hidden="true">
|
|
436
|
+
{file.icon ?? <Paperclip />}
|
|
437
|
+
</span>
|
|
438
|
+
<span className="m-attachment-text">
|
|
439
|
+
<span className="m-attachment-name">{file.name}</span>
|
|
440
|
+
{file.size && (
|
|
441
|
+
<span className="m-attachment-size">{file.size}</span>
|
|
442
|
+
)}
|
|
443
|
+
</span>
|
|
444
|
+
<KitLink href={file.href}>
|
|
445
|
+
{file.actionLabel ?? "Download"}
|
|
446
|
+
<span className="m-sr-only"> {file.name}</span>
|
|
447
|
+
</KitLink>
|
|
448
|
+
</li>
|
|
449
|
+
))}
|
|
450
|
+
</ul>
|
|
451
|
+
);
|
|
452
|
+
}
|