diff --git a/src/components/general/workspace-avatar.tsx b/src/components/general/workspace-avatar.tsx
index 4c7bab1..4161d03 100644
--- a/src/components/general/workspace-avatar.tsx
+++ b/src/components/general/workspace-avatar.tsx
@@ -5,6 +5,7 @@ import { twMerge } from "tailwind-merge";
import profileImageUrls from "@/src/utils/profile-image-urls";
const WorkspaceAvatarSizes = {
+ xs: { box: "size-[18px] rounded-[4px]", icon: 12 },
sm: { box: "size-8 rounded-default-sm", icon: 16 },
lg: { box: "size-14 rounded-default", icon: 24 },
} as const;
diff --git a/src/data/app-data.ts b/src/data/app-data.ts
index 3ff3c52..440749a 100644
--- a/src/data/app-data.ts
+++ b/src/data/app-data.ts
@@ -252,11 +252,16 @@ export const AppData = {
* Worker bots the rail lists, most recently active first, before
* "See all bots".
*/
- RailRecentBots: 8,
+ RailRecentBots: 5,
/**
- * Stones shown beside "See all bots".
+ * Workspaces the rail lists, most recently active first, before
+ * "See all workspaces".
*/
- RailSeeAllStones: 4,
+ RailRecentWorkspaces: 5,
+ /**
+ * Stones shown beside "See all bots" and "See all workspaces".
+ */
+ RailSeeAllStones: 3,
/**
* Recent sessions the rail lists under the open bot, before "All
* sessions".
diff --git a/src/layouts/bots/(partials)/all-workspaces-modal.tsx b/src/layouts/bots/(partials)/all-workspaces-modal.tsx
new file mode 100644
index 0000000..3d4c7d1
--- /dev/null
+++ b/src/layouts/bots/(partials)/all-workspaces-modal.tsx
@@ -0,0 +1,51 @@
+import { useState } from "react";
+import Input from "@moduletrace/twui/form/Input";
+import Span from "@moduletrace/twui/layout/Span";
+import Stack from "@moduletrace/twui/layout/Stack";
+import type { WorkspaceWithActivity } from "@/src/types";
+import AddModalShell from "./add-modal-shell";
+import AllWorkspacesRow from "./all-workspaces-row";
+
+type Props = {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ workspaces: WorkspaceWithActivity[];
+};
+
+/**
+ * Every workspace, latest activity first, with a search by name or
+ * description. A workspace opens its page.
+ */
+export default function AllWorkspacesModal({ open, setOpen, workspaces }: Props) {
+ const [query, setQuery] = useState("");
+ const search = query.trim().toLowerCase();
+ const found = search
+ ? workspaces.filter((workspace) =>
+ `${workspace.name} ${workspace.description || ""}`.toLowerCase().includes(search),
+ )
+ : workspaces;
+
+ return (
+
+ setQuery(e.target.value)}
+ wrapperWrapperProps={{ className: "mt-4" }}
+ />
+
+ {found.map((workspace) => (
+
+ ))}
+ {found.length ? null : (
+
+ No workspace matches "{query.trim()}".
+
+ )}
+
+
+ );
+}
diff --git a/src/layouts/bots/(partials)/all-workspaces-row.tsx b/src/layouts/bots/(partials)/all-workspaces-row.tsx
new file mode 100644
index 0000000..d3d93f0
--- /dev/null
+++ b/src/layouts/bots/(partials)/all-workspaces-row.tsx
@@ -0,0 +1,37 @@
+import Link from "@moduletrace/twui/layout/Link";
+import Span from "@moduletrace/twui/layout/Span";
+import Stack from "@moduletrace/twui/layout/Stack";
+import WorkspaceAvatar from "@/src/components/general/workspace-avatar";
+import type { WorkspaceWithActivity } from "@/src/types";
+import relativeTime from "@/src/utils/relative-time";
+
+type Props = {
+ workspace: WorkspaceWithActivity;
+};
+
+/**
+ * One workspace in the all-workspaces modal: tile, name, description, last
+ * activity.
+ */
+export default function AllWorkspacesRow({ workspace }: Props) {
+ return (
+
+
+
+ {workspace.name}
+
+ {workspace.unread ? "New messages" : workspace.description || ""}
+
+
+ {workspace.last_activity_at ? (
+
+ {relativeTime({ at: Number(workspace.last_activity_at) })}
+
+ ) : null}
+
+ );
+}
diff --git a/src/layouts/bots/(partials)/rail-bots-section.tsx b/src/layouts/bots/(partials)/rail-bots-section.tsx
index fcf8bb8..7a2f2b7 100644
--- a/src/layouts/bots/(partials)/rail-bots-section.tsx
+++ b/src/layouts/bots/(partials)/rail-bots-section.tsx
@@ -7,6 +7,7 @@ import AllBotsModal from "./all-bots-modal";
import RailBotEntry from "./rail-bot-entry";
import RailSectionLabel from "./rail-section-label";
import RailSeeAllRow from "./rail-see-all-row";
+import RailSeeAllStone from "./rail-see-all-stone";
type Props = {
title: string;
@@ -55,7 +56,9 @@ export default function RailBotsSection({ title, workers, selected_id, workspace
)}
{hidden.length ? (
(
+
+ ))}
label={`See all ${workers.length} bots`}
onOpen={() => setOpen(true)}
/>
diff --git a/src/layouts/bots/(partials)/rail-see-all-row.tsx b/src/layouts/bots/(partials)/rail-see-all-row.tsx
index 39454cb..9dee893 100644
--- a/src/layouts/bots/(partials)/rail-see-all-row.tsx
+++ b/src/layouts/bots/(partials)/rail-see-all-row.tsx
@@ -1,26 +1,24 @@
+import type { ReactNode } from "react";
import Button from "@moduletrace/twui/layout/Button";
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import { Ellipsis } from "lucide-react";
-import { AppData } from "@/src/data/app-data";
-import type { BotWithLastMessage } from "@/src/types";
-import RailSeeAllStone from "./rail-see-all-stone";
type Props = {
/**
- * The bots the rail doesn't list.
+ * A few of the records the rail doesn't list, e.g. their stones.
*/
- hidden: BotWithLastMessage[];
+ stones?: ReactNode;
label: string;
onOpen: () => void;
};
/**
- * Under the listed bots: a few stones of the ones that aren't listed and
- * "See all bots", which opens them all. An ellipsis when the rail
+ * Under the listed records: a few stones of the ones that aren't listed and
+ * "See all", which opens every one in a modal. An ellipsis when the rail
* collapses.
*/
-export default function RailSeeAllRow({ hidden, label, onOpen }: Props) {
+export default function RailSeeAllRow({ stones, label, onOpen }: Props) {
return (
);
}
diff --git a/src/layouts/bots/(partials)/rail-see-all-workspace.tsx b/src/layouts/bots/(partials)/rail-see-all-workspace.tsx
new file mode 100644
index 0000000..1e27f46
--- /dev/null
+++ b/src/layouts/bots/(partials)/rail-see-all-workspace.tsx
@@ -0,0 +1,13 @@
+import WorkspaceAvatar from "@/src/components/general/workspace-avatar";
+import type { WorkspaceWithActivity } from "@/src/types";
+
+type Props = {
+ workspace: WorkspaceWithActivity;
+};
+
+/**
+ * One small workspace tile beside "See all workspaces".
+ */
+export default function RailSeeAllWorkspace({ workspace }: Props) {
+ return ;
+}
diff --git a/src/layouts/bots/(partials)/rail-workspaces-section.tsx b/src/layouts/bots/(partials)/rail-workspaces-section.tsx
index ab5a2de..df298d8 100644
--- a/src/layouts/bots/(partials)/rail-workspaces-section.tsx
+++ b/src/layouts/bots/(partials)/rail-workspaces-section.tsx
@@ -1,9 +1,13 @@
-import { useContext } from "react";
+import { useContext, useState } from "react";
import Span from "@moduletrace/twui/layout/Span";
+import { AppData } from "@/src/data/app-data";
import type { WorkspaceWithActivity } from "@/src/types";
import { AddFlowContext } from "../(data)/add-flow-context";
+import AllWorkspacesModal from "./all-workspaces-modal";
import RailSectionAddButton from "./rail-section-add-button";
import RailSectionLabel from "./rail-section-label";
+import RailSeeAllRow from "./rail-see-all-row";
+import RailSeeAllWorkspace from "./rail-see-all-workspace";
import WorkspaceRow from "./workspace-row";
type Props = {
@@ -12,10 +16,15 @@ type Props = {
};
/**
- * The user's workspaces, with a "+" to create one.
+ * The user's workspaces, with a "+" to create one: the latest
+ * `AppData.RailRecentWorkspaces`, then "See all workspaces" for the rest,
+ * which lists them all in a modal.
*/
export default function RailWorkspacesSection({ workspaces, selected_id }: Props) {
const { openForm } = useContext(AddFlowContext);
+ const [open, setOpen] = useState(false);
+ const shown = workspaces.slice(0, AppData.RailRecentWorkspaces);
+ const hidden = workspaces.slice(AppData.RailRecentWorkspaces);
return (
<>
@@ -28,7 +37,7 @@ export default function RailWorkspacesSection({ workspaces, selected_id }: Props
/>
}
/>
- {workspaces.map((workspace) => (
+ {shown.map((workspace) => (
))}
{workspaces.length ? null : (
@@ -36,6 +45,16 @@ export default function RailWorkspacesSection({ workspaces, selected_id }: Props
No workspaces yet. A workspace keeps a project's context, bots and computers together.
)}
+ {hidden.length ? (
+ (
+
+ ))}
+ label={`See all ${workspaces.length} workspaces`}
+ onOpen={() => setOpen(true)}
+ />
+ ) : null}
+
>
);
}