metaframe-widget 0.3.6__tar.gz → 0.3.7__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metaframe-widget
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: anywidget for embedding metaframe URLs in Jupyter and marimo notebooks
5
5
  Project-URL: Homepage, https://github.com/metapages/metaframe-js
6
6
  Project-URL: Repository, https://github.com/metapages/metaframe-js
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
29
29
 
30
30
  # metaframe-widget
31
31
 
32
- An [anywidget](https://anywidget.dev/) for embedding [metaframe](https://js.mtfm.io/docs) and [metapage](https://docs.metapage.io/) URLs in Jupyter and marimo notebooks.
32
+ An [anywidget](https://anywidget.dev/) for embedding [metaframe](https://framejs.io/docs) and [metapage](https://docs.metapage.io/) URLs in Jupyter and marimo notebooks.
33
33
 
34
34
  ## Install
35
35
 
@@ -51,7 +51,7 @@ pip install "metaframe-widget[marimo]" # includes marimo
51
51
  ```python
52
52
  from metaframe_widget import MetaframeWidget
53
53
 
54
- w = MetaframeWidget(url="https://js.mtfm.io/#?js=...", height="300px")
54
+ w = MetaframeWidget(url="https://framejs.io/#?js=...", height="300px")
55
55
  w
56
56
  ```
57
57
 
@@ -61,7 +61,7 @@ w
61
61
  import marimo as mo
62
62
  from metaframe_widget import MetaframeWidget
63
63
 
64
- w = MetaframeWidget(url="https://js.mtfm.io/#?js=...", height="300px")
64
+ w = MetaframeWidget(url="https://framejs.io/#?js=...", height="300px")
65
65
  mo.ui.anywidget(w)
66
66
  ```
67
67
 
@@ -1,6 +1,6 @@
1
1
  # metaframe-widget
2
2
 
3
- An [anywidget](https://anywidget.dev/) for embedding [metaframe](https://js.mtfm.io/docs) and [metapage](https://docs.metapage.io/) URLs in Jupyter and marimo notebooks.
3
+ An [anywidget](https://anywidget.dev/) for embedding [metaframe](https://framejs.io/docs) and [metapage](https://docs.metapage.io/) URLs in Jupyter and marimo notebooks.
4
4
 
5
5
  ## Install
6
6
 
@@ -22,7 +22,7 @@ pip install "metaframe-widget[marimo]" # includes marimo
22
22
  ```python
23
23
  from metaframe_widget import MetaframeWidget
24
24
 
25
- w = MetaframeWidget(url="https://js.mtfm.io/#?js=...", height="300px")
25
+ w = MetaframeWidget(url="https://framejs.io/#?js=...", height="300px")
26
26
  w
27
27
  ```
28
28
 
@@ -32,7 +32,7 @@ w
32
32
  import marimo as mo
33
33
  from metaframe_widget import MetaframeWidget
34
34
 
35
- w = MetaframeWidget(url="https://js.mtfm.io/#?js=...", height="300px")
35
+ w = MetaframeWidget(url="https://framejs.io/#?js=...", height="300px")
36
36
  mo.ui.anywidget(w)
37
37
  ```
38
38
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "metaframe-widget"
7
- version = "0.3.6"
7
+ version = "0.3.7"
8
8
  description = "anywidget for embedding metaframe URLs in Jupyter and marimo notebooks"
9
9
  license = "MIT"
10
10
  requires-python = ">=3.8"
@@ -0,0 +1,177 @@
1
+ ESM = """
2
+ const CDN_URL = "https://cdn.jsdelivr.net/npm/@metapages/metapage@1.10.8/+esm";
3
+
4
+ let _renderMetapage = null;
5
+
6
+ async function loadRenderMetapage() {
7
+ if (!_renderMetapage) {
8
+ const mod = await import(CDN_URL);
9
+ _renderMetapage = mod.renderMetapage;
10
+ }
11
+ return _renderMetapage;
12
+ }
13
+
14
+ export default {
15
+ async render({ model, el }) {
16
+ // Lay out el as a column: the metapage fills the space, an optional
17
+ // "saved URL" footer sits beneath it (only when there is one to show).
18
+ el.style.display = "flex";
19
+ el.style.flexDirection = "column";
20
+ el.style.boxSizing = "border-box";
21
+ el.style.width = model.get("width") || "100%";
22
+ el.style.height = model.get("height") || "400px";
23
+
24
+ // Inject CSS to remove iframe borders and inline gaps
25
+ const style = document.createElement("style");
26
+ style.textContent = ".metaframe-widget-container { box-sizing: border-box; flex: 1 1 auto; min-height: 0; width: 100%; } .metaframe-widget-container iframe { border: none; display: block; margin: 0; padding: 0; box-sizing: border-box; width: 100%; height: 100%; }";
27
+ el.appendChild(style);
28
+
29
+ const container = document.createElement("div");
30
+ container.className = "metaframe-widget-container";
31
+ el.appendChild(container);
32
+
33
+ // Footer showing the latest short URL produced by editing + saving.
34
+ const footer = document.createElement("div");
35
+ footer.style.cssText = "flex: 0 0 auto; display: none; align-items: center; gap: 6px; padding: 4px 6px; font-family: sans-serif; font-size: 12px; color: #555; background: #f7f7f7; border-top: 1px solid #e0e0e0; overflow: hidden;";
36
+ const footerLabel = document.createElement("span");
37
+ footerLabel.textContent = "Saved:";
38
+ footerLabel.style.flex = "0 0 auto";
39
+ const footerLink = document.createElement("a");
40
+ footerLink.target = "_blank";
41
+ footerLink.rel = "noopener noreferrer";
42
+ footerLink.style.cssText = "flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;";
43
+ const footerCopy = document.createElement("button");
44
+ footerCopy.textContent = "copy";
45
+ footerCopy.style.cssText = "flex: 0 0 auto; cursor: pointer; font-size: 11px; padding: 1px 6px;";
46
+ footer.appendChild(footerLabel);
47
+ footer.appendChild(footerLink);
48
+ footer.appendChild(footerCopy);
49
+ el.appendChild(footer);
50
+
51
+ function renderSavedUrl() {
52
+ const savedUrl = model.get("saved_url") || "";
53
+ if (savedUrl) {
54
+ footerLink.textContent = savedUrl;
55
+ footerLink.href = savedUrl;
56
+ footer.style.display = "flex";
57
+ } else {
58
+ footer.style.display = "none";
59
+ }
60
+ }
61
+ footerCopy.addEventListener("click", () => {
62
+ const savedUrl = model.get("saved_url") || "";
63
+ if (savedUrl && navigator.clipboard) {
64
+ navigator.clipboard.writeText(savedUrl);
65
+ }
66
+ });
67
+ renderSavedUrl();
68
+
69
+ const renderMetapage = await loadRenderMetapage();
70
+
71
+ let currentResult = null;
72
+
73
+ function applyAllowToIframes() {
74
+ const allow = model.get("allow") || "";
75
+ if (allow) {
76
+ container.querySelectorAll("iframe").forEach(iframe => {
77
+ iframe.allow = allow;
78
+ });
79
+ }
80
+ }
81
+
82
+ async function createMetapage() {
83
+ if (currentResult) {
84
+ currentResult.dispose();
85
+ currentResult = null;
86
+ }
87
+ container.innerHTML = "";
88
+
89
+ const url = model.get("url");
90
+ if (!url) return;
91
+
92
+ const definition = {
93
+ version: "0.3",
94
+ metaframes: {
95
+ mf: { url },
96
+ },
97
+ };
98
+
99
+ const result = await renderMetapage({
100
+ definition,
101
+ rootDiv: container,
102
+ onOutputs: (outputs) => {
103
+ const mfOutputs = outputs.mf || {};
104
+ model.set("outputs", JSON.parse(JSON.stringify(mfOutputs)));
105
+ model.save_changes();
106
+ },
107
+ });
108
+ currentResult = result;
109
+
110
+ applyAllowToIframes();
111
+
112
+ // Push current inputs if any
113
+ const inputs = model.get("inputs");
114
+ if (inputs && Object.keys(inputs).length > 0) {
115
+ result.setInputs({ mf: inputs });
116
+ }
117
+ }
118
+
119
+ // The embedded editor (same-origin to the URL-shortening worker) mints a
120
+ // short URL when the user clicks "Save and Shorten URL", then postMessages
121
+ // it here. We record it in `saved_url` WITHOUT touching `url`, so the live
122
+ // iframe the user is editing is never torn down and reloaded.
123
+ function expectedOrigin() {
124
+ try {
125
+ return new URL(model.get("url") || "").origin;
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ const onMessage = (event) => {
131
+ const data = event.data;
132
+ if (!data || data.type !== "metaframe-widget:shorturl" || !data.url) {
133
+ return;
134
+ }
135
+ // Route to the correct widget (one notebook may hold several) and
136
+ // verify the message came from our metaframe's origin.
137
+ const iframe = container.querySelector("iframe");
138
+ if (!iframe || event.source !== iframe.contentWindow) return;
139
+ const origin = expectedOrigin();
140
+ if (origin && event.origin !== origin) return;
141
+
142
+ model.set("saved_url", data.url);
143
+ model.save_changes();
144
+ };
145
+ window.addEventListener("message", onMessage);
146
+
147
+ model.on("change:url", createMetapage);
148
+ model.on("change:saved_url", renderSavedUrl);
149
+
150
+ model.on("change:inputs", () => {
151
+ if (currentResult) {
152
+ const inputs = model.get("inputs");
153
+ currentResult.setInputs({ mf: inputs });
154
+ }
155
+ });
156
+
157
+ model.on("change:width", () => {
158
+ el.style.width = model.get("width") || "100%";
159
+ });
160
+
161
+ model.on("change:height", () => {
162
+ el.style.height = model.get("height") || "400px";
163
+ });
164
+
165
+ model.on("change:allow", applyAllowToIframes);
166
+
167
+ await createMetapage();
168
+
169
+ return () => {
170
+ window.removeEventListener("message", onMessage);
171
+ if (currentResult) {
172
+ currentResult.dispose();
173
+ }
174
+ };
175
+ },
176
+ };
177
+ """
@@ -26,6 +26,9 @@ class MetaframeWidget(anywidget.AnyWidget):
26
26
  width: CSS width for the widget container (default "100%").
27
27
  height: CSS height for the widget container (default "400px").
28
28
  allow: iframe allow attribute string (e.g. "camera; microphone").
29
+ saved_url: Latest short URL minted by editing+saving inside the widget
30
+ (read-only). Copy this into your cell to persist edits across
31
+ notebook re-runs.
29
32
  """
30
33
 
31
34
  _esm = ESM
@@ -36,6 +39,10 @@ class MetaframeWidget(anywidget.AnyWidget):
36
39
  width = traitlets.Unicode("100%").tag(sync=True)
37
40
  height = traitlets.Unicode("400px").tag(sync=True)
38
41
  allow = traitlets.Unicode("").tag(sync=True)
42
+ # Set by the ESM when the user clicks "Save and Shorten URL" inside the
43
+ # embedded editor. Read-only from Python's perspective; updating it does not
44
+ # reload the iframe (so editing is not interrupted).
45
+ saved_url = traitlets.Unicode("").tag(sync=True)
39
46
 
40
47
  def __init__(self, *args, **kwargs):
41
48
  super().__init__(*args, **kwargs)
@@ -67,6 +74,15 @@ class MetaframeWidget(anywidget.AnyWidget):
67
74
  """
68
75
  self.observe(callback, names=["outputs"])
69
76
 
77
+ def on_saved_url_change(self, callback):
78
+ """Register a callback for when the user saves edits to a new short URL.
79
+
80
+ Fires after the user clicks "Save and Shorten URL" inside the embedded
81
+ editor. The callback receives a change dict whose 'new' key holds the
82
+ freshly-minted short URL.
83
+ """
84
+ self.observe(callback, names=["saved_url"])
85
+
70
86
  @classmethod
71
87
  def from_code(
72
88
  cls,
@@ -78,7 +94,7 @@ class MetaframeWidget(anywidget.AnyWidget):
78
94
  ) -> "MetaframeWidget":
79
95
  """Create a MetaframeWidget from raw JavaScript code.
80
96
 
81
- Encodes the code into a js.mtfm.io URL with the code in the hash.
97
+ Encodes the code into a framejs.io URL with the code in the hash.
82
98
 
83
99
  Args:
84
100
  js_code: JavaScript source for the metaframe.
@@ -87,7 +103,7 @@ class MetaframeWidget(anywidget.AnyWidget):
87
103
  allow: iframe allow attribute string (e.g. "camera; microphone").
88
104
  """
89
105
  encoded = string_to_base64_string(js_code)
90
- url = f"https://js.mtfm.io/#?js={encoded}"
106
+ url = f"https://framejs.io/#?js={encoded}"
91
107
  return cls(url=url, width=width, height=height, allow=allow, **kwargs)
92
108
 
93
109
  def pipe_to(self, target: "MetaframeWidget", output_key: str, input_key: str = None):
@@ -5,8 +5,8 @@ from metaframe_widget import MetaframeWidget
5
5
 
6
6
 
7
7
  def test_create_widget():
8
- w = MetaframeWidget(url="https://js.mtfm.io/#?js=abc")
9
- assert w.url == "https://js.mtfm.io/#?js=abc"
8
+ w = MetaframeWidget(url="https://framejs.io/#?js=abc")
9
+ assert w.url == "https://framejs.io/#?js=abc"
10
10
  assert w.inputs == {}
11
11
  assert w.outputs == {}
12
12
  assert w.height == "400px"
@@ -34,14 +34,14 @@ def test_from_code():
34
34
  # from_code does: encodeURIComponent(code) then base64
35
35
  encoded_uri = urllib.parse.quote(code, safe="-_.!~*'()")
36
36
  encoded = base64.b64encode(encoded_uri.encode("ascii")).decode("ascii")
37
- expected_url = f"https://js.mtfm.io/#?js={encoded}"
37
+ expected_url = f"https://framejs.io/#?js={encoded}"
38
38
  assert w.url == expected_url
39
39
 
40
40
 
41
41
  def test_from_code_with_kwargs():
42
42
  w = MetaframeWidget.from_code("code", height="600px")
43
43
  assert w.height == "600px"
44
- assert "js.mtfm.io" in w.url
44
+ assert "framejs.io" in w.url
45
45
 
46
46
 
47
47
  def test_on_outputs_change():
@@ -1,111 +0,0 @@
1
- ESM = """
2
- const CDN_URL = "https://cdn.jsdelivr.net/npm/@metapages/metapage@1.10.8/+esm";
3
-
4
- let _renderMetapage = null;
5
-
6
- async function loadRenderMetapage() {
7
- if (!_renderMetapage) {
8
- const mod = await import(CDN_URL);
9
- _renderMetapage = mod.renderMetapage;
10
- }
11
- return _renderMetapage;
12
- }
13
-
14
- export default {
15
- async render({ model, el }) {
16
- // Size el explicitly so marimo and Jupyter honour width/height
17
- el.style.display = "block";
18
- el.style.boxSizing = "border-box";
19
- el.style.width = model.get("width") || "100%";
20
- el.style.height = model.get("height") || "400px";
21
-
22
- // Inject CSS to remove iframe borders and inline gaps
23
- const style = document.createElement("style");
24
- style.textContent = ".metaframe-widget-container { box-sizing: border-box; width: 100%; height: 100%; } .metaframe-widget-container iframe { border: none; display: block; margin: 0; padding: 0; box-sizing: border-box; width: 100%; height: 100%; }";
25
- el.appendChild(style);
26
-
27
- const container = document.createElement("div");
28
- container.className = "metaframe-widget-container";
29
- container.style.width = "100%";
30
- container.style.height = "100%";
31
- el.appendChild(container);
32
-
33
- const renderMetapage = await loadRenderMetapage();
34
-
35
- let currentResult = null;
36
-
37
- function applyAllowToIframes() {
38
- const allow = model.get("allow") || "";
39
- if (allow) {
40
- container.querySelectorAll("iframe").forEach(iframe => {
41
- iframe.allow = allow;
42
- });
43
- }
44
- }
45
-
46
- async function createMetapage() {
47
- if (currentResult) {
48
- currentResult.dispose();
49
- currentResult = null;
50
- }
51
- container.innerHTML = "";
52
-
53
- const url = model.get("url");
54
- if (!url) return;
55
-
56
- const definition = {
57
- version: "0.3",
58
- metaframes: {
59
- mf: { url },
60
- },
61
- };
62
-
63
- const result = await renderMetapage({
64
- definition,
65
- rootDiv: container,
66
- onOutputs: (outputs) => {
67
- const mfOutputs = outputs.mf || {};
68
- model.set("outputs", JSON.parse(JSON.stringify(mfOutputs)));
69
- model.save_changes();
70
- },
71
- });
72
- currentResult = result;
73
-
74
- applyAllowToIframes();
75
-
76
- // Push current inputs if any
77
- const inputs = model.get("inputs");
78
- if (inputs && Object.keys(inputs).length > 0) {
79
- result.setInputs({ mf: inputs });
80
- }
81
- }
82
-
83
- model.on("change:url", createMetapage);
84
-
85
- model.on("change:inputs", () => {
86
- if (currentResult) {
87
- const inputs = model.get("inputs");
88
- currentResult.setInputs({ mf: inputs });
89
- }
90
- });
91
-
92
- model.on("change:width", () => {
93
- el.style.width = model.get("width") || "100%";
94
- });
95
-
96
- model.on("change:height", () => {
97
- el.style.height = model.get("height") || "400px";
98
- });
99
-
100
- model.on("change:allow", applyAllowToIframes);
101
-
102
- await createMetapage();
103
-
104
- return () => {
105
- if (currentResult) {
106
- currentResult.dispose();
107
- }
108
- };
109
- },
110
- };
111
- """