@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 +1 -1
- package/package.json +1 -1
- package/src/components/pages/gallery-page.tsx +158 -0
- package/src/core/MarimoApp.tsx +8 -0
- package/src/core/mode.ts +4 -3
- package/src/core/run-app.tsx +26 -1
- package/src/mount.tsx +9 -7
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-
|
|
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
|
@@ -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;
|
package/src/core/MarimoApp.tsx
CHANGED
|
@@ -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
|
|
32
|
+
if (mode === "read" || mode === "home" || mode === "gallery") {
|
|
33
|
+
return mode;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
return mode === "edit" ? "present" : "edit";
|
package/src/core/run-app.tsx
CHANGED
|
@@ -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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
*/
|