Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/web/DESIGN.md

Large diffs are not rendered by default.

37 changes: 33 additions & 4 deletions apps/web/src/chat/attachments/message-files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
* for one, `ImageGallery` for several, and the message's shared viewer), read at Slack's thumbnail
* widths, so an image of any size shows. Other files are assistant-ui's copied `MessageAttachments`
* tiles drawn as Slack's file cards (`media.css`): the type's coloured icon, the name in bold, the
* type and size under it, and a toolbar of their own on hover; pressing one downloads it under its
* own name.
* type and size under it, and a toolbar of their own on hover; pressing one opens it in the
* message's viewer, after the message's images, where Previous and Next step through all of them.
*/

import { useState } from "react";
import { useId, useLayoutEffect, useRef, useState } from "react";
import { CheckIcon, DownloadIcon, LinkIcon } from "@chopin/icons";

import { attachmentPath, formatBytes } from "@chopin/protocol/attachment";
import { MessageAttachments } from "@/components/assistant-ui/elements/message-attachment";

import { ChatImageDocument, useImageDocument } from "../image";
import { ChatImageDocument, useImageDocument, useImageHost } from "../image";
import { FileKindIcon } from "../media/file-icon";
import { fileType } from "../media/layout";
import { TurnImages } from "../turn-images";
Expand Down Expand Up @@ -86,6 +86,33 @@ export default function MessageFiles(
}

let documentId = useImageDocument();
let host = useImageHost();
let entries = useId();
let anchor = useRef<HTMLDivElement>(null);
let registered = documents.map(file => file.id).join("\n");
useLayoutEffect(() => {
let element = anchor.current;
if (!host || !element) return;
let cleanups = documents.map((file, order) => {
let type = fileType(file.name);
return host.register(`${entries}:${file.id}`, {
element,
order,
file: {
kind: "file",
name: file.name,
href: attachmentPath(channelId, file.id),
text: file.mimeType.startsWith("text/"),
type: type.label,
size: formatBytes(file.size),
icon: <FileKindIcon kind={type.kind} />,
},
});
});
return () => {
for (let cleanup of cleanups) cleanup();
};
}, [host, channelId, registered]);
let content = (
<>
{images.length > 0 && (
Expand Down Expand Up @@ -117,9 +144,11 @@ export default function MessageFiles(
})}
data-chat-files=""
onOpen={id => {
if (host) return host.open(`${entries}:${id}`, document.activeElement as HTMLElement);
let file = documents.find(candidate => candidate.id === id);
if (file) downloadFile(file, attachmentPath(channelId, id));
}}
ref={anchor}
role="group"
/>
)}
Expand Down
197 changes: 2 additions & 195 deletions apps/web/src/chat/image.css
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,7 @@
}
}

.chat-image-actions-slot [data-slot="image-actions"],
[data-slot="dialog-content"]:has(> [data-chat-viewer-header]) [data-slot="image-actions"] {
.chat-image-actions-slot [data-slot="image-actions"] {
gap: 0;
padding: 2px;
}
Expand All @@ -186,10 +185,7 @@
outline: var(--edge-width) solid var(--color-edge);
}

.chat-image-actions-slot [data-slot="image-actions"] > :is(button, a),
[data-slot="dialog-content"]:has(> [data-chat-viewer-header])
[data-slot="image-actions"]
> :is(button, a) {
.chat-image-actions-slot [data-slot="image-actions"] > :is(button, a) {
inline-size: calc(var(--spacing) * 8);
block-size: calc(var(--spacing) * 8);
border-radius: var(--radius-sm);
Expand Down Expand Up @@ -286,192 +282,3 @@
.chat-markdown > :is(.chat-image, [data-chat-image-gallery]):first-child {
margin-block-start: 0;
}

/*
* The viewer: Slack's full-screen image viewer on Chopin's ink ground, with who sent the image and
* when along its top, the image in the middle, the arrows at the window's edges and the keys.
*/
[data-slot="dialog-content"]:has(> [data-chat-viewer-header]) {
--viewer-ink: var(--color-page);
--viewer-muted: color-mix(in oklch, var(--color-page) 70%, transparent);
--viewer-control: color-mix(in oklch, var(--color-page) 12%, transparent);
--viewer-control-hover: color-mix(in oklch, var(--color-page) 20%, transparent);
/* Slack's viewer fades in; it does not grow from the middle. */
--tw-enter-scale: 1;
inset: 0;
translate: none;
grid-template-rows: auto minmax(0, 1fr) auto;
gap: 0;
inline-size: 100%;
max-inline-size: none;
block-size: 100dvh;
padding: 0;
padding-block-end: env(safe-area-inset-bottom, 0px);
border-radius: 0;
background: var(--color-text-primary);
box-shadow: none;
color: var(--viewer-ink);
}

.chat-viewer-header {
display: flex;
align-items: center;
gap: calc(var(--spacing) * 3);
min-inline-size: 0;
min-block-size: calc(var(--spacing) * 16);
padding-block: calc(var(--spacing) * 3);
padding-inline: calc(env(safe-area-inset-left, 0px) + var(--spacing) * 5)
calc(env(safe-area-inset-right, 0px) + var(--spacing) * 44);
padding-block-start: calc(env(safe-area-inset-top, 0px) + var(--spacing) * 3);
}

.chat-viewer-face {
display: flex;
flex: none;
}

.chat-viewer-who {
display: flex;
flex-direction: column;
min-inline-size: 0;
font-size: var(--text-sm);
line-height: var(--text-sm--line-height);
}

.chat-viewer-name {
overflow: hidden;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}

.chat-viewer-detail {
display: flex;
flex-wrap: wrap;
column-gap: calc(var(--spacing) * 2);
min-inline-size: 0;
color: var(--viewer-muted);
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
}

.chat-viewer-note {
white-space: nowrap;
}

.chat-viewer-file {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

/* The picture, between the header and the footer, with room either side for the arrows. */
[data-slot="dialog-content"]:has(> [data-chat-viewer-header]) > [data-slot="dialog-title"] + div {
min-block-size: 0;
padding-inline: calc(var(--spacing) * 18);
}

[data-slot="dialog-content"]:has(> [data-chat-viewer-header])
> [data-slot="dialog-title"]
+ div img {
max-block-size: 100%;
}

[data-slot="dialog-content"]:has(> [data-chat-viewer-header])
> [data-slot="dialog-title"]
+ div > div {
block-size: 100%;
color: var(--viewer-muted);
}

/* The footer: the caption and the place in the message, with the arrows lifted to the edges. */
[data-slot="dialog-content"]:has(> [data-chat-viewer-header])
> [data-slot="dialog-title"]
+ div + div {
justify-content: center;
padding: calc(var(--spacing) * 3) calc(var(--spacing) * 5) calc(var(--spacing) * 4);
color: var(--viewer-muted);
font-size: var(--text-xs);
}

[data-slot="dialog-content"]:has(> [data-chat-viewer-header])
> [data-slot="dialog-title"]
+ div + div > div {
flex: 0 1 auto;
}

[data-slot="dialog-content"]:has(> [data-chat-viewer-header]) :is(p, span, a) {
color: inherit;
}

[data-slot="dialog-content"]:has(> [data-chat-viewer-header])
> [data-slot="dialog-title"]
+ div + div > button:is([aria-label="Previous image"], [aria-label="Next image"]) {
position: absolute;
inset-block-start: 50%;
inline-size: calc(var(--spacing) * 10);
block-size: calc(var(--spacing) * 10);
translate: 0 -50%;
border-radius: var(--radius-full);
background: var(--viewer-control);
color: var(--viewer-ink);
}

/* As Slack's, an arrow with nowhere to go is not drawn; it stays named, and disabled, for a reader. */
[data-slot="dialog-content"]:has(> [data-chat-viewer-header])
> [data-slot="dialog-title"]
+ div + div > button:is([aria-label="Previous image"], [aria-label="Next image"]):disabled {
opacity: 0;
}

[data-slot="dialog-content"]:has(> [data-chat-viewer-header])
> [data-slot="dialog-title"]
+ div + div > button[aria-label="Previous image"] {
inset-inline-start: calc(env(safe-area-inset-left, 0px) + var(--spacing) * 4);
}

[data-slot="dialog-content"]:has(> [data-chat-viewer-header])
> [data-slot="dialog-title"]
+ div + div > button[aria-label="Next image"] {
inset-inline-end: calc(env(safe-area-inset-right, 0px) + var(--spacing) * 4);
}

[data-slot="dialog-content"]:has(> [data-chat-viewer-header]) button:hover:not(:disabled),
[data-slot="dialog-content"]:has(> [data-chat-viewer-header]) a:hover {
background: var(--viewer-control-hover);
}

/* Download and copy sit in the top bar beside close, where Slack's viewer keeps them. */
[data-slot="dialog-content"]:has(> [data-chat-viewer-header]) [data-slot="image-actions"] {
position: absolute;
inset-block-start: calc(env(safe-area-inset-top, 0px) + var(--spacing) * 4);
inset-inline-end: calc(env(safe-area-inset-right, 0px) + var(--spacing) * 13);
}

[data-slot="dialog-content"]:has(> [data-chat-viewer-header])
[data-slot="image-actions"]
> :is(button, a) {
color: var(--viewer-ink);
}

[data-slot="dialog-content"]:has(> [data-chat-viewer-header]) > [data-slot="dialog-close"] {
inset-block-start: calc(env(safe-area-inset-top, 0px) + var(--spacing) * 4);
inset-inline-end: calc(env(safe-area-inset-right, 0px) + var(--spacing) * 4);
inline-size: calc(var(--spacing) * 8);
block-size: calc(var(--spacing) * 8);
color: var(--viewer-ink);
}

@media (max-width: 600px) {
[data-slot="dialog-content"]:has(> [data-chat-viewer-header]) > [data-slot="dialog-title"] + div {
padding-inline: 0;
}

[data-slot="dialog-content"]:has(> [data-chat-viewer-header])
> [data-slot="dialog-title"]
+ div + div > button:is([aria-label="Previous image"], [aria-label="Next image"]) {
inset-block-start: auto;
inset-block-end: calc(env(safe-area-inset-bottom, 0px) + var(--spacing) * 2);
translate: none;
}
}
Loading
Loading