@marimo-team/islands 0.19.8-dev19 → 0.19.8-dev23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.19.8-dev19",
3
+ "version": "0.19.8-dev23",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -609,9 +609,13 @@ export const CustomProvidersConfig: React.FC<AiConfigProps> = ({
609
609
  const isDuplicate =
610
610
  KNOWN_PROVIDERS.includes(normalizedName as KnownProviderId) ||
611
611
  (customProviders && Object.keys(customProviders).includes(normalizedName));
612
+ const hasInvalidChars = normalizedName.includes(".");
612
613
 
613
614
  const hasValidValues =
614
- normalizedName.trim() && newProviderBaseUrl.trim() && !isDuplicate;
615
+ normalizedName.trim() &&
616
+ newProviderBaseUrl.trim() &&
617
+ !isDuplicate &&
618
+ !hasInvalidChars;
615
619
 
616
620
  const resetForm = () => {
617
621
  setNewProviderName("");
@@ -669,7 +673,12 @@ export const CustomProvidersConfig: React.FC<AiConfigProps> = ({
669
673
  A provider with this name already exists.
670
674
  </p>
671
675
  )}
672
- {newProviderName && (
676
+ {hasInvalidChars && (
677
+ <p className="text-xs text-destructive">
678
+ Provider names cannot contain '.' characters.
679
+ </p>
680
+ )}
681
+ {newProviderName && !hasInvalidChars && (
673
682
  <p className="text-xs text-muted-secondary">
674
683
  Use models with prefix:{" "}
675
684
  <Kbd className="inline text-xs">{normalizedName}/</Kbd>
@@ -33,6 +33,15 @@ const tabTarget = (path: string): string => {
33
33
  return `${getSessionId()}-${encodeURIComponent(path)}`;
34
34
  };
35
35
 
36
+ const isHttpsUrl = (value: string): boolean => {
37
+ try {
38
+ const url = new URL(value);
39
+ return url.protocol === "https:";
40
+ } catch {
41
+ return false;
42
+ }
43
+ };
44
+
36
45
  const SEARCH_THRESHOLD = 10;
37
46
 
38
47
  const GalleryPage: React.FC = () => {
@@ -43,10 +52,10 @@ const GalleryPage: React.FC = () => {
43
52
  [],
44
53
  );
45
54
  const workspace = response.data;
46
- const files = workspace?.files ?? [];
47
- const root = workspace?.root ?? "";
48
55
 
49
56
  const formattedFiles = useMemo(() => {
57
+ const files = workspace?.files ?? [];
58
+ const root = workspace?.root ?? "";
50
59
  return files
51
60
  .filter((file) => !file.isDirectory)
52
61
  .map((file) => {
@@ -54,17 +63,28 @@ const GalleryPage: React.FC = () => {
54
63
  root && Paths.isAbsolute(file.path) && file.path.startsWith(root)
55
64
  ? Paths.rest(file.path, root)
56
65
  : file.path;
57
- const title = titleCase(Paths.basename(relativePath));
66
+ const title =
67
+ file.opengraph?.title ?? titleCase(Paths.basename(relativePath));
58
68
  const subtitle = titleCase(Paths.dirname(relativePath));
69
+ const description = file.opengraph?.description ?? "";
70
+ const opengraphImage = file.opengraph?.image;
71
+ const thumbnailUrl =
72
+ opengraphImage && isHttpsUrl(opengraphImage)
73
+ ? opengraphImage
74
+ : asURL(
75
+ `/og/thumbnail?file=${encodeURIComponent(relativePath)}`,
76
+ ).toString();
59
77
  return {
60
78
  ...file,
61
79
  relativePath,
62
80
  title,
63
81
  subtitle,
82
+ description,
83
+ thumbnailUrl,
64
84
  };
65
85
  })
66
86
  .sort((a, b) => a.relativePath.localeCompare(b.relativePath));
67
- }, [files, root]);
87
+ }, [workspace?.files, workspace?.root]);
68
88
 
69
89
  const filteredFiles = useMemo(() => {
70
90
  if (!searchQuery) {
@@ -130,8 +150,14 @@ const GalleryPage: React.FC = () => {
130
150
  target={tabTarget(file.path)}
131
151
  className="no-underline"
132
152
  >
133
- <Card className="h-full hover:bg-accent/20 transition-colors">
134
- <CardContent className="p-6">
153
+ <Card className="h-full overflow-hidden hover:bg-accent/20 transition-colors">
154
+ <img
155
+ src={file.thumbnailUrl}
156
+ alt={file.title}
157
+ loading="lazy"
158
+ className="w-full aspect-1200/630 object-cover border-b border-border/60"
159
+ />
160
+ <CardContent className="p-6 pt-4">
135
161
  <div className="flex flex-col gap-1">
136
162
  {file.subtitle && (
137
163
  <div className="text-sm font-semibold text-muted-foreground">
@@ -141,6 +167,11 @@ const GalleryPage: React.FC = () => {
141
167
  <div className="text-lg font-medium">
142
168
  {file.title}
143
169
  </div>
170
+ {file.description && (
171
+ <div className="text-sm text-muted-foreground line-clamp-3 mt-1">
172
+ {file.description}
173
+ </div>
174
+ )}
144
175
  </div>
145
176
  </CardContent>
146
177
  </Card>
@@ -76,4 +76,26 @@ describe("parseContent", () => {
76
76
  url: "https://avatars.githubusercontent.com/u/123",
77
77
  });
78
78
  });
79
+
80
+ it("preserves newlines between URLs", () => {
81
+ const parts = parseContent("https://marimo.io\nhttps://github.com\n");
82
+ expect(parts).toEqual([
83
+ { type: "url", url: "https://marimo.io" },
84
+ { type: "text", value: "\n" },
85
+ { type: "url", url: "https://github.com" },
86
+ { type: "text", value: "\n" },
87
+ ]);
88
+ });
89
+
90
+ it("preserves whitespace in mixed content", () => {
91
+ const parts = parseContent(
92
+ "Line 1: https://marimo.io\nLine 2: https://github.com",
93
+ );
94
+ expect(parts).toEqual([
95
+ { type: "text", value: "Line 1: " },
96
+ { type: "url", url: "https://marimo.io" },
97
+ { type: "text", value: "\nLine 2: " },
98
+ { type: "url", url: "https://github.com" },
99
+ ]);
100
+ });
79
101
  });
@@ -19,7 +19,7 @@ export function parseContent(text: string): ContentPart[] {
19
19
  return [{ type: "image", url: text }];
20
20
  }
21
21
 
22
- const parts = text.split(urlRegex).filter((part) => part.trim() !== "");
22
+ const parts = text.split(urlRegex).filter((part) => part !== "");
23
23
  return parts.map((part) => {
24
24
  const isUrl = urlRegex.test(part);
25
25
  if (isUrl) {