@marimo-team/islands 0.17.8 → 0.18.0

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.
Files changed (53) hide show
  1. package/dist/{Combination-BH_L276x.js → Combination-D68fi0fY.js} +22 -21
  2. package/dist/{ConnectedDataExplorerComponent-WbiFXhKG.js → ConnectedDataExplorerComponent-BUgUSo2B.js} +7 -7
  3. package/dist/{any-language-editor-YPQMljy9.js → any-language-editor-BS-Z5AY5.js} +3 -3
  4. package/dist/assets/__vite-browser-external-CSegkGa0.js +1 -0
  5. package/dist/assets/{worker-BrDpRi2I.js → worker-CiT2i-Vo.js} +2 -2
  6. package/dist/{error-banner-BqE1uF21.js → error-banner-CPLhCPHA.js} +24 -24
  7. package/dist/{esm-hR1r0nyt.js → esm-DxgKy8Wv.js} +1 -1
  8. package/dist/{formats-dvT8nDgH.js → formats-oddMfm9_.js} +27 -7
  9. package/dist/{glide-data-editor-B26PhZvE.js → glide-data-editor-BFv4VQnc.js} +4 -4
  10. package/dist/{label-D3LNCORf.js → label-Dsm6T1fr.js} +72 -72
  11. package/dist/main.js +359 -250
  12. package/dist/{mermaid-Dl3ywmV2.js → mermaid-BeGlg1JH.js} +2 -2
  13. package/dist/{react-vega-ypEMYp9o.js → react-vega-DDXWt_PN.js} +852 -1544
  14. package/dist/{react-vega-BIDT9Ttp.js → react-vega-DV2IwPx_.js} +1 -1
  15. package/dist/{spec-qDDGe5hl.js → spec-BotzCMo3.js} +2 -2
  16. package/dist/style.css +1 -1
  17. package/dist/{types-2eTEqSwS.js → types-IRrkdH-H.js} +14 -14
  18. package/dist/{useAsyncData-6gisQ4pR.js → useAsyncData-CsSW6_Zh.js} +1 -1
  19. package/dist/{useTheme-B-2frT0L.js → useTheme-D56Xlrez.js} +1 -0
  20. package/dist/{vega-component-C-bCSv1b.js → vega-component-CLjz4see.js} +6 -6
  21. package/package.json +2 -2
  22. package/src/components/chat/chat-panel.tsx +6 -2
  23. package/src/components/data-table/TableActions.tsx +18 -14
  24. package/src/components/data-table/data-table.tsx +3 -0
  25. package/src/components/editor/chrome/panels/packages-panel.tsx +3 -1
  26. package/src/components/editor/file-tree/__tests__/file-expolorer.test.ts +178 -0
  27. package/src/components/editor/file-tree/file-explorer.tsx +70 -1
  28. package/src/components/pages/home-page.tsx +8 -3
  29. package/src/core/ai/tools/__tests__/registry.test.ts +6 -2
  30. package/src/core/ai/tools/registry.ts +5 -2
  31. package/src/core/cells/__tests__/session.test.ts +0 -9
  32. package/src/core/cells/session.ts +0 -1
  33. package/src/core/codemirror/copilot/client.ts +21 -1
  34. package/src/core/codemirror/copilot/copilot-config.tsx +29 -1
  35. package/src/core/config/__tests__/config-schema.test.ts +2 -0
  36. package/src/core/config/config-schema.ts +1 -0
  37. package/src/core/packages/__tests__/package-input-utils.test.ts +93 -0
  38. package/src/core/packages/package-input-utils.ts +36 -0
  39. package/src/css/md.css +5 -0
  40. package/src/plugins/core/__test__/sanitize.test.ts +1 -1
  41. package/src/plugins/core/sanitize.ts +3 -1
  42. package/src/plugins/impl/DataTablePlugin.tsx +10 -1
  43. package/src/plugins/impl/chat/ChatPlugin.tsx +1 -0
  44. package/src/plugins/impl/chat/chat-ui.tsx +140 -10
  45. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +1 -0
  46. package/src/plugins/layout/NavigationMenuPlugin.tsx +14 -3
  47. package/src/plugins/layout/ProgressPlugin.tsx +8 -5
  48. package/src/plugins/layout/StatPlugin.tsx +11 -4
  49. package/src/plugins/layout/__test__/ProgressPlugin.test.ts +37 -21
  50. package/src/utils/__tests__/urls.test.ts +165 -1
  51. package/src/utils/urls.ts +120 -0
  52. package/src/utils/vitals.ts +1 -1
  53. package/dist/assets/__vite-browser-external-BTNiCQ6O.js +0 -1
@@ -17,6 +17,7 @@ interface Data {
17
17
  caption?: string;
18
18
  bordered?: boolean;
19
19
  direction?: "increase" | "decrease";
20
+ target_direction?: "increase" | "decrease";
20
21
  }
21
22
 
22
23
  export class StatPlugin implements IStatelessPlugin<Data> {
@@ -28,6 +29,7 @@ export class StatPlugin implements IStatelessPlugin<Data> {
28
29
  caption: z.string().optional(),
29
30
  bordered: z.boolean().default(false),
30
31
  direction: z.enum(["increase", "decrease"]).optional(),
32
+ target_direction: z.enum(["increase", "decrease"]).default("increase"),
31
33
  });
32
34
 
33
35
  render({ data }: IStatelessPluginProps<Data>): JSX.Element {
@@ -41,6 +43,7 @@ export const StatComponent: React.FC<Data> = ({
41
43
  caption,
42
44
  bordered,
43
45
  direction,
46
+ target_direction,
44
47
  }) => {
45
48
  const { locale } = useLocale();
46
49
 
@@ -64,6 +67,10 @@ export const StatComponent: React.FC<Data> = ({
64
67
  return String(value);
65
68
  };
66
69
 
70
+ const onTarget = direction === target_direction;
71
+ const fillColor = onTarget ? "var(--grass-8)" : "var(--red-8)";
72
+ const strokeColor = onTarget ? "var(--grass-9)" : "var(--red-9)";
73
+
67
74
  return (
68
75
  <div
69
76
  className={cn(
@@ -83,15 +90,15 @@ export const StatComponent: React.FC<Data> = ({
83
90
  {direction === "increase" && (
84
91
  <TriangleIcon
85
92
  className="w-4 h-4 mr-1 p-0.5"
86
- fill="var(--grass-8)"
87
- stroke="var(--grass-9)"
93
+ fill={fillColor}
94
+ stroke={strokeColor}
88
95
  />
89
96
  )}
90
97
  {direction === "decrease" && (
91
98
  <TriangleIcon
92
99
  className="w-4 h-4 mr-1 p-0.5 transform rotate-180"
93
- fill="var(--red-8)"
94
- stroke="var(--red-9)"
100
+ fill={fillColor}
101
+ stroke={strokeColor}
95
102
  />
96
103
  )}
97
104
  {caption}
@@ -2,25 +2,41 @@
2
2
  import { expect, test } from "vitest";
3
3
  import { prettyTime } from "../ProgressPlugin";
4
4
 
5
- // examples of expected output
6
- test("prettyTime", () => {
7
- // exact
8
- expect(prettyTime(0)).toMatchInlineSnapshot('"0s"');
9
- expect(prettyTime(1)).toMatchInlineSnapshot('"1s"');
10
- expect(prettyTime(60)).toMatchInlineSnapshot('"1m"');
11
- expect(prettyTime(60 * 60)).toMatchInlineSnapshot('"1h"');
12
- expect(prettyTime(60 * 60 * 24)).toMatchInlineSnapshot('"1d"');
13
-
14
- // decimal
15
- expect(prettyTime(0.5)).toMatchInlineSnapshot('"0.5s"');
16
- expect(prettyTime(1.5)).toMatchInlineSnapshot('"1.5s"');
17
- expect(prettyTime(60 * 1.5)).toMatchInlineSnapshot('"1m, 30s"');
18
- expect(prettyTime(60 * 60 * 1.5)).toMatchInlineSnapshot('"1h, 30m"');
19
- expect(prettyTime(60 * 60 * 24 * 1.5)).toMatchInlineSnapshot('"1d, 12h"');
20
-
5
+ const Cases: Array<[number, string]> = [
6
+ // exact values
7
+ [0, "0s"],
8
+ [1, "1s"],
9
+ [5, "5s"],
10
+ [15, "15s"],
11
+ [60, "1m"],
12
+ [100, "1m, 40s"],
13
+ [60 * 60, "1h"],
14
+ [60 * 60 * 24, "1d"],
15
+ [60 * 60 * 24 * 7, "1w"],
16
+ [60 * 60 * 24 * 8, "1w, 1d"],
17
+ [60 * 60 * 24 * 30, "4w, 2d"],
18
+ [60 * 60 * 24 * 366, "1y, 18h"],
19
+ [60 * 60 * 24 * 466, "1y, 3mo"],
20
+ // decimal values
21
+ [0.5, "0.5s"],
22
+ [1.5, "1.5s"],
23
+ [5.2, "5.2s"],
24
+ [5.33, "5.33s"],
25
+ [15.2, "15s"],
26
+ [60 * 1.5, "1m, 30s"],
27
+ [100.2, "1m, 40s"],
28
+ [60 * 60 * 1.5, "1h, 30m"],
29
+ [60 * 60 * 24 * 1.5, "1d, 12h"],
21
30
  // edge cases
22
- expect(prettyTime(0)).toMatchInlineSnapshot('"0s"');
23
- expect(prettyTime(0.0001)).toMatchInlineSnapshot('"0s"');
24
- expect(prettyTime(0.001)).toMatchInlineSnapshot('"0s"');
25
- expect(prettyTime(0.01)).toMatchInlineSnapshot('"0.01s"');
26
- });
31
+ [0, "0s"],
32
+ [0.0001, "0s"],
33
+ [0.001, "0s"],
34
+ [0.01, "0.01s"],
35
+ ];
36
+
37
+ // generate one test per pair
38
+ for (const [input, expected] of Cases) {
39
+ test(`prettyTime(${input}) → ${expected}`, () => {
40
+ expect(prettyTime(input)).toBe(expected);
41
+ });
42
+ }
@@ -4,7 +4,7 @@ import {
4
4
  EDGE_CASE_FILENAMES,
5
5
  URL_SPECIAL_CHAR_FILENAMES,
6
6
  } from "../../__tests__/mocks";
7
- import { isUrl, updateQueryParams } from "../urls";
7
+ import { appendQueryParams, isUrl, updateQueryParams } from "../urls";
8
8
 
9
9
  describe("isUrl", () => {
10
10
  it("should return true for a valid URL", () => {
@@ -63,3 +63,167 @@ describe("URL parameter handling with edge case filenames", () => {
63
63
  });
64
64
  });
65
65
  });
66
+
67
+ describe("appendQueryParams", () => {
68
+ it("should append params to a simple path", () => {
69
+ const result = appendQueryParams({
70
+ href: "/about",
71
+ queryParams: new URLSearchParams("file=test.py"),
72
+ });
73
+ expect(result).toBe("/about?file=test.py");
74
+ });
75
+
76
+ it("should append params to a hash-based path", () => {
77
+ const result = appendQueryParams({
78
+ href: "#/about",
79
+ queryParams: new URLSearchParams("file=test.py"),
80
+ });
81
+ expect(result).toBe("/?file=test.py#/about");
82
+ });
83
+
84
+ it("should accept query string instead of URLSearchParams", () => {
85
+ const result = appendQueryParams({
86
+ href: "/about",
87
+ queryParams: "file=test.py&mode=edit",
88
+ });
89
+ expect(result).toBe("/about?file=test.py&mode=edit");
90
+ });
91
+
92
+ it("should filter params by keys when provided", () => {
93
+ const result = appendQueryParams({
94
+ href: "/about",
95
+ queryParams: "file=test.py&mode=edit&extra=data",
96
+ keys: ["file", "mode"],
97
+ });
98
+ expect(result).toBe("/about?file=test.py&mode=edit");
99
+ });
100
+
101
+ it("should filter params by keys in the middle of the query string", () => {
102
+ const result = appendQueryParams({
103
+ href: "/about",
104
+ queryParams: "file=test.py&mode=edit&extra=data",
105
+ keys: ["file", "extra"],
106
+ });
107
+ expect(result).toBe("/about?file=test.py&extra=data");
108
+ });
109
+
110
+ it("should preserve existing query params", () => {
111
+ const result = appendQueryParams({
112
+ href: "/about?existing=1",
113
+ queryParams: "file=test.py",
114
+ });
115
+ expect(result).toBe("/about?existing=1&file=test.py");
116
+ });
117
+
118
+ it("should overwrite existing params with same key", () => {
119
+ const result = appendQueryParams({
120
+ href: "/about?file=old.py",
121
+ queryParams: "file=new.py",
122
+ });
123
+ expect(result).toBe("/about?file=new.py");
124
+ });
125
+
126
+ it("should preserve hash fragment and put params before it", () => {
127
+ const result = appendQueryParams({
128
+ href: "/about#section",
129
+ queryParams: "file=test.py",
130
+ });
131
+ expect(result).toBe("/about?file=test.py#section");
132
+ });
133
+
134
+ it("should handle hash-based path with existing params and hash", () => {
135
+ const result = appendQueryParams({
136
+ href: "#/about?existing=1",
137
+ queryParams: "file=test.py",
138
+ });
139
+ expect(result).toBe("#/about?existing=1&file=test.py");
140
+ });
141
+
142
+ it("should handle hash-based path", () => {
143
+ const result = appendQueryParams({
144
+ href: "#/about",
145
+ queryParams: "file=test.py",
146
+ });
147
+ expect(result).toBe("/?file=test.py#/about");
148
+ });
149
+
150
+ it("should not modify external links", () => {
151
+ const httpUrl = "http://example.com/page";
152
+ const httpsUrl = "https://example.com/page";
153
+
154
+ expect(
155
+ appendQueryParams({
156
+ href: httpUrl,
157
+ queryParams: "file=test.py",
158
+ }),
159
+ ).toBe(httpUrl);
160
+
161
+ expect(
162
+ appendQueryParams({
163
+ href: httpsUrl,
164
+ queryParams: "file=test.py",
165
+ }),
166
+ ).toBe(httpsUrl);
167
+ });
168
+
169
+ it("should return original href when no params to append", () => {
170
+ const result = appendQueryParams({
171
+ href: "/about",
172
+ queryParams: new URLSearchParams(),
173
+ });
174
+ expect(result).toBe("/about");
175
+ });
176
+
177
+ it("should handle complex scenarios with all features", () => {
178
+ const result = appendQueryParams({
179
+ href: "#/dashboard?view=grid#top",
180
+ queryParams: "file=notebook.py&view=list&mode=edit",
181
+ keys: ["file", "mode"],
182
+ });
183
+ // view=grid should remain, file and mode should be added (view is not in keys)
184
+ expect(result).toBe("#/dashboard?view=grid&file=notebook.py&mode=edit#top");
185
+ });
186
+
187
+ it("should handle empty path", () => {
188
+ const result = appendQueryParams({
189
+ href: "",
190
+ queryParams: "file=test.py",
191
+ });
192
+ expect(result).toBe("?file=test.py");
193
+ });
194
+
195
+ it("should handle just a hash", () => {
196
+ const result = appendQueryParams({
197
+ href: "#",
198
+ queryParams: "file=test.py",
199
+ });
200
+ expect(result).toBe("/?file=test.py#");
201
+ });
202
+
203
+ it("should handle unicode filenames in params", () => {
204
+ const result = appendQueryParams({
205
+ href: "/about",
206
+ queryParams: new URLSearchParams([
207
+ ["file", "文件.py"],
208
+ ["name", "テスト"],
209
+ ]),
210
+ });
211
+ expect(result).toContain("/about?");
212
+ // Verify the params are properly encoded
213
+ const url = new URL(result, "http://example.com");
214
+ expect(url.searchParams.get("file")).toBe("文件.py");
215
+ expect(url.searchParams.get("name")).toBe("テスト");
216
+ });
217
+
218
+ it("should handle special characters in param values", () => {
219
+ const result = appendQueryParams({
220
+ href: "/about",
221
+ queryParams: new URLSearchParams([
222
+ ["path", "folder/file with spaces.py"],
223
+ ]),
224
+ });
225
+ expect(result).toContain("/about?");
226
+ const url = new URL(result, "http://example.com");
227
+ expect(url.searchParams.get("path")).toBe("folder/file with spaces.py");
228
+ });
229
+ });
package/src/utils/urls.ts CHANGED
@@ -31,3 +31,123 @@ const urlRegex = /^(https?:\/\/\S+)$/;
31
31
  export function isUrl(value: unknown): boolean {
32
32
  return typeof value === "string" && urlRegex.test(value);
33
33
  }
34
+
35
+ /**
36
+ * Appends query parameters to an href, handling various edge cases.
37
+ *
38
+ * @param href - The href to append params to (e.g., "/path", "#/hash", "/path?existing=1", "/path#hash")
39
+ * @param queryParams - URLSearchParams or query string to append
40
+ * @param keys - Optional array of keys to filter which params to append
41
+ * @returns The href with appended query parameters
42
+ *
43
+ * @example
44
+ * appendQueryParams({ href: "/about", queryParams: new URLSearchParams("file=test.py") })
45
+ * // Returns: "/about?file=test.py"
46
+ *
47
+ * @example
48
+ * appendQueryParams({ href: "#/about", queryParams: "file=test.py&mode=edit", keys: ["file"] })
49
+ * // Returns: "/?file=test.py#/about"
50
+ *
51
+ * @example
52
+ * appendQueryParams({ href: "#/about?existing=1", queryParams: "file=test.py" })
53
+ * // Returns: "#/about?existing=1&file=test.py"
54
+ *
55
+ * @example
56
+ * appendQueryParams({ href: "/about?existing=1", queryParams: "file=test.py" })
57
+ * // Returns: "/about?existing=1&file=test.py"
58
+ *
59
+ * @example
60
+ * appendQueryParams({ href: "/about#section", queryParams: "file=test.py" })
61
+ * // Returns: "/about?file=test.py#section"
62
+ */
63
+ export function appendQueryParams({
64
+ href,
65
+ queryParams,
66
+ keys,
67
+ }: {
68
+ href: string;
69
+ queryParams: URLSearchParams | string;
70
+ keys?: string[];
71
+ }): string {
72
+ // Convert queryParams to URLSearchParams if it's a string
73
+ const params =
74
+ typeof queryParams === "string"
75
+ ? new URLSearchParams(queryParams)
76
+ : queryParams;
77
+
78
+ // If no params to append, return as is
79
+ if (params.size === 0) {
80
+ return href;
81
+ }
82
+
83
+ // Don't modify external links (full URLs)
84
+ if (href.startsWith("http://") || href.startsWith("https://")) {
85
+ return href;
86
+ }
87
+
88
+ // Special handling for hash-based routing
89
+ const isHashBased = href.startsWith("#");
90
+ const hasQueryInHash = isHashBased && href.includes("?");
91
+
92
+ if (isHashBased && !hasQueryInHash) {
93
+ // For hash-based hrefs without query params (e.g., #/about),
94
+ // put query params on the main path before the hash: /?params#/route
95
+ // This is common in SPAs where query params on the main URL need to be preserved
96
+ const paramsToAdd = keys
97
+ ? [...params.entries()].filter(([key]) => keys.includes(key))
98
+ : [...params.entries()];
99
+
100
+ const queryParams = new URLSearchParams();
101
+ for (const [key, value] of paramsToAdd) {
102
+ queryParams.set(key, value);
103
+ }
104
+
105
+ const queryString = queryParams.toString();
106
+ if (!queryString) {
107
+ return href;
108
+ }
109
+
110
+ return `/?${queryString}${href}`;
111
+ }
112
+
113
+ // Parse the href to extract parts for all other cases
114
+ let basePath = href;
115
+ let hash = "";
116
+ let existingParams = new URLSearchParams();
117
+
118
+ // For hash-based routing with query params, look for a second # to find actual hash fragment
119
+ const hashSearchStart = isHashBased ? 1 : 0;
120
+ const hashIndex = href.indexOf("#", hashSearchStart);
121
+
122
+ if (hashIndex !== -1) {
123
+ hash = href.slice(hashIndex);
124
+ basePath = href.slice(0, hashIndex);
125
+ }
126
+
127
+ // Extract existing query params
128
+ const queryIndex = basePath.indexOf("?");
129
+ if (queryIndex !== -1) {
130
+ existingParams = new URLSearchParams(basePath.slice(queryIndex + 1));
131
+ basePath = basePath.slice(0, queryIndex);
132
+ }
133
+
134
+ // Merge params (new params overwrite existing ones with same key)
135
+ const mergedParams = new URLSearchParams(existingParams);
136
+
137
+ // Filter params by keys if provided
138
+ const paramsToAdd = keys
139
+ ? [...params.entries()].filter(([key]) => keys.includes(key))
140
+ : [...params.entries()];
141
+
142
+ for (const [key, value] of paramsToAdd) {
143
+ mergedParams.set(key, value);
144
+ }
145
+
146
+ // Build the final URL
147
+ const queryString = mergedParams.toString();
148
+ if (!queryString) {
149
+ return href;
150
+ }
151
+
152
+ return `${basePath}?${queryString}${hash}`;
153
+ }
@@ -7,7 +7,7 @@ export async function reportVitals() {
7
7
  }
8
8
 
9
9
  const { onLCP, onINP, onCLS } = await import("web-vitals");
10
- Logger.log("Reporting vitals");
10
+ Logger.debug("Reporting vitals");
11
11
  const logMetric = (metric: {
12
12
  name: string;
13
13
  value: number;
@@ -1 +0,0 @@
1
- import{t as e}from"./worker-BrDpRi2I.js";var t=e(((e,t)=>{t.exports={}}));export default t();