@marimo-team/islands 0.19.8-dev0 → 0.19.8-dev3

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/dist/main.js CHANGED
@@ -73175,7 +73175,7 @@ Image URL: ${r.imageUrl}`)), contextToXml({
73175
73175
  return Logger.warn("Failed to get version from mount config"), null;
73176
73176
  }
73177
73177
  }
73178
- const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.19.8-dev0"), showCodeInRunModeAtom = atom(true);
73178
+ const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.19.8-dev3"), showCodeInRunModeAtom = atom(true);
73179
73179
  atom(null);
73180
73180
  var import_compiler_runtime$88 = require_compiler_runtime();
73181
73181
  function useKeydownOnElement(e, r) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.19.8-dev0",
3
+ "version": "0.19.8-dev3",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,158 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { SearchIcon } from "lucide-react";
4
+ import type React from "react";
5
+ import { Suspense, useMemo, useState } from "react";
6
+ import { ErrorBoundary } from "@/components/editor/boundary/ErrorBoundary";
7
+ import { Spinner } from "@/components/icons/spinner";
8
+ import { Card, CardContent } from "@/components/ui/card";
9
+ import { Input } from "@/components/ui/input";
10
+ import { getSessionId } from "@/core/kernel/session";
11
+ import { useRequestClient } from "@/core/network/requests";
12
+ import { useAsyncData } from "@/hooks/useAsyncData";
13
+ import { Banner } from "@/plugins/impl/common/error-banner";
14
+ import { prettyError } from "@/utils/errors";
15
+ import { PathBuilder, Paths } from "@/utils/paths";
16
+ import { asURL } from "@/utils/url";
17
+
18
+ const capitalize = (word: string): string => {
19
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
20
+ };
21
+
22
+ const titleCase = (path: string): string => {
23
+ const delimiter = PathBuilder.guessDeliminator(path).deliminator;
24
+ return path
25
+ .replace(/\.[^./]+$/, "")
26
+ .split(delimiter)
27
+ .filter(Boolean)
28
+ .map((part) => part.split(/[_-]/).map(capitalize).join(" "))
29
+ .join(" > ");
30
+ };
31
+
32
+ const tabTarget = (path: string): string => {
33
+ return `${getSessionId()}-${encodeURIComponent(path)}`;
34
+ };
35
+
36
+ const SEARCH_THRESHOLD = 10;
37
+
38
+ const GalleryPage: React.FC = () => {
39
+ const { getWorkspaceFiles } = useRequestClient();
40
+ const [searchQuery, setSearchQuery] = useState("");
41
+ const response = useAsyncData(
42
+ () => getWorkspaceFiles({ includeMarkdown: false }),
43
+ [],
44
+ );
45
+ const workspace = response.data;
46
+ const files = workspace?.files ?? [];
47
+ const root = workspace?.root ?? "";
48
+
49
+ const formattedFiles = useMemo(() => {
50
+ return files
51
+ .filter((file) => !file.isDirectory)
52
+ .map((file) => {
53
+ const relativePath =
54
+ root && Paths.isAbsolute(file.path) && file.path.startsWith(root)
55
+ ? Paths.rest(file.path, root)
56
+ : file.path;
57
+ const title = titleCase(Paths.basename(relativePath));
58
+ const subtitle = titleCase(Paths.dirname(relativePath));
59
+ return {
60
+ ...file,
61
+ relativePath,
62
+ title,
63
+ subtitle,
64
+ };
65
+ })
66
+ .sort((a, b) => a.relativePath.localeCompare(b.relativePath));
67
+ }, [files, root]);
68
+
69
+ const filteredFiles = useMemo(() => {
70
+ if (!searchQuery) {
71
+ return formattedFiles;
72
+ }
73
+ const query = searchQuery.toLowerCase();
74
+ return formattedFiles.filter((file) =>
75
+ file.title.toLowerCase().includes(query),
76
+ );
77
+ }, [formattedFiles, searchQuery]);
78
+
79
+ if (response.isPending) {
80
+ return <Spinner centered={true} size="xlarge" className="mt-6" />;
81
+ }
82
+
83
+ if (response.error) {
84
+ return (
85
+ <Banner kind="danger" className="rounded p-4">
86
+ {prettyError(response.error)}
87
+ </Banner>
88
+ );
89
+ }
90
+
91
+ if (!workspace) {
92
+ return <Spinner centered={true} size="xlarge" className="mt-6" />;
93
+ }
94
+
95
+ return (
96
+ <Suspense>
97
+ <div className="flex flex-col gap-6 max-w-6xl container pt-5 pb-20 z-10">
98
+ <img src="logo.png" alt="marimo logo" className="w-48 mb-2" />
99
+ <ErrorBoundary>
100
+ <div className="flex flex-col gap-2">
101
+ {workspace.hasMore && (
102
+ <Banner kind="warn" className="rounded p-4">
103
+ Showing first {workspace.fileCount} files. Your workspace has
104
+ more files.
105
+ </Banner>
106
+ )}
107
+ {formattedFiles.length > SEARCH_THRESHOLD && (
108
+ <Input
109
+ id="search"
110
+ value={searchQuery}
111
+ icon={<SearchIcon className="h-4 w-4" />}
112
+ onChange={(event) => setSearchQuery(event.target.value)}
113
+ placeholder="Search"
114
+ rootClassName="mb-3"
115
+ className="mb-0 border-border"
116
+ />
117
+ )}
118
+ {filteredFiles.length === 0 ? (
119
+ <Banner kind="warn" className="rounded p-4">
120
+ No marimo apps found.
121
+ </Banner>
122
+ ) : (
123
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
124
+ {filteredFiles.map((file) => (
125
+ <a
126
+ key={file.path}
127
+ href={asURL(
128
+ `?file=${encodeURIComponent(file.relativePath)}`,
129
+ ).toString()}
130
+ target={tabTarget(file.path)}
131
+ className="no-underline"
132
+ >
133
+ <Card className="h-full hover:bg-accent/20 transition-colors">
134
+ <CardContent className="p-6">
135
+ <div className="flex flex-col gap-1">
136
+ {file.subtitle && (
137
+ <div className="text-sm font-semibold text-muted-foreground">
138
+ {file.subtitle}
139
+ </div>
140
+ )}
141
+ <div className="text-lg font-medium">
142
+ {file.title}
143
+ </div>
144
+ </div>
145
+ </CardContent>
146
+ </Card>
147
+ </a>
148
+ ))}
149
+ </div>
150
+ )}
151
+ </div>
152
+ </ErrorBoundary>
153
+ </div>
154
+ </Suspense>
155
+ );
156
+ };
157
+
158
+ export default GalleryPage;
@@ -34,10 +34,15 @@ const LazyRunPage = reactLazyWithPreload(
34
34
  const LazyEditPage = reactLazyWithPreload(
35
35
  () => import("@/components/pages/edit-page"),
36
36
  );
37
+ const LazyGalleryPage = reactLazyWithPreload(
38
+ () => import("@/components/pages/gallery-page"),
39
+ );
37
40
 
38
41
  export function preloadPage(mode: string) {
39
42
  if (mode === "home") {
40
43
  LazyHomePage.preload();
44
+ } else if (mode === "gallery") {
45
+ LazyGalleryPage.preload();
41
46
  } else if (mode === "read") {
42
47
  LazyRunPage.preload();
43
48
  } else {
@@ -58,6 +63,9 @@ export const MarimoApp: React.FC = memo(() => {
58
63
  if (initialMode === "home") {
59
64
  return <LazyHomePage.Component />;
60
65
  }
66
+ if (initialMode === "gallery") {
67
+ return <LazyGalleryPage.Component />;
68
+ }
61
69
  if (initialMode === "read") {
62
70
  return <LazyRunPage.Component appConfig={appConfig} />;
63
71
  }
package/src/core/mode.ts CHANGED
@@ -13,8 +13,9 @@ import { store } from "./state/jotai";
13
13
  * - `edit`: A user is editing the notebook. Can switch to present mode.
14
14
  * - `present`: A user is presenting the notebook, it looks like read mode but with some editing features. Cannot switch to present mode.
15
15
  * - `home`: A user is in the home page.
16
+ * - `gallery`: A user is in the gallery page.
16
17
  */
17
- export type AppMode = "read" | "edit" | "present" | "home";
18
+ export type AppMode = "read" | "edit" | "present" | "home" | "gallery";
18
19
 
19
20
  export function getInitialAppMode(): Exclude<AppMode, "present"> {
20
21
  const initialMode = store.get(initialModeAtom);
@@ -28,8 +29,8 @@ export function getInitialAppMode(): Exclude<AppMode, "present"> {
28
29
 
29
30
  export function toggleAppMode(mode: AppMode): AppMode {
30
31
  // Can't switch to present mode.
31
- if (mode === "read") {
32
- return "read";
32
+ if (mode === "read" || mode === "home" || mode === "gallery") {
33
+ return mode;
33
34
  }
34
35
 
35
36
  return mode === "edit" ? "present" : "edit";
@@ -56,13 +56,38 @@ export const RunApp: React.FC<AppProps> = ({ appConfig }) => {
56
56
  return <CellsRenderer appConfig={appConfig} mode="read" />;
57
57
  };
58
58
 
59
+ const galleryHref = (() => {
60
+ if (typeof window === "undefined") {
61
+ return null;
62
+ }
63
+ const url = new URL(window.location.href);
64
+ if (!url.searchParams.has("file")) {
65
+ return null;
66
+ }
67
+ url.searchParams.delete("file");
68
+ const search = url.searchParams.toString();
69
+ return search ? `${url.pathname}?${search}` : url.pathname;
70
+ })();
71
+
59
72
  return (
60
73
  <AppContainer
61
74
  connection={connection}
62
75
  isRunning={isRunning}
63
76
  width={appConfig.width}
64
77
  >
65
- <AppHeader connection={connection} className={"sm:pt-8"} />
78
+ <AppHeader connection={connection} className={"sm:pt-8"}>
79
+ {galleryHref && (
80
+ <div className="flex items-center px-6 pt-4">
81
+ <a
82
+ href={galleryHref}
83
+ aria-label="Back to gallery"
84
+ className="inline-flex items-center"
85
+ >
86
+ <img src="logo.png" alt="marimo logo" className="h-6 w-auto" />
87
+ </a>
88
+ </div>
89
+ )}
90
+ </AppHeader>
66
91
  {renderCells()}
67
92
  </AppContainer>
68
93
  );
package/src/mount.tsx CHANGED
@@ -153,14 +153,16 @@ const mountOptionsSchema = z.object({
153
153
  .nullish()
154
154
  .transform((val) => val ?? "unknown"),
155
155
  /**
156
- * 'edit' or 'read'/'run' or 'home'
156
+ * 'edit' or 'read'/'run' or 'home' or 'gallery'
157
157
  */
158
- mode: z.enum(["edit", "read", "home", "run"]).transform((val): AppMode => {
159
- if (val === "run") {
160
- return "read";
161
- }
162
- return val;
163
- }),
158
+ mode: z
159
+ .enum(["edit", "read", "home", "run", "gallery"])
160
+ .transform((val): AppMode => {
161
+ if (val === "run") {
162
+ return "read";
163
+ }
164
+ return val;
165
+ }),
164
166
  /**
165
167
  * marimo config
166
168
  */