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
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"dependencies": {
"@corbits-builder/client": "workspace:*",
"@corbits-builder/schemas": "workspace:*",
"@corbits/react-ui": "0.4.0",
"@corbits/react-ui": "0.5.0",
"@radix-ui/react-dialog": "1.3.1",
"@radix-ui/react-dropdown-menu": "2.2.1",
"@radix-ui/react-slot": "1.5.0",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ export const wording = {
shareDocument: "Share as Markdown",
downloadDesign: "Download the design",
shareDesign: "Share the design",
downloadSheet: "Download as CSV",
shareSheet: "Share as CSV",
downloaded: (name: string) => `Downloaded ${name}.`,
shareReady: (name: string) => `${name} is ready to share.`,
shareNow: "Share now",
Expand Down
7 changes: 5 additions & 2 deletions apps/web/src/pages/workspace/artifact-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import {
DeliverableBody,
type DeliverableState,
fillsPane,
type ReadVersion,
} from "./deliverable-block.tsx";
import {
Expand Down Expand Up @@ -68,7 +69,7 @@ export const LIVE_TAB = "live-app";
const liveIds = panelTabIds(TAB_PREFIX, LIVE_TAB);
const THEMES_TAB = "design-themes";
const themesIds = panelTabIds(TAB_PREFIX, THEMES_TAB);
// A pane whose page fills it edge to edge: the app, a mockup or the design draft.
// A pane whose page fills it edge to edge: the app, a mockup, a spreadsheet or the design draft.
const FILL_PANE = "workspace-pane-fill";
const POINTER_TABS = "data-pointer-tabs";
const TAB_KEYS = new Set([
Expand Down Expand Up @@ -473,7 +474,9 @@ export function ArtifactPane({
<PanelPane
key={view.value}
value={view.value}
{...(view.part === "mockup" ? { className: FILL_PANE } : {})}
{...(view.part === "mockup" || (current && fillsPane(state))
? { className: FILL_PANE }
: {})}
id={ids.pane}
role="tabpanel"
aria-labelledby={ids.tab}
Expand Down
146 changes: 90 additions & 56 deletions apps/web/src/pages/workspace/deliverable-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { wording } from "../../config.ts";
import { useRef, useState } from "react";
import { Button, ShimmerText } from "@corbits/react-ui";
import { Button, CsvTable, ShimmerText } from "@corbits/react-ui";
import { Markdown } from "../../markdown.jsx";
import {
DESIGN_MEDIA_TYPE,
Expand All @@ -22,9 +22,9 @@ import { versionGroup } from "./version-menu.tsx";
import { useTipsGroup } from "./document-tips.tsx";
import { PaneBar } from "./pane-bar.tsx";
import {
csvFile,
fileStem,
markdownFile,
MARKDOWN_TYPE,
type PaneFile,
useFileGroup,
ZIP_TYPE,
Expand Down Expand Up @@ -61,15 +61,23 @@ export type DeliverableState =

function mediaKind(
mediaType: string,
): "html" | "design" | "json" | "prose" | "text" | "image" | "file" {
title: string,
): "html" | "design" | "json" | "prose" | "sheet" | "text" | "image" | "file" {
if (mediaType === "text/html") return "html";
if (mediaType === DESIGN_MEDIA_TYPE) return "design";
if (mediaType === "application/json") return "json";
if (mediaType === "text/markdown") return "prose";
if (mediaType === "text/plain") return "text";
// A material is titled with its file name; a CSV that is not UTF-8 is a data: URL, so it stays a file.
if (mediaType === "text/plain")
return title.toLowerCase().endsWith(".csv") ? "sheet" : "text";
return mediaType.startsWith("image/") ? "image" : "file";
}

/** A spreadsheet fills its pane edge to edge, as a mockup does. */
export const fillsPane = (state: DeliverableState | undefined) =>
state?.status === "ready" &&
mediaKind(state.document.mediaType, state.document.title) === "sheet";

export function DeliverableBody({
state,
designPart = "mockup",
Expand Down Expand Up @@ -132,15 +140,22 @@ export function DeliverableBody({
);
}

const documentFile = (file: File): PaneFile => ({
type: MARKDOWN_TYPE,
download: wording.paneFile.downloadDocument,
share: wording.paneFile.shareDocument,
const shownFile = (file: File, download: string, share: string): PaneFile => ({
type: file.type,
download,
share,
key: file.name,
prepare: "shown",
build: () => Promise.resolve({ file }),
});

const documentFile = (file: File) =>
shownFile(
file,
wording.paneFile.downloadDocument,
wording.paneFile.shareDocument,
);

function ReadyDocument({
document,
designPart,
Expand Down Expand Up @@ -193,7 +208,7 @@ function ReadyDocument({
const viewed = document.versions?.find((item) => item.version === viewing);
const mediaType = viewed?.mediaType ?? document.mediaType;
const title = viewed?.title ?? document.title;
const kind = mediaKind(mediaType);
const kind = mediaKind(mediaType, title);
const shown = readFor(viewing);
const approaches = shown?.approaches;
const chooseApproach =
Expand Down Expand Up @@ -274,28 +289,36 @@ function ReadyDocument({
) : null;
const stem = fileStem(
fileName.project,
fileName.document,
kind === "sheet"
? fileName.document.replace(/\.csv$/i, "")
: fileName.document,
`v${String(viewing)}`,
);
const design =
kind === "design" && afterText !== null ? parseBundle(afterText) : null;
const plan = designPart === "plan" ? design?.["DESIGN.md"] : undefined;
const source: PaneFile | null =
kind === "prose" && afterText !== null
? documentFile(markdownFile(stem, afterText))
: plan !== undefined
? documentFile(markdownFile(stem, plan))
: design !== null && designPart === "mockup"
? {
type: ZIP_TYPE,
download: wording.paneFile.downloadDesign,
share: wording.paneFile.shareDesign,
key: `${stem}.zip`,
prepare: "shown",
build: async () => ({ file: await zipFile(stem, design) }),
}
: null;
const files = useFileGroup(source);
const sourceFile = (): PaneFile | null => {
if (kind === "prose" && afterText !== null)
return documentFile(markdownFile(stem, afterText));
if (plan !== undefined) return documentFile(markdownFile(stem, plan));
if (design !== null && designPart === "mockup")
return {
type: ZIP_TYPE,
download: wording.paneFile.downloadDesign,
share: wording.paneFile.shareDesign,
key: `${stem}.zip`,
prepare: "shown",
build: async () => ({ file: await zipFile(stem, design) }),
};
if (kind === "sheet" && afterText !== null)
return shownFile(
csvFile(stem, afterText),
wording.paneFile.downloadSheet,
wording.paneFile.shareSheet,
);
return null;
};
const files = useFileGroup(sourceFile());
const barGroups = [
versionGroup({
versions: document.versions ?? [],
Expand All @@ -314,33 +337,42 @@ function ReadyDocument({
];
const visual =
kind === "html" || (kind === "design" && designPart === "mockup");
const reading =
kind === "html" ? (
<DevicePreview
html={body}
device={device}
onDevice={setDevice}
trailing={barGroups}
/>
) : kind === "design" ? (
<DesignPreview
content={body}
part={designPart}
device={device}
onDevice={setDevice}
trailing={barGroups}
/>
) : kind === "json" || kind === "text" ? (
<pre className="document-raw">{body}</pre>
) : kind === "image" ? (
<img className="document-image" src={body} alt={title} />
) : kind === "file" ? (
<p>
<a href={body} download={title}>
{wording.attach.open(title)}
</a>
</p>
) : comparing ? null : (
const readingView = () => {
if (kind === "html")
return (
<DevicePreview
html={body}
device={device}
onDevice={setDevice}
trailing={barGroups}
/>
);
if (kind === "design")
return (
<DesignPreview
content={body}
part={designPart}
device={device}
onDevice={setDevice}
trailing={barGroups}
/>
);
if (kind === "sheet")
return <CsvTable text={body} caption={title} interactive />;
if (kind === "json" || kind === "text")
return <pre className="document-raw">{body}</pre>;
if (kind === "image")
return <img className="document-image" src={body} alt={title} />;
if (kind === "file")
return (
<p>
<a href={body} download={title}>
{wording.attach.open(title)}
</a>
</p>
);
if (comparing) return null;
return (
<>
{approaches === undefined ? null : (
<ApproachesComparison
Expand All @@ -361,10 +393,12 @@ function ReadyDocument({
<Markdown source={body} />
</>
);
};
const add = onAddToPrompt === undefined ? {} : { onAdd: onAddToPrompt };

// A mockup fills the pane edge to edge: its tab names it, and its notes float over the page.
const fill = kind === "design" && designPart === "mockup";
// A mockup or spreadsheet fills the pane edge to edge: its tab names it, and its notes float over the page.
const fill =
(kind === "design" && designPart === "mockup") || kind === "sheet";
const notes = (
<>
{viewing !== document.version ? (
Expand Down Expand Up @@ -411,7 +445,7 @@ function ReadyDocument({
</>
)}
<PassageQuotes title={title} version={viewing} {...add}>
{reading}
{readingView()}
{comparison}
</PassageQuotes>
{visual ? null : (
Expand Down
12 changes: 9 additions & 3 deletions apps/web/src/pages/workspace/pane-file.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/**
* A pane's Download and Share actions for the one file it gives away: a document as Markdown, a
* design as a zip, the app as the hub's zip. Both actions take the same file, built before the
* press where it can be, because a share sheet opens only inside the press that asked for it.
* spreadsheet as its CSV, a design as a zip, the app as the hub's zip. Both actions take the same
* file, built before the press where it can be, because a share sheet opens only inside the press
* that asked for it.
*/
import JSZip from "jszip";
import { useEffect, useRef } from "react";
Expand All @@ -10,12 +11,14 @@ import { toast } from "sonner";
import { wording } from "../../config.ts";
import type { PaneBarGroup } from "./pane-bar.tsx";

export const MARKDOWN_TYPE = "text/markdown";
const MARKDOWN_TYPE = "text/markdown";
export const ZIP_TYPE = "application/zip";
const CSV_TYPE = "text/csv";

const EXTENSION: Record<string, string> = {
[MARKDOWN_TYPE]: "md",
[ZIP_TYPE]: "zip",
[CSV_TYPE]: "csv",
};

/** A file name a person's system keeps as one name, as the hub names the app's zip. */
Expand All @@ -29,6 +32,9 @@ export const fileStem = (...parts: readonly string[]) =>
export const markdownFile = (stem: string, markdown: string) =>
new File([markdown], `${stem}.md`, { type: MARKDOWN_TYPE });

export const csvFile = (stem: string, csv: string) =>
new File([csv], `${stem}.csv`, { type: CSV_TYPE });

/** The files under one directory named after the zip, as the app's zip is laid out. */
export async function zipFile(
stem: string,
Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/pages/workspace/passage-note.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ function passageQuote(
root: HTMLElement,
): string | null {
if (!(target instanceof Element)) return null;
if (target.closest("button, a, input, textarea, select, label, iframe"))
// A click in a grid picks a cell, so it offers no quote.
if (
target.closest(
"button, a, input, textarea, select, label, iframe, [role=grid]",
)
)
return null;
const block = target.closest(
"p, li, h1, h2, h3, h4, blockquote, pre, td, th",
Expand Down
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions scripts/react-ui-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const groups = {
controls: {
title: "buttons, inputs, filters, dialogs, menus and toasts",
modules: [
"action-bar",
"bulk-action-bar",
"button",
"checkbox",
Expand Down Expand Up @@ -93,6 +94,7 @@ const groups = {
"rich-empty-state",
"shimmer-text",
"skeleton",
"sort-header-button",
"sortable-table",
"sparkline",
"stat-grid",
Expand Down
2 changes: 1 addition & 1 deletion workflows/coder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"./assets": "./src/assets.ts"
},
"dependencies": {
"@corbits/react-ui": "0.4.0",
"@corbits/react-ui": "0.5.0",
"@intx/agent": "workspace:*",
"@intx/workflow": "workspace:*"
}
Expand Down
4 changes: 2 additions & 2 deletions workflows/coder/starter/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading