rift-ds 1.2.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.
- package/README.md +2 -2
- package/components/Attachment/Attachment.css +1036 -0
- package/components/Attachment/AttachmentDropZone.d.ts +39 -0
- package/components/Attachment/AttachmentDropZone.js +76 -0
- package/components/Attachment/AttachmentGroup.d.ts +60 -0
- package/components/Attachment/AttachmentGroup.js +179 -0
- package/components/Attachment/AttachmentTile.d.ts +80 -0
- package/components/Attachment/AttachmentTile.js +143 -0
- package/components/Attachment/AttachmentViewer.d.ts +66 -0
- package/components/Attachment/AttachmentViewer.js +215 -0
- package/components/Attachment/fileTypes.d.ts +101 -0
- package/components/Attachment/fileTypes.js +182 -0
- package/components/Attachment/typeMark.d.ts +11 -0
- package/components/Attachment/typeMark.js +51 -0
- package/components/Attachment/useAttachments.d.ts +65 -0
- package/components/Attachment/useAttachments.js +201 -0
- package/components/Chart/BarChart.js +1 -1
- package/components/Chart/ComboChart.js +1 -1
- package/components/Chart/Treemap.js +83 -6
- package/components/ChatMessage/ChatMessage.css +24 -0
- package/components/ChatMessage/ChatMessage.d.ts +6 -0
- package/components/ChatMessage/ChatMessage.js +8 -5
- package/components/ColorPicker/ColorPicker.js +1 -1
- package/components/Composer/Composer.css +140 -5
- package/components/Composer/Composer.d.ts +71 -14
- package/components/Composer/Composer.js +204 -11
- package/components/DatePicker/DatePicker.css +1 -1
- package/components/DocumentChip/DocumentChip.d.ts +7 -5
- package/components/EventCalendar/EventCalendar.css +2 -2
- package/components/FileInput/FileInput.css +1 -1
- package/components/FileInput/FileInput.js +1 -7
- package/components/Globe/Globe.d.ts +5 -3
- package/components/Globe/Globe.js +18 -2
- package/components/Inspector/Inspector.css +108 -3
- package/components/Inspector/InspectorColorPicker.d.ts +43 -0
- package/components/Inspector/InspectorColorPicker.js +51 -0
- package/components/Instructions/Instructions.css +4 -4
- package/components/MessageCard/MessageCard.d.ts +1 -1
- package/components/Prose/Prose.css +27 -0
- package/components/Sparkline/Sparkline.css +2 -2
- package/components/Spinner/Spinner.css +1 -1
- package/components/Timeline/Timeline.css +1 -1
- package/components/WorldMap/land.d.ts +1 -1
- package/components/registry.json +48 -3
- package/components/registry.json.d.ts +48 -3
- package/components/registry.json.js +1 -1
- package/index.d.ts +5 -0
- package/index.js +21 -0
- package/package.json +9 -1
- package/tokens/motion.d.ts +2 -0
- package/tokens/motion.js +2 -0
- package/tokens/presets/contrast.css +2 -4
- package/tokens/presets/coral.css +2 -4
- package/tokens/presets/forest.css +2 -4
- package/tokens/presets/gold.css +4 -4
- package/tokens/presets/mono.css +0 -2
- package/tokens/presets/pink.css +4 -4
- package/tokens/presets/terminal.css +5 -4
- package/tokens/presets/violet.css +2 -4
- package/tokens/presets/warm.css +3 -4
- package/tokens/presets/zest.css +2 -4
- package/tokens/registry.json +5 -0
- package/tokens/registry.json.d.ts +5 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +14 -5
- package/tokens/tokens-light.css +16 -2
- package/tokens/tokens-motion.css +2 -0
- package/tokens/tokens-primitives.css +10 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by AttachmentDropZone itself — everything else falls through to the root element. */
|
|
3
|
+
type AttachmentDropZoneOwnProps = {
|
|
4
|
+
/** Fires with the files dropped anywhere on the zone. */
|
|
5
|
+
onFilesSelected?: (files: File[]) => void;
|
|
6
|
+
/** Stops the zone reacting to drags; its children behave as if it were not there. */
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Icon above the overlay's headline: a Material Symbol name, or any
|
|
10
|
+
* custom element. Pass `null` for none.
|
|
11
|
+
*/
|
|
12
|
+
icon?: string | React.ReactNode;
|
|
13
|
+
/** Headline of the overlay shown while files are dragged over the zone. */
|
|
14
|
+
label?: string;
|
|
15
|
+
/** Second line of the overlay: what may be dropped, e.g. accepted types and limits. */
|
|
16
|
+
hint?: string;
|
|
17
|
+
/** Additional CSS classes */
|
|
18
|
+
className?: string;
|
|
19
|
+
/** The surface that accepts drops: a chat panel, a thread and its composer. */
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
};
|
|
22
|
+
export interface AttachmentDropZoneProps extends AttachmentDropZoneOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof AttachmentDropZoneOwnProps> {
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* AttachmentDropZone turns a whole surface into a drop target. Wrap a chat
|
|
26
|
+
* in it and a file dragged anywhere over the conversation lands, instead of
|
|
27
|
+
* the person having to aim for the composer. While files hover, an
|
|
28
|
+
* outlined overlay covers the surface and says what dropping will do.
|
|
29
|
+
*
|
|
30
|
+
* The zone hears drags in the capture phase and keeps the ones that carry
|
|
31
|
+
* files to itself, so a drop target nested inside it, or a page-level one
|
|
32
|
+
* outside it, never handles the same drop twice. A drag of anything else
|
|
33
|
+
* (selected text, a link) passes straight through.
|
|
34
|
+
*
|
|
35
|
+
* Dropping is a pointer path only. Pair it with a picker button, which
|
|
36
|
+
* Composer provides, so the same files can be added from the keyboard.
|
|
37
|
+
*/
|
|
38
|
+
export declare const AttachmentDropZone: React.ForwardRefExoticComponent<AttachmentDropZoneProps & React.RefAttributes<HTMLDivElement>>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
+
import React, { useState, useRef } from "react";
|
|
4
|
+
import "../../fonts/material-symbols.css";
|
|
5
|
+
import "./Attachment.css";
|
|
6
|
+
const carriesFiles = (event) => Array.from(event.dataTransfer?.types ?? []).includes("Files");
|
|
7
|
+
const AttachmentDropZone = React.forwardRef(
|
|
8
|
+
({
|
|
9
|
+
onFilesSelected,
|
|
10
|
+
disabled = false,
|
|
11
|
+
icon = "upload_file",
|
|
12
|
+
label = "Drop files to attach",
|
|
13
|
+
hint,
|
|
14
|
+
className = "",
|
|
15
|
+
children,
|
|
16
|
+
...rest
|
|
17
|
+
}, ref) => {
|
|
18
|
+
const baseClass = "ds-attachment-drop-zone";
|
|
19
|
+
const [active, setActive] = useState(false);
|
|
20
|
+
const depth = useRef(0);
|
|
21
|
+
const claims = (event) => !disabled && carriesFiles(event);
|
|
22
|
+
const handleDragEnter = (event) => {
|
|
23
|
+
if (!claims(event)) return;
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
event.stopPropagation();
|
|
26
|
+
depth.current += 1;
|
|
27
|
+
setActive(true);
|
|
28
|
+
};
|
|
29
|
+
const handleDragOver = (event) => {
|
|
30
|
+
if (!claims(event)) return;
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
event.stopPropagation();
|
|
33
|
+
event.dataTransfer.dropEffect = "copy";
|
|
34
|
+
};
|
|
35
|
+
const handleDragLeave = (event) => {
|
|
36
|
+
if (!claims(event)) return;
|
|
37
|
+
event.stopPropagation();
|
|
38
|
+
depth.current = Math.max(0, depth.current - 1);
|
|
39
|
+
if (depth.current === 0) setActive(false);
|
|
40
|
+
};
|
|
41
|
+
const handleDrop = (event) => {
|
|
42
|
+
if (!claims(event)) return;
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
event.stopPropagation();
|
|
45
|
+
depth.current = 0;
|
|
46
|
+
setActive(false);
|
|
47
|
+
const files = Array.from(event.dataTransfer.files ?? []);
|
|
48
|
+
if (files.length > 0) onFilesSelected?.(files);
|
|
49
|
+
};
|
|
50
|
+
const classes = [baseClass, active ? `${baseClass}--active` : "", className].filter(Boolean).join(" ");
|
|
51
|
+
return /* @__PURE__ */ jsxs(
|
|
52
|
+
"div",
|
|
53
|
+
{
|
|
54
|
+
...rest,
|
|
55
|
+
ref,
|
|
56
|
+
className: classes,
|
|
57
|
+
onDragEnterCapture: handleDragEnter,
|
|
58
|
+
onDragOverCapture: handleDragOver,
|
|
59
|
+
onDragLeaveCapture: handleDragLeave,
|
|
60
|
+
onDropCapture: handleDrop,
|
|
61
|
+
children: [
|
|
62
|
+
children,
|
|
63
|
+
/* @__PURE__ */ jsx("div", { className: `${baseClass}__overlay`, "aria-hidden": "true", children: /* @__PURE__ */ jsxs("div", { className: `${baseClass}__message`, children: [
|
|
64
|
+
icon !== null && icon !== void 0 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, children: typeof icon === "string" ? /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: icon }) : icon }),
|
|
65
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: label }),
|
|
66
|
+
hint && /* @__PURE__ */ jsx("span", { className: `${baseClass}__hint`, children: hint })
|
|
67
|
+
] }) })
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
AttachmentDropZone.displayName = "AttachmentDropZone";
|
|
74
|
+
export {
|
|
75
|
+
AttachmentDropZone
|
|
76
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { AttachmentItem } from './fileTypes';
|
|
3
|
+
export { revokeAttachmentUrls, useAttachments } from './useAttachments';
|
|
4
|
+
export type { AttachmentRejection, AttachmentRejectionReason, AttachmentUploadContext, UseAttachmentsOptions, UseAttachmentsResult, } from './useAttachments';
|
|
5
|
+
/** Props owned by AttachmentGroup itself — everything else falls through to the root element. */
|
|
6
|
+
type AttachmentGroupOwnProps = {
|
|
7
|
+
/** The files on the message, in order. */
|
|
8
|
+
items: AttachmentItem[];
|
|
9
|
+
/** Which edge the layout hugs: `end` for the sender's own turns. */
|
|
10
|
+
align?: 'start' | 'end';
|
|
11
|
+
/**
|
|
12
|
+
* Most cells shown before the rest collapse. Over the limit, the last
|
|
13
|
+
* cell becomes a "+N" button standing for the hidden files.
|
|
14
|
+
*/
|
|
15
|
+
max?: number;
|
|
16
|
+
/** Whether the overflow is expanded, for controlled use. Pair with `onOpenChange`. */
|
|
17
|
+
open?: boolean;
|
|
18
|
+
/** Whether the overflow starts expanded, for uncontrolled use. */
|
|
19
|
+
defaultOpen?: boolean;
|
|
20
|
+
/** Fires when the overflow expands or collapses. */
|
|
21
|
+
onOpenChange?: (open: boolean) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Open a pressed file in the built-in AttachmentViewer, which steps
|
|
24
|
+
* through the rest of the group. Turn it off to handle presses yourself
|
|
25
|
+
* through `onItemClick`.
|
|
26
|
+
*/
|
|
27
|
+
viewer?: boolean;
|
|
28
|
+
/** Fires when a file is pressed, whether or not the viewer opens. */
|
|
29
|
+
onItemClick?: (item: AttachmentItem) => void;
|
|
30
|
+
/** Passed to the viewer: draws a file it cannot, such as the pages of a PDF. */
|
|
31
|
+
renderPreview?: (item: AttachmentItem) => React.ReactNode;
|
|
32
|
+
/** Passed to the viewer: fires when Download is pressed, and renders the buttons. */
|
|
33
|
+
onDownload?: (item: AttachmentItem) => void;
|
|
34
|
+
/** Passed to the viewer: fires when "Try again" is pressed on a file that failed to load. */
|
|
35
|
+
onRetry?: (item: AttachmentItem) => void;
|
|
36
|
+
/** Tile size, passed to every tile. */
|
|
37
|
+
size?: 'default' | 'compact';
|
|
38
|
+
/** Accessible label for the list. */
|
|
39
|
+
label?: string;
|
|
40
|
+
/** Builds the accessible label of the "+N" button from the hidden count. */
|
|
41
|
+
showMoreLabel?: (hidden: number) => string;
|
|
42
|
+
/** Text of the collapse button under an expanded group. */
|
|
43
|
+
showLessLabel?: string;
|
|
44
|
+
/** Additional CSS classes */
|
|
45
|
+
className?: string;
|
|
46
|
+
};
|
|
47
|
+
export interface AttachmentGroupProps extends AttachmentGroupOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof AttachmentGroupOwnProps> {
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* AttachmentGroup lays out the files on a sent message. One picture keeps
|
|
51
|
+
* its own aspect ratio; anything else becomes a grid of square
|
|
52
|
+
* AttachmentTiles. Past `max`, the tail collapses behind a "+N" cell laid
|
|
53
|
+
* over the next file, which expands the grid in place; a "Show fewer"
|
|
54
|
+
* button folds it again.
|
|
55
|
+
*
|
|
56
|
+
* Pressing a file opens it in an AttachmentViewer that steps through the
|
|
57
|
+
* whole group. A file that is no longer available stays on the message as
|
|
58
|
+
* a record and opens nothing.
|
|
59
|
+
*/
|
|
60
|
+
export declare const AttachmentGroup: React.ForwardRefExoticComponent<AttachmentGroupProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import React, { useId, useState, useRef, useEffect } from "react";
|
|
4
|
+
import { AttachmentTile } from "./AttachmentTile.js";
|
|
5
|
+
import { formatBytes } from "./fileTypes.js";
|
|
6
|
+
import { renderTypeMark } from "./typeMark.js";
|
|
7
|
+
import { AttachmentViewer } from "./AttachmentViewer.js";
|
|
8
|
+
import "./Attachment.css";
|
|
9
|
+
const isPicture = (item) => item.status === "ready" && item.kind === "image" && Boolean(item.previewSrc);
|
|
10
|
+
const AttachmentGroup = React.forwardRef(
|
|
11
|
+
({
|
|
12
|
+
items,
|
|
13
|
+
align = "start",
|
|
14
|
+
max = 4,
|
|
15
|
+
open,
|
|
16
|
+
defaultOpen = false,
|
|
17
|
+
onOpenChange,
|
|
18
|
+
viewer = true,
|
|
19
|
+
onItemClick,
|
|
20
|
+
renderPreview,
|
|
21
|
+
onDownload,
|
|
22
|
+
onRetry,
|
|
23
|
+
size = "default",
|
|
24
|
+
label = "Attachments",
|
|
25
|
+
showMoreLabel = (hidden) => `Show ${hidden} more`,
|
|
26
|
+
showLessLabel = "Show fewer",
|
|
27
|
+
className = "",
|
|
28
|
+
...rest
|
|
29
|
+
}, ref) => {
|
|
30
|
+
const baseClass = "ds-attachment-group";
|
|
31
|
+
const gridId = useId();
|
|
32
|
+
const isControlled = open !== void 0;
|
|
33
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
34
|
+
const isOpen = isControlled ? open : uncontrolledOpen;
|
|
35
|
+
const [viewing, setViewing] = useState(null);
|
|
36
|
+
const moreRef = useRef(null);
|
|
37
|
+
const lessRef = useRef(null);
|
|
38
|
+
const moveFocus = useRef(false);
|
|
39
|
+
const setOpen = (next) => {
|
|
40
|
+
moveFocus.current = true;
|
|
41
|
+
if (!isControlled) setUncontrolledOpen(next);
|
|
42
|
+
onOpenChange?.(next);
|
|
43
|
+
};
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!moveFocus.current) return;
|
|
46
|
+
moveFocus.current = false;
|
|
47
|
+
(isOpen ? lessRef.current : moreRef.current)?.focus();
|
|
48
|
+
}, [isOpen]);
|
|
49
|
+
const overflowing = items.length > Math.max(1, max);
|
|
50
|
+
const visible = overflowing && !isOpen ? items.slice(0, Math.max(1, max) - 1) : items;
|
|
51
|
+
const hidden = items.length - visible.length;
|
|
52
|
+
const viewed = items.find((item) => item.id === viewing);
|
|
53
|
+
const clickHandler = (item) => {
|
|
54
|
+
const opens = viewer && item.status !== "unavailable";
|
|
55
|
+
if (!opens && !onItemClick) return void 0;
|
|
56
|
+
return () => {
|
|
57
|
+
onItemClick?.(item);
|
|
58
|
+
if (opens) setViewing(item.id);
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const classes = [baseClass, `${baseClass}--${align}`, `${baseClass}--${size}`, className].filter(Boolean).join(" ");
|
|
62
|
+
const single = items.length === 1 && isPicture(items[0]) ? items[0] : void 0;
|
|
63
|
+
let layout;
|
|
64
|
+
if (single) {
|
|
65
|
+
const onClick = clickHandler(single);
|
|
66
|
+
const image = /* @__PURE__ */ jsx(
|
|
67
|
+
"img",
|
|
68
|
+
{
|
|
69
|
+
className: `${baseClass}__single-image`,
|
|
70
|
+
src: single.previewSrc,
|
|
71
|
+
alt: single.previewAlt ?? single.name,
|
|
72
|
+
width: single.width,
|
|
73
|
+
height: single.height
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
const badge = /* @__PURE__ */ jsx("span", { className: `${baseClass}__single-mark`, "aria-hidden": "true", children: renderTypeMark(single.name, single.kind, { form: "badge" }) });
|
|
77
|
+
layout = onClick ? /* @__PURE__ */ jsxs("button", { type: "button", className: `${baseClass}__single`, onClick, children: [
|
|
78
|
+
image,
|
|
79
|
+
badge
|
|
80
|
+
] }) : /* @__PURE__ */ jsxs("span", { className: `${baseClass}__single`, children: [
|
|
81
|
+
image,
|
|
82
|
+
badge
|
|
83
|
+
] });
|
|
84
|
+
} else {
|
|
85
|
+
layout = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
86
|
+
/* @__PURE__ */ jsxs(
|
|
87
|
+
"ul",
|
|
88
|
+
{
|
|
89
|
+
className: `${baseClass}__grid`,
|
|
90
|
+
id: gridId,
|
|
91
|
+
"aria-label": label,
|
|
92
|
+
"data-cells": visible.length + (hidden > 0 ? 1 : 0),
|
|
93
|
+
children: [
|
|
94
|
+
visible.map((item) => /* @__PURE__ */ jsx("li", { className: `${baseClass}__cell`, children: /* @__PURE__ */ jsx(
|
|
95
|
+
AttachmentTile,
|
|
96
|
+
{
|
|
97
|
+
name: item.name,
|
|
98
|
+
kind: item.kind,
|
|
99
|
+
status: item.status,
|
|
100
|
+
progress: item.progress,
|
|
101
|
+
error: item.error,
|
|
102
|
+
previewSrc: item.previewSrc,
|
|
103
|
+
previewAlt: item.previewAlt,
|
|
104
|
+
excerpt: item.excerpt,
|
|
105
|
+
previewMark: true,
|
|
106
|
+
meta: item.size === void 0 ? void 0 : formatBytes(item.size),
|
|
107
|
+
size,
|
|
108
|
+
onClick: clickHandler(item)
|
|
109
|
+
}
|
|
110
|
+
) }, item.id)),
|
|
111
|
+
hidden > 0 && /* @__PURE__ */ jsxs("li", { className: `${baseClass}__cell ${baseClass}__cell--more`, children: [
|
|
112
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__peek`, "aria-hidden": "true", children: /* @__PURE__ */ jsx(
|
|
113
|
+
AttachmentTile,
|
|
114
|
+
{
|
|
115
|
+
name: items[visible.length].name,
|
|
116
|
+
kind: items[visible.length].kind,
|
|
117
|
+
previewSrc: items[visible.length].previewSrc,
|
|
118
|
+
previewAlt: "",
|
|
119
|
+
size
|
|
120
|
+
}
|
|
121
|
+
) }),
|
|
122
|
+
/* @__PURE__ */ jsxs(
|
|
123
|
+
"button",
|
|
124
|
+
{
|
|
125
|
+
type: "button",
|
|
126
|
+
ref: moreRef,
|
|
127
|
+
className: `${baseClass}__more`,
|
|
128
|
+
"aria-expanded": false,
|
|
129
|
+
"aria-controls": gridId,
|
|
130
|
+
"aria-label": showMoreLabel(hidden),
|
|
131
|
+
onClick: () => setOpen(true),
|
|
132
|
+
children: [
|
|
133
|
+
"+",
|
|
134
|
+
hidden
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
] })
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
),
|
|
142
|
+
overflowing && isOpen && /* @__PURE__ */ jsx(
|
|
143
|
+
"button",
|
|
144
|
+
{
|
|
145
|
+
type: "button",
|
|
146
|
+
ref: lessRef,
|
|
147
|
+
className: `${baseClass}__less`,
|
|
148
|
+
"aria-expanded": true,
|
|
149
|
+
"aria-controls": gridId,
|
|
150
|
+
onClick: () => setOpen(false),
|
|
151
|
+
children: showLessLabel
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
] });
|
|
155
|
+
}
|
|
156
|
+
return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
|
|
157
|
+
layout,
|
|
158
|
+
viewer && /* @__PURE__ */ jsx(
|
|
159
|
+
AttachmentViewer,
|
|
160
|
+
{
|
|
161
|
+
open: viewed !== void 0,
|
|
162
|
+
onOpenChange: (next) => {
|
|
163
|
+
if (!next) setViewing(null);
|
|
164
|
+
},
|
|
165
|
+
items,
|
|
166
|
+
activeId: viewed?.id ?? items[0]?.id,
|
|
167
|
+
onActiveChange: setViewing,
|
|
168
|
+
renderPreview,
|
|
169
|
+
onDownload,
|
|
170
|
+
onRetry
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
] });
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
AttachmentGroup.displayName = "AttachmentGroup";
|
|
177
|
+
export {
|
|
178
|
+
AttachmentGroup
|
|
179
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { AttachmentStatus, FileKind } from './fileTypes';
|
|
3
|
+
export { FILE_KINDS, formatBytes, getFileExtension, getFileKind, getFileTypeLabel, matchesAccept, splitFileName, } from './fileTypes';
|
|
4
|
+
export type { AttachmentItem, AttachmentStatus, FileKind, FileKindMeta, FileLike, } from './fileTypes';
|
|
5
|
+
/** Props owned by AttachmentTile itself — everything else falls through to the root element. */
|
|
6
|
+
type AttachmentTileOwnProps = {
|
|
7
|
+
/** File name, extension included. Truncates in the middle so the extension stays visible. */
|
|
8
|
+
name: string;
|
|
9
|
+
/**
|
|
10
|
+
* What kind of file it is, which picks the type mark: a coloured lettered
|
|
11
|
+
* glyph for the well-known formats, a neutral extension badge for the
|
|
12
|
+
* rest. Derive it with `getFileKind`.
|
|
13
|
+
*/
|
|
14
|
+
kind?: FileKind;
|
|
15
|
+
/** Short uppercase label on the type mark. Defaults from the kind and the extension. */
|
|
16
|
+
typeLabel?: string;
|
|
17
|
+
/** Where the file is in its life. Anything but `ready` shows the name with a status line. */
|
|
18
|
+
status?: AttachmentStatus;
|
|
19
|
+
/** Upload progress, 0 to 100, reported to assistive technology while `uploading`. */
|
|
20
|
+
progress?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Image that fills the tile while `ready`: an object URL for a picture, or
|
|
23
|
+
* a first page the host rendered for a document. A non-image kind keeps
|
|
24
|
+
* its type mark over the corner.
|
|
25
|
+
*/
|
|
26
|
+
previewSrc?: string;
|
|
27
|
+
/** Alt text for the preview. Defaults to the file name. */
|
|
28
|
+
previewAlt?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Whether the type mark sits over the corner of a preview. Defaults to on
|
|
31
|
+
* for a document's first page, which still has to say what format it is,
|
|
32
|
+
* and off for a picture, which is its own description. A sent message
|
|
33
|
+
* turns it on for pictures too.
|
|
34
|
+
*/
|
|
35
|
+
previewMark?: boolean;
|
|
36
|
+
/** First lines of a pasted block, drawn as a faded excerpt filling the tile while `ready`. */
|
|
37
|
+
excerpt?: string;
|
|
38
|
+
/** Secondary line under the name while `ready`, e.g. "1.2 MB". Callers keep their own formatting. */
|
|
39
|
+
meta?: string;
|
|
40
|
+
/** Why the file failed. Replaces `errorLabel` on the status line. */
|
|
41
|
+
error?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Tile size. Compact shrinks the square and drops the secondary line of a
|
|
44
|
+
* ready file. Unrelated to any native `size` attribute: the root is a div.
|
|
45
|
+
*/
|
|
46
|
+
size?: 'default' | 'compact';
|
|
47
|
+
/** Click handler. Its presence makes the tile body a button. */
|
|
48
|
+
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
|
49
|
+
/** Remove handler. Its presence renders the corner remove button. */
|
|
50
|
+
onRemove?: () => void;
|
|
51
|
+
/** Status line while `uploading`. */
|
|
52
|
+
uploadingLabel?: string;
|
|
53
|
+
/** Status line while `error`, when no `error` message is given. */
|
|
54
|
+
errorLabel?: string;
|
|
55
|
+
/** Status line while `unavailable`. */
|
|
56
|
+
unavailableLabel?: string;
|
|
57
|
+
/** Accessible label for the remove button. Defaults to "Remove" and the file name. */
|
|
58
|
+
removeLabel?: string;
|
|
59
|
+
/** Additional CSS classes */
|
|
60
|
+
className?: string;
|
|
61
|
+
};
|
|
62
|
+
export interface AttachmentTileProps extends AttachmentTileOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof AttachmentTileOwnProps> {
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* AttachmentTile is one file as a square: a picture fills it, a well-known
|
|
66
|
+
* document format (PDF, spreadsheet, document, presentation) wears a
|
|
67
|
+
* coloured lettered mark, and every other file wears its extension as a
|
|
68
|
+
* neutral badge. Under the mark sit the name, truncated in the middle so
|
|
69
|
+
* the extension survives, and one secondary line.
|
|
70
|
+
*
|
|
71
|
+
* The same tile serves a composer's queue and a sent message. `status`
|
|
72
|
+
* swaps the mark for a spinner, an alert or an unavailable glyph and the
|
|
73
|
+
* secondary line for the matching words, so a file reads the same wherever
|
|
74
|
+
* it appears and whatever happened to it.
|
|
75
|
+
*
|
|
76
|
+
* The root is always a `<div>`; `onClick` turns the body into a `<button>`
|
|
77
|
+
* so click and remove coexist without nesting controls. No hooks, so it
|
|
78
|
+
* renders from a Server Component.
|
|
79
|
+
*/
|
|
80
|
+
export declare const AttachmentTile: React.ForwardRefExoticComponent<AttachmentTileProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { FILE_KINDS, splitFileName } from "./fileTypes.js";
|
|
4
|
+
import { formatBytes, getFileExtension, getFileKind, getFileTypeLabel, matchesAccept } from "./fileTypes.js";
|
|
5
|
+
import { renderTypeMark } from "./typeMark.js";
|
|
6
|
+
import "../../fonts/material-symbols.css";
|
|
7
|
+
import "./Attachment.css";
|
|
8
|
+
const AttachmentTile = React.forwardRef(
|
|
9
|
+
({
|
|
10
|
+
name,
|
|
11
|
+
kind = "generic",
|
|
12
|
+
typeLabel,
|
|
13
|
+
status = "ready",
|
|
14
|
+
progress,
|
|
15
|
+
previewSrc,
|
|
16
|
+
previewAlt,
|
|
17
|
+
previewMark,
|
|
18
|
+
excerpt,
|
|
19
|
+
meta,
|
|
20
|
+
error,
|
|
21
|
+
size = "default",
|
|
22
|
+
onClick,
|
|
23
|
+
onRemove,
|
|
24
|
+
uploadingLabel = "Uploading…",
|
|
25
|
+
errorLabel = "Upload failed",
|
|
26
|
+
unavailableLabel = "Unavailable",
|
|
27
|
+
removeLabel,
|
|
28
|
+
className = "",
|
|
29
|
+
...rest
|
|
30
|
+
}, ref) => {
|
|
31
|
+
const baseClass = "ds-attachment-tile";
|
|
32
|
+
const ready = status === "ready";
|
|
33
|
+
const colored = FILE_KINDS[kind].colored;
|
|
34
|
+
const showPreview = ready && Boolean(previewSrc);
|
|
35
|
+
const showExcerpt = ready && !showPreview && Boolean(excerpt);
|
|
36
|
+
const classes = [
|
|
37
|
+
baseClass,
|
|
38
|
+
`${baseClass}--${size}`,
|
|
39
|
+
`${baseClass}--${status}`,
|
|
40
|
+
colored ? `${baseClass}--${kind}` : `${baseClass}--neutral`,
|
|
41
|
+
showPreview ? `${baseClass}--preview` : "",
|
|
42
|
+
showExcerpt ? `${baseClass}--excerpt` : "",
|
|
43
|
+
onClick ? `${baseClass}--clickable` : "",
|
|
44
|
+
className
|
|
45
|
+
].filter(Boolean).join(" ");
|
|
46
|
+
const typeMark = renderTypeMark(name, kind, { typeLabel });
|
|
47
|
+
const cornerMark = renderTypeMark(name, kind, { typeLabel, form: "badge" });
|
|
48
|
+
let mark = typeMark;
|
|
49
|
+
let secondary = meta;
|
|
50
|
+
if (status === "uploading") {
|
|
51
|
+
const value = progress === void 0 ? void 0 : Math.max(0, Math.min(100, Math.round(progress)));
|
|
52
|
+
mark = /* @__PURE__ */ jsx(
|
|
53
|
+
"span",
|
|
54
|
+
{
|
|
55
|
+
className: `${baseClass}__spinner`,
|
|
56
|
+
role: "progressbar",
|
|
57
|
+
"aria-label": `Uploading ${name}`,
|
|
58
|
+
"aria-valuemin": 0,
|
|
59
|
+
"aria-valuemax": 100,
|
|
60
|
+
"aria-valuenow": value,
|
|
61
|
+
children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", focusable: "false", children: [
|
|
62
|
+
/* @__PURE__ */ jsx(
|
|
63
|
+
"circle",
|
|
64
|
+
{
|
|
65
|
+
className: `${baseClass}__spinner-track`,
|
|
66
|
+
cx: "12",
|
|
67
|
+
cy: "12",
|
|
68
|
+
r: "9",
|
|
69
|
+
strokeWidth: "1.75"
|
|
70
|
+
}
|
|
71
|
+
),
|
|
72
|
+
/* @__PURE__ */ jsx(
|
|
73
|
+
"circle",
|
|
74
|
+
{
|
|
75
|
+
className: `${baseClass}__spinner-arc`,
|
|
76
|
+
cx: "12",
|
|
77
|
+
cy: "12",
|
|
78
|
+
r: "9",
|
|
79
|
+
strokeWidth: "1.75",
|
|
80
|
+
strokeLinecap: "round"
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
] })
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
secondary = uploadingLabel;
|
|
87
|
+
} else if (status === "error") {
|
|
88
|
+
mark = /* @__PURE__ */ jsx("span", { className: `${baseClass}__state material-symbols-rounded`, "aria-hidden": "true", children: "error" });
|
|
89
|
+
secondary = error ?? errorLabel;
|
|
90
|
+
} else if (status === "unavailable") {
|
|
91
|
+
mark = /* @__PURE__ */ jsx("span", { className: `${baseClass}__state material-symbols-rounded`, "aria-hidden": "true", children: "block" });
|
|
92
|
+
secondary = unavailableLabel;
|
|
93
|
+
}
|
|
94
|
+
const showSecondary = Boolean(secondary) && !(size === "compact" && ready);
|
|
95
|
+
const { head, tail } = splitFileName(name);
|
|
96
|
+
const stack = /* @__PURE__ */ jsxs("span", { className: `${baseClass}__stack`, children: [
|
|
97
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__mark`, children: mark }),
|
|
98
|
+
/* @__PURE__ */ jsxs("span", { className: `${baseClass}__name`, title: name, children: [
|
|
99
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__sr-only`, children: name }),
|
|
100
|
+
head !== "" && /* @__PURE__ */ jsx("span", { className: `${baseClass}__name-head`, "aria-hidden": "true", children: head }),
|
|
101
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__name-tail`, "aria-hidden": "true", children: tail })
|
|
102
|
+
] }),
|
|
103
|
+
showSecondary && /* @__PURE__ */ jsx("span", { className: `${baseClass}__meta`, children: secondary })
|
|
104
|
+
] });
|
|
105
|
+
let content = stack;
|
|
106
|
+
if (showPreview) {
|
|
107
|
+
content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
108
|
+
/* @__PURE__ */ jsx("img", { className: `${baseClass}__preview`, src: previewSrc, alt: previewAlt ?? name }),
|
|
109
|
+
(previewMark ?? kind !== "image") && /* @__PURE__ */ jsx("span", { className: `${baseClass}__corner`, children: cornerMark })
|
|
110
|
+
] });
|
|
111
|
+
} else if (showExcerpt) {
|
|
112
|
+
content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
113
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__sr-only`, children: name }),
|
|
114
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__excerpt`, children: excerpt }),
|
|
115
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__corner`, children: cornerMark })
|
|
116
|
+
] });
|
|
117
|
+
}
|
|
118
|
+
return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
|
|
119
|
+
onClick ? /* @__PURE__ */ jsx("button", { type: "button", className: `${baseClass}__body`, onClick, children: content }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__body`, children: content }),
|
|
120
|
+
onRemove && /* @__PURE__ */ jsx(
|
|
121
|
+
"button",
|
|
122
|
+
{
|
|
123
|
+
type: "button",
|
|
124
|
+
className: `${baseClass}__remove`,
|
|
125
|
+
onClick: onRemove,
|
|
126
|
+
"aria-label": removeLabel ?? `Remove ${name}`,
|
|
127
|
+
children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", "aria-hidden": "true", children: "close" })
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
] });
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
AttachmentTile.displayName = "AttachmentTile";
|
|
134
|
+
export {
|
|
135
|
+
AttachmentTile,
|
|
136
|
+
FILE_KINDS,
|
|
137
|
+
formatBytes,
|
|
138
|
+
getFileExtension,
|
|
139
|
+
getFileKind,
|
|
140
|
+
getFileTypeLabel,
|
|
141
|
+
matchesAccept,
|
|
142
|
+
splitFileName
|
|
143
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { AttachmentItem } from './fileTypes';
|
|
3
|
+
/** Props owned by AttachmentViewer itself — everything else falls through to the panel. */
|
|
4
|
+
type AttachmentViewerOwnProps = {
|
|
5
|
+
/** Whether the viewer is open. */
|
|
6
|
+
open: boolean;
|
|
7
|
+
/** Fires when the viewer asks to close: the close button, Escape, or the backdrop. */
|
|
8
|
+
onOpenChange: (open: boolean) => void;
|
|
9
|
+
/** The files the viewer steps through, in order. */
|
|
10
|
+
items: AttachmentItem[];
|
|
11
|
+
/** Id of the file on show, for controlled use. Pair with `onActiveChange`. */
|
|
12
|
+
activeId?: string;
|
|
13
|
+
/** Id of the file shown first, for uncontrolled use. Defaults to the first item. */
|
|
14
|
+
defaultActiveId?: string;
|
|
15
|
+
/** Fires with the id of the file stepped to. */
|
|
16
|
+
onActiveChange?: (id: string) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Draws a file the viewer cannot: the pages of a PDF, a media player, a
|
|
19
|
+
* rendered spreadsheet. Return nothing to fall back to the built-in view.
|
|
20
|
+
*/
|
|
21
|
+
renderPreview?: (item: AttachmentItem) => React.ReactNode;
|
|
22
|
+
/** Fires with the file on show when Download is pressed. Its presence renders the buttons. */
|
|
23
|
+
onDownload?: (item: AttachmentItem) => void;
|
|
24
|
+
/** Fires with a file that failed to load when "Try again" is pressed. Its presence renders the button. */
|
|
25
|
+
onRetry?: (item: AttachmentItem) => void;
|
|
26
|
+
/** Whether Escape, the backdrop and the close button dismiss the viewer. */
|
|
27
|
+
dismissible?: boolean;
|
|
28
|
+
/** Text of the Download buttons. */
|
|
29
|
+
downloadLabel?: string;
|
|
30
|
+
/** Accessible label for the close button. */
|
|
31
|
+
closeLabel?: string;
|
|
32
|
+
/** Accessible label for the previous-file button. */
|
|
33
|
+
previousLabel?: string;
|
|
34
|
+
/** Accessible label for the next-file button. */
|
|
35
|
+
nextLabel?: string;
|
|
36
|
+
/** Text of the retry button. */
|
|
37
|
+
retryLabel?: string;
|
|
38
|
+
/** Headline when a file has nothing the viewer can draw. */
|
|
39
|
+
noPreviewLabel?: string;
|
|
40
|
+
/** Headline when a file failed to load or is no longer available. */
|
|
41
|
+
failedLabel?: string;
|
|
42
|
+
/** Sentence under `failedLabel`, when the file carries no error message of its own. */
|
|
43
|
+
failedDescription?: string;
|
|
44
|
+
/** Builds the position text, e.g. "2 of 5", from a one-based index and the total. */
|
|
45
|
+
formatPosition?: (position: number, total: number) => string;
|
|
46
|
+
/** Additional CSS classes — applied to the portal container, not the panel. */
|
|
47
|
+
className?: string;
|
|
48
|
+
};
|
|
49
|
+
export interface AttachmentViewerProps extends AttachmentViewerOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof AttachmentViewerOwnProps> {
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* AttachmentViewer opens a file from a conversation: a centred dialog on a
|
|
53
|
+
* wide screen, a full-screen sheet on a narrow one. The header names the
|
|
54
|
+
* file with its type mark, size and position; the stage shows a picture, a
|
|
55
|
+
* host-drawn preview, or a plain statement that there is nothing to show;
|
|
56
|
+
* the footer steps through the rest of the message's files.
|
|
57
|
+
*
|
|
58
|
+
* The viewer draws pictures itself. Everything else is the host's to
|
|
59
|
+
* render through `renderPreview`, so the library carries no document
|
|
60
|
+
* renderer. A file that failed or has expired says so and offers
|
|
61
|
+
* "Try again" when the host can.
|
|
62
|
+
*
|
|
63
|
+
* Forwards a ref to the panel and spreads unrecognised props onto it.
|
|
64
|
+
*/
|
|
65
|
+
export declare const AttachmentViewer: React.ForwardRefExoticComponent<AttachmentViewerProps & React.RefAttributes<HTMLDivElement>>;
|
|
66
|
+
export {};
|