rift-ds 1.3.0 → 1.4.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 (37) hide show
  1. package/README.md +2 -2
  2. package/components/Attachment/Attachment.css +1036 -0
  3. package/components/Attachment/AttachmentDropZone.d.ts +39 -0
  4. package/components/Attachment/AttachmentDropZone.js +76 -0
  5. package/components/Attachment/AttachmentGroup.d.ts +60 -0
  6. package/components/Attachment/AttachmentGroup.js +179 -0
  7. package/components/Attachment/AttachmentTile.d.ts +80 -0
  8. package/components/Attachment/AttachmentTile.js +143 -0
  9. package/components/Attachment/AttachmentViewer.d.ts +66 -0
  10. package/components/Attachment/AttachmentViewer.js +215 -0
  11. package/components/Attachment/fileTypes.d.ts +101 -0
  12. package/components/Attachment/fileTypes.js +182 -0
  13. package/components/Attachment/typeMark.d.ts +11 -0
  14. package/components/Attachment/typeMark.js +51 -0
  15. package/components/Attachment/useAttachments.d.ts +65 -0
  16. package/components/Attachment/useAttachments.js +201 -0
  17. package/components/ChatMessage/ChatMessage.css +24 -0
  18. package/components/ChatMessage/ChatMessage.d.ts +6 -0
  19. package/components/ChatMessage/ChatMessage.js +8 -5
  20. package/components/Composer/Composer.css +95 -2
  21. package/components/Composer/Composer.d.ts +57 -8
  22. package/components/Composer/Composer.js +179 -6
  23. package/components/DocumentChip/DocumentChip.d.ts +7 -5
  24. package/components/FileInput/FileInput.js +1 -7
  25. package/components/MessageCard/MessageCard.d.ts +1 -1
  26. package/components/registry.json +38 -2
  27. package/components/registry.json.d.ts +38 -2
  28. package/components/registry.json.js +1 -1
  29. package/index.d.ts +4 -0
  30. package/index.js +19 -0
  31. package/package.json +9 -1
  32. package/tokens/registry.json +4 -0
  33. package/tokens/registry.json.d.ts +4 -0
  34. package/tokens/registry.json.js +1 -1
  35. package/tokens/tokens-dark.css +6 -0
  36. package/tokens/tokens-light.css +12 -0
  37. package/tokens/tokens-primitives.css +10 -0
@@ -0,0 +1,1036 @@
1
+ /* ============================================
2
+ ATTACHMENT FAMILY
3
+ AttachmentTile: one file as a square.
4
+ AttachmentGroup: the files on a sent message.
5
+ AttachmentViewer: the preview dialog.
6
+ AttachmentDropZone: the drop target over a chat.
7
+ One stylesheet, shared by every member.
8
+ ============================================ */
9
+
10
+ /* ============================================
11
+ TYPE MARK — what kind of file this is
12
+ Shared by the tile and the viewer. Every mark, and every glyph a tile
13
+ swaps it for (spinner, alert, unavailable), fills one square box of the
14
+ same edge, so a file changing state never moves or resizes anything.
15
+ ============================================ */
16
+
17
+ .ds-attachment-mark {
18
+ /* Consumer and parent hook: the square's edge */
19
+ --ds-attachment-mark-edge: var(--ds-attachment-mark-size, var(--icon-size-800));
20
+
21
+ flex: none;
22
+ color: var(--color-text-secondary);
23
+ }
24
+
25
+ .ds-attachment-mark--pdf {
26
+ color: var(--color-file-pdf);
27
+ }
28
+
29
+ .ds-attachment-mark--spreadsheet {
30
+ color: var(--color-file-spreadsheet);
31
+ }
32
+
33
+ .ds-attachment-mark--document {
34
+ color: var(--color-file-document);
35
+ }
36
+
37
+ .ds-attachment-mark--presentation {
38
+ color: var(--color-file-presentation);
39
+ }
40
+
41
+ /* ---------- Icon form: one square ---------- */
42
+
43
+ .ds-attachment-mark--icon {
44
+ display: inline-flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ width: var(--ds-attachment-mark-edge);
48
+ height: var(--ds-attachment-mark-edge);
49
+ }
50
+
51
+ .ds-attachment-mark__glyph {
52
+ display: block;
53
+ width: 100%;
54
+ height: 100%;
55
+ overflow: visible;
56
+ }
57
+
58
+ /* The folded corner: a darker flap, as on the icons these formats wear in
59
+ the wild. The scrim over the silhouette deepens whichever colour is
60
+ beneath it, so no second colour per format is needed. */
61
+ .ds-attachment-mark__fold {
62
+ fill: var(--color-scrim);
63
+ opacity: 0.45;
64
+ }
65
+
66
+ /* The lettering under the page. */
67
+ /* ds-allow(typography): lettering inside an icon. The size and weight are
68
+ in the glyph's own 32-unit drawing space and scale with the mark, like a
69
+ stroke width; they are not a step on the type scale. */
70
+ .ds-attachment-mark__letters {
71
+ /* A text colour, never the format colour: the letters have to be read,
72
+ and a presentation's yellow cannot carry text on a light tile */
73
+ fill: var(--color-text-secondary);
74
+ font-family: var(--font-family-body);
75
+ font-size: 10px;
76
+ font-weight: 800;
77
+ letter-spacing: 0.04em;
78
+ }
79
+
80
+ /* A neutral file's extension, as a chip centred in the same square. It may
81
+ run wider than the square (a five-letter extension) but never taller. */
82
+ .ds-attachment-mark__chip,
83
+ .ds-attachment-mark--badge {
84
+ display: inline-flex;
85
+ align-items: center;
86
+ padding: var(--padding-050) var(--padding-150);
87
+ border-radius: var(--radius-100);
88
+ background-color: var(--color-bg-container-secondary);
89
+ font-family: var(--font-caption-family);
90
+ font-size: var(--font-caption-size);
91
+ font-weight: var(--font-overline-weight);
92
+ line-height: var(--font-caption-line-height);
93
+ letter-spacing: var(--font-caption-letter-spacing);
94
+ text-transform: uppercase;
95
+ white-space: nowrap;
96
+ }
97
+
98
+ /* ---------- Badge form: the label alone, over a preview's corner ---------- */
99
+
100
+ .ds-attachment-mark--badge {
101
+ gap: var(--gap-100);
102
+ background-color: var(--color-bg-container-primary);
103
+ /* The label reads in a text colour; the format's own colour rides in
104
+ the dot beside it */
105
+ color: var(--color-text-secondary);
106
+ }
107
+
108
+ .ds-attachment-mark__dot {
109
+ flex: none;
110
+ width: var(--gap-200);
111
+ height: var(--gap-200);
112
+ border-radius: var(--radius-050);
113
+ }
114
+
115
+ .ds-attachment-mark--pdf .ds-attachment-mark__dot {
116
+ background-color: var(--color-file-pdf);
117
+ }
118
+
119
+ .ds-attachment-mark--spreadsheet .ds-attachment-mark__dot {
120
+ background-color: var(--color-file-spreadsheet);
121
+ }
122
+
123
+ .ds-attachment-mark--document .ds-attachment-mark__dot {
124
+ background-color: var(--color-file-document);
125
+ }
126
+
127
+ .ds-attachment-mark--presentation .ds-attachment-mark__dot {
128
+ background-color: var(--color-file-presentation);
129
+ }
130
+
131
+ /* ============================================
132
+ TILE
133
+ ============================================ */
134
+
135
+ .ds-attachment-tile {
136
+ /* Consumer and parent hooks. A parent that seats tiles inside its own
137
+ rounded shell (Composer's tray) repoints the radius so the corners
138
+ stay concentric; the size is the one number that sets the whole tile.
139
+ The edge lengths are raw values: a tile is a fixed object like an
140
+ avatar or an icon button, not a step on the spacing scale. */
141
+ --ds-attachment-tile-edge: var(--ds-attachment-tile-size, 136px);
142
+ --ds-attachment-tile-corner: var(--ds-attachment-tile-radius, var(--radius-400));
143
+ /* Border indirection, so the error state repoints one variable */
144
+ --ds-attachment-tile-border: var(--color-bg-container-border);
145
+ /* The remove button's diameter; its offset derives from it */
146
+ --ds-attachment-tile-remove-size: 24px;
147
+
148
+ position: relative;
149
+ flex: none;
150
+ box-sizing: border-box;
151
+ display: inline-flex;
152
+ width: var(--ds-attachment-tile-edge);
153
+ height: var(--ds-attachment-tile-edge);
154
+ border: var(--border-025) solid var(--ds-attachment-tile-border);
155
+ border-radius: var(--ds-attachment-tile-corner);
156
+ background-color: var(--color-bg-container-primary);
157
+ color: var(--color-text-primary);
158
+ transition: border-color var(--motion-duration-base) var(--motion-ease-standard);
159
+ }
160
+
161
+ .ds-attachment-tile--compact {
162
+ --ds-attachment-tile-edge: var(--ds-attachment-tile-size, 96px);
163
+ }
164
+
165
+ /* ---------- State ---------- */
166
+
167
+ .ds-attachment-tile--error {
168
+ --ds-attachment-tile-border: var(--color-status-error-border);
169
+ }
170
+
171
+ /* ============================================
172
+ BODY — the whole square; a button when clickable
173
+ ============================================ */
174
+
175
+ .ds-attachment-tile__body {
176
+ position: relative;
177
+ box-sizing: border-box;
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ width: 100%;
182
+ height: 100%;
183
+ min-width: 0;
184
+ margin: 0;
185
+ padding: var(--padding-300);
186
+ border: none;
187
+ /* Inside the border, so the inner corner is the outer one less the line */
188
+ border-radius: calc(var(--ds-attachment-tile-corner) - var(--border-025));
189
+ background: none;
190
+ font: inherit;
191
+ letter-spacing: inherit;
192
+ color: inherit;
193
+ text-align: center;
194
+ /* Full-bleed content is clipped to the tile's own corner */
195
+ overflow: hidden;
196
+ }
197
+
198
+ button.ds-attachment-tile__body {
199
+ cursor: pointer;
200
+ transition: background-color var(--motion-duration-base) var(--motion-ease-standard);
201
+ }
202
+
203
+ /* Hover: a pressable tile lifts its border and warms its surface, and a
204
+ picture brightens under a light wash. The same answer on every tile, so
205
+ the grid reads as one set of controls. */
206
+ button.ds-attachment-tile__body:hover {
207
+ background-color: var(--color-bg-container-secondary);
208
+ }
209
+
210
+ .ds-attachment-tile--clickable:hover {
211
+ --ds-attachment-tile-border: var(--color-text-tertiary);
212
+ }
213
+
214
+ .ds-attachment-tile--clickable.ds-attachment-tile--error:hover {
215
+ --ds-attachment-tile-border: var(--color-status-error-border);
216
+ }
217
+
218
+ .ds-attachment-tile__preview,
219
+ .ds-attachment-group__single-image {
220
+ transition:
221
+ opacity var(--motion-duration-base) var(--motion-ease-standard),
222
+ transform var(--motion-duration-slow) var(--motion-ease-emphasized);
223
+ }
224
+
225
+ button.ds-attachment-tile__body:hover .ds-attachment-tile__preview,
226
+ button.ds-attachment-group__single:hover .ds-attachment-group__single-image {
227
+ opacity: 0.88;
228
+ transform: scale(1.03);
229
+ }
230
+
231
+ /* Full-bleed tiles give the whole square to their content */
232
+ .ds-attachment-tile--preview .ds-attachment-tile__body,
233
+ .ds-attachment-tile--excerpt .ds-attachment-tile__body {
234
+ padding: 0;
235
+ }
236
+
237
+ /* ============================================
238
+ STACK — type mark, name, secondary line
239
+ ============================================ */
240
+
241
+ .ds-attachment-tile__stack {
242
+ display: flex;
243
+ flex-direction: column;
244
+ align-items: center;
245
+ gap: var(--gap-050);
246
+ width: 100%;
247
+ min-width: 0;
248
+ }
249
+
250
+ .ds-attachment-tile__mark {
251
+ display: inline-flex;
252
+ align-items: center;
253
+ justify-content: center;
254
+ /* One square seat for every mark: the type mark, the spinner and the
255
+ state glyphs all fill it exactly */
256
+ width: var(--ds-attachment-tile-mark, var(--icon-size-800));
257
+ height: var(--ds-attachment-tile-mark, var(--icon-size-800));
258
+ margin-bottom: var(--gap-100);
259
+ /* The chip of a long extension may run wider than its seat */
260
+ overflow: visible;
261
+ }
262
+
263
+ .ds-attachment-tile__mark > * {
264
+ --ds-attachment-mark-size: var(--ds-attachment-tile-mark, var(--icon-size-800));
265
+ }
266
+
267
+ .ds-attachment-tile--compact {
268
+ --ds-attachment-tile-mark: var(--icon-size-600);
269
+ }
270
+
271
+ .ds-attachment-tile--compact .ds-attachment-tile__mark {
272
+ margin-bottom: var(--gap-050);
273
+ }
274
+
275
+ /* ---------- State glyphs ---------- */
276
+
277
+ .ds-attachment-tile__state {
278
+ --icon-size: var(--ds-attachment-tile-mark, var(--icon-size-800));
279
+ color: var(--color-icon-primary);
280
+ }
281
+
282
+ .ds-attachment-tile--error .ds-attachment-tile__state {
283
+ color: var(--color-status-error-icon);
284
+ }
285
+
286
+ .ds-attachment-tile__spinner {
287
+ display: inline-flex;
288
+ width: var(--ds-attachment-tile-mark, var(--icon-size-800));
289
+ height: var(--ds-attachment-tile-mark, var(--icon-size-800));
290
+ }
291
+
292
+ .ds-attachment-tile__spinner svg {
293
+ width: 100%;
294
+ height: 100%;
295
+ animation: ds-attachment-tile-spin var(--motion-duration-loop-spin) var(--motion-ease-linear)
296
+ infinite;
297
+ }
298
+
299
+ .ds-attachment-tile__spinner-track {
300
+ stroke: var(--color-bg-container-tertiary);
301
+ }
302
+
303
+ .ds-attachment-tile__spinner-arc {
304
+ stroke: var(--color-text-secondary);
305
+ /* A quarter of the ring (circumference 56.5 at r=9), the rest gap. The
306
+ ring is drawn to a state glyph's own circle: same diameter, same
307
+ stroke, so the spinner and the alert beside it weigh the same. */
308
+ stroke-dasharray: 14 42.5;
309
+ }
310
+
311
+ @keyframes ds-attachment-tile-spin {
312
+ from {
313
+ transform: rotate(0deg);
314
+ }
315
+
316
+ to {
317
+ transform: rotate(360deg);
318
+ }
319
+ }
320
+
321
+ /* ---------- Name: middle truncation ----------
322
+ Two halves on one line. The head gives way and ellipsizes; the tail (the
323
+ end of the stem plus the extension) never shrinks, so a long name still
324
+ says what the file is. Pure layout: no measuring, no script. */
325
+
326
+ .ds-attachment-tile__name {
327
+ display: flex;
328
+ justify-content: center;
329
+ max-width: 100%;
330
+ min-width: 0;
331
+ font-family: var(--font-paragraph-sm-family);
332
+ font-size: var(--font-paragraph-sm-size);
333
+ font-weight: var(--font-paragraph-sm-weight);
334
+ line-height: var(--font-paragraph-sm-line-height);
335
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
336
+ color: var(--color-text-primary);
337
+ /* Spaces at the cut are part of the name */
338
+ white-space: pre;
339
+ }
340
+
341
+ .ds-attachment-tile__name-head {
342
+ flex: 0 1 auto;
343
+ min-width: 0;
344
+ overflow: hidden;
345
+ text-overflow: ellipsis;
346
+ }
347
+
348
+ .ds-attachment-tile__name-tail {
349
+ flex: none;
350
+ }
351
+
352
+ .ds-attachment-tile__meta {
353
+ max-width: 100%;
354
+ overflow: hidden;
355
+ text-overflow: ellipsis;
356
+ white-space: nowrap;
357
+ font-family: var(--font-caption-family);
358
+ font-size: var(--font-caption-size);
359
+ font-weight: var(--font-caption-weight);
360
+ line-height: var(--font-caption-line-height);
361
+ letter-spacing: var(--font-caption-letter-spacing);
362
+ font-variant-numeric: tabular-nums;
363
+ color: var(--color-text-tertiary);
364
+ }
365
+
366
+ .ds-attachment-tile--error .ds-attachment-tile__meta {
367
+ color: var(--color-status-error-icon);
368
+ }
369
+
370
+ /* ============================================
371
+ FULL BLEED — a preview or a pasted excerpt
372
+ ============================================ */
373
+
374
+ .ds-attachment-tile__preview {
375
+ display: block;
376
+ width: 100%;
377
+ height: 100%;
378
+ object-fit: cover;
379
+ }
380
+
381
+ .ds-attachment-tile__excerpt {
382
+ box-sizing: border-box;
383
+ display: block;
384
+ width: 100%;
385
+ height: 100%;
386
+ padding: var(--padding-200) var(--padding-300);
387
+ overflow: hidden;
388
+ font-family: var(--font-caption-family);
389
+ font-size: var(--font-caption-size);
390
+ font-weight: var(--font-caption-weight);
391
+ line-height: var(--font-caption-line-height);
392
+ letter-spacing: var(--font-caption-letter-spacing);
393
+ color: var(--color-text-secondary);
394
+ text-align: left;
395
+ white-space: pre-wrap;
396
+ overflow-wrap: anywhere;
397
+ /* The text fades out toward the foot instead of being cut mid-line */
398
+ /* ds-allow(color): mask luminance keying — the #000 paints alpha coverage
399
+ for the fade, not a theme colour */
400
+ mask-image: linear-gradient(to bottom, #000 45%, transparent 92%);
401
+ }
402
+
403
+ /* The type mark over a full-bleed tile's bottom-left corner. It sits on a
404
+ plate of the tile surface so it reads over any picture. */
405
+ .ds-attachment-tile__corner {
406
+ position: absolute;
407
+ left: var(--padding-150);
408
+ bottom: var(--padding-150);
409
+ display: inline-flex;
410
+ }
411
+
412
+ /* ============================================
413
+ REMOVE — a small circle straddling the corner
414
+ ============================================ */
415
+
416
+ .ds-attachment-tile__remove {
417
+ position: absolute;
418
+ /* A third of the button hangs outside the tile, as the corner control of
419
+ a removable thumbnail does on every platform */
420
+ top: calc(var(--ds-attachment-tile-remove-size) / -3);
421
+ right: calc(var(--ds-attachment-tile-remove-size) / -3);
422
+ display: inline-flex;
423
+ align-items: center;
424
+ justify-content: center;
425
+ width: var(--ds-attachment-tile-remove-size);
426
+ height: var(--ds-attachment-tile-remove-size);
427
+ margin: 0;
428
+ padding: 0;
429
+ border: var(--border-025) solid var(--color-bg-container-border);
430
+ border-radius: var(--radius-pill);
431
+ background-color: var(--color-bg-container-primary);
432
+ color: var(--color-text-secondary);
433
+ cursor: pointer;
434
+ /* Opacity, never visibility: a hidden button would leave the tab order
435
+ and a keyboard user could never reveal it (ChatMessage's actions row
436
+ owns the same rule) */
437
+ opacity: 0;
438
+ transition:
439
+ opacity var(--motion-duration-fast) var(--motion-ease-standard),
440
+ color var(--motion-duration-base) var(--motion-ease-standard),
441
+ background-color var(--motion-duration-base) var(--motion-ease-standard);
442
+ }
443
+
444
+ .ds-attachment-tile__remove .material-symbols-rounded {
445
+ /* ds-allow(icon-size): a 16px close glyph inside the 24px remove circle,
446
+ one step under the scale's 20px floor */
447
+ --icon-size: 16px;
448
+ color: inherit;
449
+ }
450
+
451
+ .ds-attachment-tile:hover .ds-attachment-tile__remove,
452
+ .ds-attachment-tile:focus-within .ds-attachment-tile__remove,
453
+ .ds-attachment-tile--error .ds-attachment-tile__remove {
454
+ opacity: 1;
455
+ }
456
+
457
+ /* Touch has no hover to reveal it */
458
+ @media (hover: none) {
459
+ .ds-attachment-tile__remove {
460
+ opacity: 1;
461
+ }
462
+ }
463
+
464
+ .ds-attachment-tile__remove:hover {
465
+ background-color: var(--color-bg-container-secondary);
466
+ color: var(--color-text-primary);
467
+ }
468
+
469
+ .ds-attachment-tile__remove:disabled {
470
+ opacity: 0.4;
471
+ cursor: not-allowed;
472
+ }
473
+
474
+ /* ============================================
475
+ FOCUS
476
+ ============================================ */
477
+
478
+ button.ds-attachment-tile__body:focus-visible,
479
+ .ds-attachment-tile__remove:focus-visible {
480
+ outline: 2px solid var(--color-action-primary-bg);
481
+ outline-offset: 2px;
482
+ }
483
+
484
+ /* ============================================
485
+ VISUALLY HIDDEN
486
+ ============================================ */
487
+
488
+ .ds-attachment-tile__sr-only,
489
+ .ds-attachment-group__sr-only {
490
+ position: absolute;
491
+ width: 1px;
492
+ height: 1px;
493
+ padding: 0;
494
+ margin: -1px;
495
+ overflow: hidden;
496
+ clip: rect(0, 0, 0, 0);
497
+ white-space: nowrap;
498
+ border: 0;
499
+ }
500
+
501
+ /* ============================================
502
+ REDUCED MOTION
503
+ The global guard collapses the spin to a still arc, which still reads as
504
+ "in progress" beside the words under it.
505
+ ============================================ */
506
+
507
+ @media (prefers-reduced-motion: reduce) {
508
+ .ds-attachment-tile__spinner svg {
509
+ animation: none;
510
+ }
511
+ }
512
+
513
+ /* ============================================
514
+ GROUP — the files on a sent message
515
+ ============================================ */
516
+
517
+ .ds-attachment-group {
518
+ display: flex;
519
+ flex-direction: column;
520
+ align-items: flex-start;
521
+ gap: var(--gap-200);
522
+ /* The full width of whatever holds it, so the grid can ask how much room
523
+ it has (the container query below); `align` still places the content */
524
+ width: 100%;
525
+ min-width: 0;
526
+ container-type: inline-size;
527
+ }
528
+
529
+ .ds-attachment-group--end {
530
+ align-items: flex-end;
531
+ }
532
+
533
+ /* ---------- Grid ---------- */
534
+
535
+ .ds-attachment-group__grid {
536
+ display: flex;
537
+ flex-wrap: wrap;
538
+ gap: var(--gap-200);
539
+ margin: 0;
540
+ padding: 0;
541
+ list-style: none;
542
+ max-width: 100%;
543
+ }
544
+
545
+ .ds-attachment-group--end .ds-attachment-group__grid {
546
+ justify-content: flex-end;
547
+ }
548
+
549
+ .ds-attachment-group__cell {
550
+ display: flex;
551
+ }
552
+
553
+ /* Four cells that cannot share a row would wrap three and one, which reads
554
+ as a mistake. Below the width four tiles need, they sit two by two. The
555
+ thresholds restate the default tile edges (136px, 96px compact) and the
556
+ grid gap, because a container query cannot read a custom property; a
557
+ host that resizes the tiles gets the plain wrap. */
558
+ @container (max-width: 567px) {
559
+ .ds-attachment-group--default .ds-attachment-group__grid[data-cells='4'] {
560
+ max-width: calc(2 * var(--ds-attachment-tile-size, 136px) + var(--gap-200));
561
+ }
562
+ }
563
+
564
+ @container (max-width: 407px) {
565
+ .ds-attachment-group--compact .ds-attachment-group__grid[data-cells='4'] {
566
+ max-width: calc(2 * var(--ds-attachment-tile-size, 96px) + var(--gap-200));
567
+ }
568
+ }
569
+
570
+ /* The "+N" cell: the next file, dimmed under a scrim that carries the
571
+ count. The button is the whole cell; the tile beneath is only a picture. */
572
+ .ds-attachment-group__cell--more {
573
+ position: relative;
574
+ }
575
+
576
+ .ds-attachment-group__peek {
577
+ display: flex;
578
+ pointer-events: none;
579
+ }
580
+
581
+ .ds-attachment-group__more {
582
+ position: absolute;
583
+ inset: 0;
584
+ display: inline-flex;
585
+ align-items: center;
586
+ justify-content: center;
587
+ margin: 0;
588
+ padding: 0;
589
+ border: none;
590
+ border-radius: var(--ds-attachment-tile-radius, var(--radius-400));
591
+ background-color: var(--color-scrim);
592
+ font-family: var(--font-title-body-family);
593
+ font-size: var(--font-title-body-size);
594
+ font-weight: var(--font-title-body-weight);
595
+ line-height: var(--font-title-body-line-height);
596
+ letter-spacing: var(--font-title-body-letter-spacing);
597
+ font-variant-numeric: tabular-nums;
598
+ /* ds-allow(color): on-scrim text. The scrim is dark in both themes, so
599
+ its label lives in scrim-world, not page-world (Lightbox's caption owns
600
+ the same rule) */
601
+ color: #ffffff;
602
+ cursor: pointer;
603
+ transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
604
+ }
605
+
606
+ .ds-attachment-group__more:hover {
607
+ opacity: 0.88;
608
+ }
609
+
610
+ /* "Show fewer": a quiet text button under the expanded grid */
611
+ .ds-attachment-group__less {
612
+ margin: 0;
613
+ padding: var(--padding-050) var(--padding-200);
614
+ border: none;
615
+ border-radius: var(--radius-pill);
616
+ background: none;
617
+ font-family: var(--font-paragraph-sm-emphasis-family);
618
+ font-size: var(--font-paragraph-sm-emphasis-size);
619
+ font-weight: var(--font-paragraph-sm-emphasis-weight);
620
+ line-height: var(--font-paragraph-sm-emphasis-line-height);
621
+ letter-spacing: var(--font-paragraph-sm-emphasis-letter-spacing);
622
+ color: var(--color-text-secondary);
623
+ cursor: pointer;
624
+ transition:
625
+ background-color var(--motion-duration-base) var(--motion-ease-standard),
626
+ color var(--motion-duration-base) var(--motion-ease-standard);
627
+ }
628
+
629
+ .ds-attachment-group__less:hover {
630
+ background-color: var(--color-bg-container-secondary);
631
+ color: var(--color-text-primary);
632
+ }
633
+
634
+ /* ---------- Single image: its own shape, not a square ---------- */
635
+
636
+ .ds-attachment-group__single {
637
+ position: relative;
638
+ display: block;
639
+ max-width: min(100%, var(--ds-attachment-single-max-width, 320px));
640
+ margin: 0;
641
+ padding: 0;
642
+ border: var(--border-025) solid var(--color-bg-container-border);
643
+ /* The bubble's radius: a lone picture is a message surface in its own right */
644
+ border-radius: var(--radius-600);
645
+ background: none;
646
+ overflow: hidden;
647
+ /* Collapse the inline gap under the image */
648
+ line-height: 0;
649
+ }
650
+
651
+ button.ds-attachment-group__single {
652
+ cursor: zoom-in;
653
+ transition: border-color var(--motion-duration-base) var(--motion-ease-standard);
654
+ }
655
+
656
+ button.ds-attachment-group__single:hover {
657
+ border-color: var(--color-text-tertiary);
658
+ }
659
+
660
+ /* The extension, on a plate of the surface so it reads over any picture */
661
+ .ds-attachment-group__single-mark {
662
+ position: absolute;
663
+ left: var(--padding-200);
664
+ bottom: var(--padding-200);
665
+ display: inline-flex;
666
+ line-height: normal;
667
+ }
668
+
669
+ .ds-attachment-group__single-image {
670
+ display: block;
671
+ max-width: 100%;
672
+ max-height: var(--ds-attachment-single-max-height, 320px);
673
+ width: auto;
674
+ height: auto;
675
+ }
676
+
677
+ .ds-attachment-group__more:focus-visible,
678
+ .ds-attachment-group__less:focus-visible,
679
+ button.ds-attachment-group__single:focus-visible {
680
+ outline: 2px solid var(--color-action-primary-bg);
681
+ outline-offset: 2px;
682
+ }
683
+
684
+ /* ============================================
685
+ VIEWER — the preview dialog
686
+ A centred dialog on a wide screen, a full-screen sheet on a narrow one.
687
+ ============================================ */
688
+
689
+ .ds-attachment-viewer {
690
+ position: fixed;
691
+ inset: 0;
692
+ z-index: 100;
693
+ display: flex;
694
+ align-items: center;
695
+ justify-content: center;
696
+ padding: var(--padding-1000);
697
+ opacity: 0;
698
+ visibility: hidden; /* keeps the panel's controls out of the tab order when closed */
699
+ pointer-events: none;
700
+ transition:
701
+ opacity var(--motion-duration-base) var(--motion-ease-standard),
702
+ visibility var(--motion-duration-base) var(--motion-ease-standard);
703
+ }
704
+
705
+ .ds-attachment-viewer--open {
706
+ opacity: 1;
707
+ visibility: visible;
708
+ pointer-events: auto;
709
+ }
710
+
711
+ .ds-attachment-viewer__backdrop {
712
+ position: absolute;
713
+ inset: 0;
714
+ background-color: var(--color-scrim);
715
+ }
716
+
717
+ .ds-attachment-viewer__panel {
718
+ position: relative;
719
+ z-index: 1;
720
+ display: flex;
721
+ flex-direction: column;
722
+ /* The dialog's footprint: wide enough for a page or a landscape picture,
723
+ never taller than the viewport it sits in. Lengths with no token family. */
724
+ width: min(100%, 960px);
725
+ height: min(100%, 720px);
726
+ border: var(--border-025) solid var(--color-bg-container-border);
727
+ border-radius: var(--radius-300);
728
+ background-color: var(--color-bg-page-primary);
729
+ box-shadow: var(--shadow-modal);
730
+ overflow: hidden;
731
+ outline: none;
732
+ transform: scale(0.96);
733
+ transition: transform var(--motion-duration-base) var(--motion-ease-standard);
734
+ }
735
+
736
+ .ds-attachment-viewer--open .ds-attachment-viewer__panel {
737
+ transform: scale(1);
738
+ }
739
+
740
+ /* ---------- Header ---------- */
741
+
742
+ .ds-attachment-viewer__header {
743
+ flex: none;
744
+ display: flex;
745
+ align-items: center;
746
+ gap: var(--gap-300);
747
+ padding: var(--padding-300) var(--padding-400);
748
+ border-bottom: var(--border-025) solid var(--color-bg-container-border);
749
+ background-color: var(--color-bg-container-primary);
750
+ }
751
+
752
+ .ds-attachment-viewer__header-mark {
753
+ display: inline-flex;
754
+ flex: none;
755
+ }
756
+
757
+ .ds-attachment-viewer__heading {
758
+ flex: 1;
759
+ min-width: 0;
760
+ display: flex;
761
+ flex-direction: column;
762
+ }
763
+
764
+ .ds-attachment-viewer__title {
765
+ margin: 0;
766
+ overflow: hidden;
767
+ text-overflow: ellipsis;
768
+ white-space: nowrap;
769
+ font-family: var(--font-paragraph-sm-emphasis-family);
770
+ font-size: var(--font-paragraph-sm-emphasis-size);
771
+ font-weight: var(--font-overline-weight);
772
+ line-height: var(--font-paragraph-sm-emphasis-line-height);
773
+ letter-spacing: var(--font-paragraph-sm-emphasis-letter-spacing);
774
+ color: var(--color-text-primary);
775
+ }
776
+
777
+ .ds-attachment-viewer__subtitle {
778
+ margin: 0;
779
+ font-family: var(--font-caption-family);
780
+ font-size: var(--font-caption-size);
781
+ font-weight: var(--font-caption-weight);
782
+ line-height: var(--font-caption-line-height);
783
+ letter-spacing: var(--font-caption-letter-spacing);
784
+ font-variant-numeric: tabular-nums;
785
+ color: var(--color-text-tertiary);
786
+ }
787
+
788
+ .ds-attachment-viewer__download {
789
+ flex: none;
790
+ }
791
+
792
+ /* ---------- Stage ---------- */
793
+
794
+ .ds-attachment-viewer__stage {
795
+ flex: 1;
796
+ min-height: 0;
797
+ display: flex;
798
+ /* `safe` keeps the top of tall content reachable: plain centring would
799
+ push it above the scroll origin */
800
+ align-items: safe center;
801
+ justify-content: safe center;
802
+ padding: var(--padding-400);
803
+ overflow: auto;
804
+ /* A dotted working-canvas ground: the file sits on a surface that reads
805
+ as "a place things are laid out", not a flat fill. Dot size and pitch
806
+ are drawing geometry, not spacing roles, and match the docs site's
807
+ dotted stages so every one shares a texture. */
808
+ background-color: var(--color-bg-page-primary);
809
+ background-image: radial-gradient(
810
+ circle,
811
+ var(--color-bg-container-border) 1.25px,
812
+ transparent 1.25px
813
+ );
814
+ background-size: 26px 26px;
815
+ }
816
+
817
+ .ds-attachment-viewer__image {
818
+ display: block;
819
+ max-width: 100%;
820
+ max-height: 100%;
821
+ width: auto;
822
+ height: auto;
823
+ border-radius: var(--radius-200);
824
+ }
825
+
826
+ .ds-attachment-viewer__text {
827
+ align-self: flex-start;
828
+ width: 100%;
829
+ margin: 0;
830
+ font-family: var(--font-paragraph-sm-family);
831
+ font-size: var(--font-paragraph-sm-size);
832
+ font-weight: var(--font-paragraph-sm-weight);
833
+ line-height: var(--font-paragraph-sm-line-height);
834
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
835
+ color: var(--color-text-primary);
836
+ white-space: pre-wrap;
837
+ overflow-wrap: anywhere;
838
+ }
839
+
840
+ /* Nothing to draw, or a file that failed: a small centred statement */
841
+ .ds-attachment-viewer__empty {
842
+ display: flex;
843
+ flex-direction: column;
844
+ align-items: center;
845
+ gap: var(--gap-100);
846
+ max-width: 100%;
847
+ text-align: center;
848
+ }
849
+
850
+ .ds-attachment-viewer__empty-mark {
851
+ display: inline-flex;
852
+ align-items: center;
853
+ justify-content: center;
854
+ /* A tile-sized seat for the type mark, so the empty state shows the same
855
+ object the person pressed */
856
+ width: 88px;
857
+ height: 88px;
858
+ margin-bottom: var(--gap-200);
859
+ border: var(--border-025) solid var(--color-bg-container-border);
860
+ border-radius: var(--radius-400);
861
+ background-color: var(--color-bg-container-primary);
862
+ }
863
+
864
+ .ds-attachment-viewer__empty-title {
865
+ margin: 0;
866
+ font-family: var(--font-title-body-family);
867
+ font-size: var(--font-title-body-size);
868
+ font-weight: var(--font-title-body-weight);
869
+ line-height: var(--font-title-body-line-height);
870
+ letter-spacing: var(--font-title-body-letter-spacing);
871
+ color: var(--color-text-primary);
872
+ }
873
+
874
+ .ds-attachment-viewer__empty-detail {
875
+ margin: 0;
876
+ font-family: var(--font-paragraph-sm-family);
877
+ font-size: var(--font-paragraph-sm-size);
878
+ font-weight: var(--font-paragraph-sm-weight);
879
+ line-height: var(--font-paragraph-sm-line-height);
880
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
881
+ color: var(--color-text-secondary);
882
+ overflow-wrap: anywhere;
883
+ }
884
+
885
+ .ds-attachment-viewer__empty-actions {
886
+ display: flex;
887
+ flex-wrap: wrap;
888
+ justify-content: center;
889
+ gap: var(--gap-200);
890
+ margin-top: var(--gap-300);
891
+ }
892
+
893
+ /* ---------- Footer ---------- */
894
+
895
+ .ds-attachment-viewer__footer {
896
+ flex: none;
897
+ display: flex;
898
+ align-items: center;
899
+ justify-content: center;
900
+ gap: var(--gap-300);
901
+ padding: var(--padding-200) var(--padding-400);
902
+ border-top: var(--border-025) solid var(--color-bg-container-border);
903
+ background-color: var(--color-bg-container-primary);
904
+ }
905
+
906
+ .ds-attachment-viewer__position {
907
+ /* Room for the widest count, so stepping never shifts the buttons */
908
+ min-width: 7ch;
909
+ text-align: center;
910
+ font-family: var(--font-paragraph-sm-family);
911
+ font-size: var(--font-paragraph-sm-size);
912
+ font-weight: var(--font-paragraph-sm-weight);
913
+ line-height: var(--font-paragraph-sm-line-height);
914
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
915
+ font-variant-numeric: tabular-nums;
916
+ color: var(--color-text-secondary);
917
+ }
918
+
919
+ /* ---------- Narrow: a full-screen sheet ----------
920
+ Below 30rem there is no room for a floating dialog and its margin, so
921
+ the panel takes the whole viewport. */
922
+
923
+ @media (max-width: 30rem) {
924
+ .ds-attachment-viewer {
925
+ padding: 0;
926
+ }
927
+
928
+ .ds-attachment-viewer__panel {
929
+ width: 100%;
930
+ height: 100%;
931
+ border: none;
932
+ border-radius: 0;
933
+ }
934
+ }
935
+
936
+ /* ============================================
937
+ DROP ZONE — a whole surface as a drop target
938
+ ============================================ */
939
+
940
+ .ds-attachment-drop-zone {
941
+ position: relative;
942
+ }
943
+
944
+ .ds-attachment-drop-zone__overlay {
945
+ /* The frost's strength and the glow's spread: effect lengths with no
946
+ token family, chosen by eye (Lightbox's frost and Composer's glow are
947
+ the precedents) */
948
+ --ds-attachment-drop-zone-frost: 10px;
949
+ --ds-attachment-drop-zone-glow: 14px;
950
+
951
+ position: absolute;
952
+ /* Flush with the surface it covers, corners and all, so the edge reads
953
+ as the surface's own outline lighting up */
954
+ inset: 0;
955
+ z-index: 10;
956
+ display: flex;
957
+ align-items: center;
958
+ justify-content: center;
959
+ padding: var(--padding-400);
960
+ /* A solid edge in the active-input colour: while files hover, the whole
961
+ surface is the input that is about to take them, and it wears the same
962
+ border a focused field does (Composer's own drop state uses it too) */
963
+ border: var(--border-050) solid var(--color-input-border-selected);
964
+ border-radius: inherit;
965
+ /* Frosted, not painted over: the surface blurs and dims behind the
966
+ message, so the person still sees what they are dropping onto without
967
+ its text competing with the overlay's */
968
+ background-color: color-mix(in srgb, var(--color-bg-container-primary) 60%, transparent);
969
+ backdrop-filter: blur(var(--ds-attachment-drop-zone-frost));
970
+ /* Never a target itself: the zone hears the drag, and an overlay that
971
+ caught the pointer would fire a leave on everything beneath it */
972
+ pointer-events: none;
973
+ opacity: 0;
974
+ visibility: hidden;
975
+ transition:
976
+ opacity var(--motion-duration-fast) var(--motion-ease-standard),
977
+ visibility var(--motion-duration-fast) var(--motion-ease-standard);
978
+ }
979
+
980
+ /* The glow: the edge's own colour, blurred, hugging the border inside and
981
+ out. A colour effect like the Composer's AI ring, not an elevation
982
+ shadow, so depth stays token-owned. */
983
+ .ds-attachment-drop-zone__overlay::before {
984
+ content: '';
985
+ position: absolute;
986
+ inset: calc(-1 * var(--border-050));
987
+ /* Twice the edge's weight, so the blur has enough colour to spread */
988
+ border: calc(var(--border-050) * 2) solid var(--color-input-border-selected);
989
+ border-radius: inherit;
990
+ filter: blur(var(--ds-attachment-drop-zone-glow));
991
+ opacity: 0.8;
992
+ pointer-events: none;
993
+ }
994
+
995
+ .ds-attachment-drop-zone--active .ds-attachment-drop-zone__overlay {
996
+ opacity: 1;
997
+ visibility: visible;
998
+ }
999
+
1000
+ .ds-attachment-drop-zone__message {
1001
+ display: flex;
1002
+ flex-direction: column;
1003
+ align-items: center;
1004
+ gap: var(--gap-100);
1005
+ text-align: center;
1006
+ }
1007
+
1008
+ .ds-attachment-drop-zone__icon {
1009
+ display: inline-flex;
1010
+ margin-bottom: var(--gap-100);
1011
+ /* The edge's own colour: the icon and the outline light up together */
1012
+ color: var(--color-input-border-selected);
1013
+ }
1014
+
1015
+ .ds-attachment-drop-zone__icon .material-symbols-rounded {
1016
+ --icon-size: var(--icon-size-800);
1017
+ color: inherit;
1018
+ }
1019
+
1020
+ .ds-attachment-drop-zone__label {
1021
+ font-family: var(--font-title-body-family);
1022
+ font-size: var(--font-title-body-size);
1023
+ font-weight: var(--font-title-body-weight);
1024
+ line-height: var(--font-title-body-line-height);
1025
+ letter-spacing: var(--font-title-body-letter-spacing);
1026
+ color: var(--color-text-primary);
1027
+ }
1028
+
1029
+ .ds-attachment-drop-zone__hint {
1030
+ font-family: var(--font-paragraph-sm-family);
1031
+ font-size: var(--font-paragraph-sm-size);
1032
+ font-weight: var(--font-paragraph-sm-weight);
1033
+ line-height: var(--font-paragraph-sm-line-height);
1034
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
1035
+ color: var(--color-text-secondary);
1036
+ }