@marimo-team/islands 0.21.2-dev43 → 0.21.2-dev47
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 +11 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/storage/storage-file-viewer.tsx +35 -1
- package/src/components/storage/storage-inspector.tsx +8 -2
- package/src/components/storage/storage-snippets.ts +3 -3
- package/src/css/md-tooltip.css +4 -39
- package/src/plugins/core/RenderHTML.tsx +17 -0
- package/src/plugins/core/__test__/RenderHTML.test.ts +45 -0
package/package.json
CHANGED
|
@@ -4,27 +4,32 @@ import { FileIcon, LoaderCircle, RefreshCwIcon } from "lucide-react";
|
|
|
4
4
|
import type React from "react";
|
|
5
5
|
import { useCallback } from "react";
|
|
6
6
|
import { useLocale } from "react-aria";
|
|
7
|
+
import { useAddCodeToNewCell } from "@/components/editor/cell/useAddCell";
|
|
7
8
|
import { FilePreviewHeader } from "@/components/editor/file-tree/file-header";
|
|
8
9
|
import { renderFileIcon } from "@/components/editor/file-tree/file-icons";
|
|
9
10
|
import {
|
|
10
11
|
FileContentRenderer,
|
|
11
12
|
isMediaMime,
|
|
12
13
|
} from "@/components/editor/file-tree/renderers";
|
|
14
|
+
import { Tooltip } from "@/components/ui/tooltip";
|
|
13
15
|
import { toast } from "@/components/ui/use-toast";
|
|
14
16
|
import { DownloadStorage } from "@/core/storage/request-registry";
|
|
15
|
-
import type { StorageEntry } from "@/core/storage/types";
|
|
17
|
+
import type { StorageEntry, StorageNamespace } from "@/core/storage/types";
|
|
16
18
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
17
19
|
import { downloadByURL } from "@/utils/download";
|
|
18
20
|
import { formatBytes } from "@/utils/formatting";
|
|
19
21
|
import { Logger } from "@/utils/Logger";
|
|
20
22
|
import { CopyClipboardIcon } from "../icons/copy-icon";
|
|
21
23
|
import { Button } from "../ui/button";
|
|
24
|
+
import { STORAGE_SNIPPETS } from "./storage-snippets";
|
|
22
25
|
|
|
23
26
|
const MAX_MEDIA_PREVIEW_SIZE = 100 * 1024 * 1024; // 100 MB
|
|
24
27
|
|
|
25
28
|
interface Props {
|
|
26
29
|
entry: StorageEntry;
|
|
27
30
|
namespace: string;
|
|
31
|
+
protocol: string;
|
|
32
|
+
backendType: StorageNamespace["backendType"];
|
|
28
33
|
onBack: () => void;
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -41,9 +46,12 @@ type PreviewData =
|
|
|
41
46
|
export const StorageFileViewer: React.FC<Props> = ({
|
|
42
47
|
entry,
|
|
43
48
|
namespace,
|
|
49
|
+
protocol,
|
|
50
|
+
backendType,
|
|
44
51
|
onBack,
|
|
45
52
|
}) => {
|
|
46
53
|
const { locale } = useLocale();
|
|
54
|
+
const addCodeToNewCell = useAddCodeToNewCell();
|
|
47
55
|
const name = displayName(entry.path);
|
|
48
56
|
const mime = entry.mimeType || "text/plain";
|
|
49
57
|
const isMedia = isMediaMime(mime);
|
|
@@ -111,12 +119,38 @@ export const StorageFileViewer: React.FC<Props> = ({
|
|
|
111
119
|
}
|
|
112
120
|
}, [namespace, entry.path, name]);
|
|
113
121
|
|
|
122
|
+
const snippetActions = STORAGE_SNIPPETS.map((snippet) => {
|
|
123
|
+
const code = snippet.getCode({
|
|
124
|
+
variableName: namespace,
|
|
125
|
+
protocol,
|
|
126
|
+
entry,
|
|
127
|
+
backendType,
|
|
128
|
+
});
|
|
129
|
+
if (code === null) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const Icon = snippet.icon;
|
|
133
|
+
return (
|
|
134
|
+
<Tooltip key={snippet.id} content={snippet.label}>
|
|
135
|
+
<Button
|
|
136
|
+
variant="text"
|
|
137
|
+
size="xs"
|
|
138
|
+
onClick={() => addCodeToNewCell(code)}
|
|
139
|
+
aria-label={snippet.label}
|
|
140
|
+
>
|
|
141
|
+
<Icon className="h-3.5 w-3.5" />
|
|
142
|
+
</Button>
|
|
143
|
+
</Tooltip>
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
114
147
|
const header = (
|
|
115
148
|
<FilePreviewHeader
|
|
116
149
|
filename={name}
|
|
117
150
|
filenameIcon={renderFileIcon(name)}
|
|
118
151
|
onBack={onBack}
|
|
119
152
|
onDownload={handleDownload}
|
|
153
|
+
actions={snippetActions}
|
|
120
154
|
/>
|
|
121
155
|
);
|
|
122
156
|
|
|
@@ -64,6 +64,8 @@ import { STORAGE_SNIPPETS } from "./storage-snippets";
|
|
|
64
64
|
interface OpenFileInfo {
|
|
65
65
|
entry: StorageEntry;
|
|
66
66
|
namespace: string;
|
|
67
|
+
protocol: string;
|
|
68
|
+
backendType: StorageNamespace["backendType"];
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
// Pixels per depth level. Applied as paddingLeft on each full-width item
|
|
@@ -315,7 +317,7 @@ const StorageEntryRow: React.FC<{
|
|
|
315
317
|
if (isDir) {
|
|
316
318
|
setIsExpanded(!effectiveExpanded);
|
|
317
319
|
} else {
|
|
318
|
-
onOpenFile({ entry, namespace });
|
|
320
|
+
onOpenFile({ entry, namespace, protocol, backendType });
|
|
319
321
|
}
|
|
320
322
|
}}
|
|
321
323
|
>
|
|
@@ -361,7 +363,9 @@ const StorageEntryRow: React.FC<{
|
|
|
361
363
|
>
|
|
362
364
|
{!isDir && (
|
|
363
365
|
<DropdownMenuItem
|
|
364
|
-
onSelect={() =>
|
|
366
|
+
onSelect={() =>
|
|
367
|
+
onOpenFile({ entry, namespace, protocol, backendType })
|
|
368
|
+
}
|
|
365
369
|
>
|
|
366
370
|
<ViewIcon className={MENU_ITEM_ICON_CLASS} />
|
|
367
371
|
View
|
|
@@ -585,6 +589,8 @@ export const StorageInspector: React.FC = () => {
|
|
|
585
589
|
<StorageFileViewer
|
|
586
590
|
entry={openFile.entry}
|
|
587
591
|
namespace={openFile.namespace}
|
|
592
|
+
protocol={openFile.protocol}
|
|
593
|
+
backendType={openFile.backendType}
|
|
588
594
|
onBack={() => setOpenFile(null)}
|
|
589
595
|
/>
|
|
590
596
|
)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import type { LucideIcon } from "lucide-react";
|
|
4
|
-
import {
|
|
4
|
+
import { BookPlusIcon, FileSymlink } from "lucide-react";
|
|
5
5
|
import type { StorageEntry, StorageNamespace } from "@/core/storage/types";
|
|
6
6
|
|
|
7
7
|
type BackendType = StorageNamespace["backendType"];
|
|
@@ -31,7 +31,7 @@ export const STORAGE_SNIPPETS: StorageSnippet[] = [
|
|
|
31
31
|
{
|
|
32
32
|
id: "read-file",
|
|
33
33
|
label: "Insert read snippet",
|
|
34
|
-
icon:
|
|
34
|
+
icon: BookPlusIcon,
|
|
35
35
|
getCode: (ctx) => {
|
|
36
36
|
if (ctx.entry.kind === "directory") {
|
|
37
37
|
return null;
|
|
@@ -46,7 +46,7 @@ export const STORAGE_SNIPPETS: StorageSnippet[] = [
|
|
|
46
46
|
{
|
|
47
47
|
id: "download-file",
|
|
48
48
|
label: "Insert download snippet",
|
|
49
|
-
icon:
|
|
49
|
+
icon: FileSymlink,
|
|
50
50
|
getCode: (ctx) => {
|
|
51
51
|
if (ctx.entry.kind === "directory") {
|
|
52
52
|
return null;
|
package/src/css/md-tooltip.css
CHANGED
|
@@ -3,50 +3,15 @@
|
|
|
3
3
|
/*
|
|
4
4
|
This allows you to create a basic tooltip using the data-tooltip attribute
|
|
5
5
|
e.g. <span data-tooltip="Hello, World!">Hover me</span>
|
|
6
|
+
|
|
7
|
+
The tooltip content is rendered via the React Tooltip component (Radix UI portal),
|
|
8
|
+
which prevents clipping inside containers with overflow:hidden.
|
|
9
|
+
See: RenderHTML.tsx -> wrapTooltipTargets
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
.markdown {
|
|
9
13
|
[data-tooltip] {
|
|
10
|
-
position: relative;
|
|
11
14
|
cursor: pointer;
|
|
12
15
|
text-decoration: underline dotted;
|
|
13
16
|
}
|
|
14
|
-
|
|
15
|
-
[data-tooltip]::before,
|
|
16
|
-
[data-tooltip]::after {
|
|
17
|
-
visibility: hidden;
|
|
18
|
-
opacity: 0;
|
|
19
|
-
pointer-events: none;
|
|
20
|
-
transition: all 0.2s ease;
|
|
21
|
-
position: absolute;
|
|
22
|
-
z-index: 1000;
|
|
23
|
-
left: 50%;
|
|
24
|
-
transform: translateX(-50%);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
[data-tooltip]::before {
|
|
28
|
-
content: attr(data-tooltip);
|
|
29
|
-
bottom: calc(100% + 10px);
|
|
30
|
-
padding: 5px 10px;
|
|
31
|
-
width: max-content;
|
|
32
|
-
max-width: 300px;
|
|
33
|
-
border-radius: 6px;
|
|
34
|
-
background-color: ;
|
|
35
|
-
text-align: center;
|
|
36
|
-
line-height: 1.4;
|
|
37
|
-
white-space: pre-wrap;
|
|
38
|
-
|
|
39
|
-
@apply bg-background text-foreground shadow-md border text-base;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
[data-tooltip]:hover::before,
|
|
43
|
-
[data-tooltip]:hover::after {
|
|
44
|
-
visibility: visible;
|
|
45
|
-
opacity: 1;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
[data-tooltip]:hover::before {
|
|
49
|
-
/* stylelint-disable-next-line unit-allowed-list */
|
|
50
|
-
transform: translateX(-50%) translateY(10px);
|
|
51
|
-
}
|
|
52
17
|
}
|
|
@@ -14,6 +14,7 @@ import React, {
|
|
|
14
14
|
} from "react";
|
|
15
15
|
import { CopyClipboardIcon } from "@/components/icons/copy-icon";
|
|
16
16
|
import { QueryParamPreservingLink } from "@/components/ui/query-param-preserving-link";
|
|
17
|
+
import { Tooltip } from "@/components/ui/tooltip";
|
|
17
18
|
import { DocHoverTarget } from "@/core/documentation/DocHoverTarget";
|
|
18
19
|
import { sanitizeHtml, useSanitizeHtml } from "./sanitize";
|
|
19
20
|
|
|
@@ -160,6 +161,21 @@ const wrapDocHoverTargets: TransformFn = (
|
|
|
160
161
|
}
|
|
161
162
|
};
|
|
162
163
|
|
|
164
|
+
// Wrap elements with data-tooltip attribute in a Tooltip component.
|
|
165
|
+
// This renders the tooltip in a portal (top layer), fixing clipping inside
|
|
166
|
+
// containers with overflow:hidden (e.g. grid cells).
|
|
167
|
+
const wrapTooltipTargets: TransformFn = (
|
|
168
|
+
reactNode: ReactNode,
|
|
169
|
+
domNode: DOMNode,
|
|
170
|
+
): JSX.Element | undefined => {
|
|
171
|
+
if (domNode instanceof Element && domNode.attribs?.["data-tooltip"]) {
|
|
172
|
+
const tooltipContent = domNode.attribs["data-tooltip"];
|
|
173
|
+
return (
|
|
174
|
+
<Tooltip content={tooltipContent}>{reactNode as JSX.Element}</Tooltip>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
163
179
|
const CopyableCode = ({ children }: { children: ReactNode }) => {
|
|
164
180
|
const ref = useRef<HTMLDivElement>(null);
|
|
165
181
|
return (
|
|
@@ -239,6 +255,7 @@ function parseHtml({
|
|
|
239
255
|
addCopyButtonToCodehilite,
|
|
240
256
|
preserveQueryParamsInAnchorLinks,
|
|
241
257
|
wrapDocHoverTargets,
|
|
258
|
+
wrapTooltipTargets,
|
|
242
259
|
removeWrappingBodyTags,
|
|
243
260
|
removeWrappingHtmlTags,
|
|
244
261
|
];
|
|
@@ -197,6 +197,51 @@ describe("parseHtml", () => {
|
|
|
197
197
|
});
|
|
198
198
|
});
|
|
199
199
|
|
|
200
|
+
describe("wrapTooltipTargets", () => {
|
|
201
|
+
test("data-tooltip wraps element in Tooltip component", () => {
|
|
202
|
+
const html = '<span data-tooltip="Hello world">Hover me</span>';
|
|
203
|
+
expect(parseHtml({ html })).toMatchInlineSnapshot(`
|
|
204
|
+
<Tooltip
|
|
205
|
+
content="Hello world"
|
|
206
|
+
>
|
|
207
|
+
<span
|
|
208
|
+
data-tooltip="Hello world"
|
|
209
|
+
>
|
|
210
|
+
Hover me
|
|
211
|
+
</span>
|
|
212
|
+
</Tooltip>
|
|
213
|
+
`);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("element without data-tooltip is not wrapped", () => {
|
|
217
|
+
const html = "<span>No tooltip</span>";
|
|
218
|
+
expect(parseHtml({ html })).toMatchInlineSnapshot(`
|
|
219
|
+
<span>
|
|
220
|
+
No tooltip
|
|
221
|
+
</span>
|
|
222
|
+
`);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("data-tooltip on nested element wraps only that element", () => {
|
|
226
|
+
const html = '<p>Outer <span data-tooltip="tip">inner</span> text</p>';
|
|
227
|
+
expect(parseHtml({ html })).toMatchInlineSnapshot(`
|
|
228
|
+
<p>
|
|
229
|
+
Outer
|
|
230
|
+
<Tooltip
|
|
231
|
+
content="tip"
|
|
232
|
+
>
|
|
233
|
+
<span
|
|
234
|
+
data-tooltip="tip"
|
|
235
|
+
>
|
|
236
|
+
inner
|
|
237
|
+
</span>
|
|
238
|
+
</Tooltip>
|
|
239
|
+
text
|
|
240
|
+
</p>
|
|
241
|
+
`);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
200
245
|
describe("parseHtml with < nad >", () => {
|
|
201
246
|
const html =
|
|
202
247
|
'thread <unnamed> panicked at "assertion failed: `(left == right)`"';
|