spaday-regular-layout 0.2.7__tar.gz → 0.3.0__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.
Files changed (47) hide show
  1. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/PKG-INFO +9 -4
  2. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/README.md +7 -2
  3. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/build.mjs +17 -0
  4. spaday_regular_layout-0.3.0/js/examples/build_pyodide_example.py +50 -0
  5. spaday_regular_layout-0.3.0/js/examples/pyodide-worker.js +105 -0
  6. spaday_regular_layout-0.3.0/js/examples/pyodide.html +136 -0
  7. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/package.json +3 -3
  8. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/playwright.config.js +12 -6
  9. spaday_regular_layout-0.3.0/js/src/ts/define-guard.ts +48 -0
  10. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/src/ts/index.ts +6 -1
  11. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/tests/index.spec.js +20 -0
  12. spaday_regular_layout-0.3.0/js/tests/pyodide.spec.js +48 -0
  13. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/pyproject.toml +3 -3
  14. spaday_regular_layout-0.3.0/spaday_regular_layout/__init__.py +34 -0
  15. spaday_regular_layout-0.3.0/spaday_regular_layout/extension/cdn/index.js +21 -0
  16. spaday_regular_layout-0.3.0/spaday_regular_layout/extension/cdn/index.js.map +7 -0
  17. spaday_regular_layout-0.3.0/spaday_regular_layout/extension/versions.json +3 -0
  18. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/spaday_regular_layout/tests/test_all.py +13 -1
  19. spaday_regular_layout-0.3.0/spaday_regular_layout/tests/test_versions.py +18 -0
  20. spaday_regular_layout-0.2.7/js/src/ts/define-guard.ts +0 -25
  21. spaday_regular_layout-0.2.7/spaday_regular_layout/__init__.py +0 -30
  22. spaday_regular_layout-0.2.7/spaday_regular_layout/extension/cdn/index.js +0 -21
  23. spaday_regular_layout-0.2.7/spaday_regular_layout/extension/cdn/index.js.map +0 -7
  24. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/.gitignore +0 -0
  25. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/LICENSE +0 -0
  26. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/.oxfmtrc.json +0 -0
  27. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/.oxlintrc.json +0 -0
  28. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/pnpm-lock.yaml +0 -0
  29. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/pnpm-workspace.yaml +0 -0
  30. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/src/css/index.css +0 -0
  31. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/src/css/spa.css +0 -0
  32. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/src/html/index.html +0 -0
  33. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/src/ts/css.d.ts +0 -0
  34. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/tools/bundle.mjs +0 -0
  35. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/tools/css.mjs +0 -0
  36. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/tools/externals.mjs +0 -0
  37. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/tools/getarg.mjs +0 -0
  38. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/tools/html.mjs +0 -0
  39. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/js/tsconfig.json +0 -0
  40. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/spaday_regular_layout/components.cem.json +0 -0
  41. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/spaday_regular_layout/components.py +0 -0
  42. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/spaday_regular_layout/example.py +0 -0
  43. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/spaday_regular_layout/extension/css/index.css +0 -0
  44. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/spaday_regular_layout/extension/css/lorax.css +0 -0
  45. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/spaday_regular_layout/extension/css/spa.css +0 -0
  46. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/spaday_regular_layout/extension/index.html +0 -0
  47. {spaday_regular_layout-0.2.7 → spaday_regular_layout-0.3.0}/spaday_regular_layout/tests/test_example.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: spaday-regular-layout
3
- Version: 0.2.7
3
+ Version: 0.3.0
4
4
  Summary: regular-layout for spaday
5
5
  Project-URL: Repository, https://github.com/1kbgz/spaday-regular-layout
6
6
  Project-URL: Homepage, https://github.com/1kbgz/spaday-regular-layout
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.14
17
17
  Classifier: Programming Language :: Python :: Implementation :: CPython
18
18
  Classifier: Programming Language :: Python :: Implementation :: PyPy
19
19
  Requires-Python: >=3.11
20
- Requires-Dist: spaday<1.0,>=0.6.0
20
+ Requires-Dist: spaday<1.0,>=0.11.1
21
21
  Provides-Extra: develop
22
22
  Requires-Dist: build; extra == 'develop'
23
23
  Requires-Dist: bump-my-version; extra == 'develop'
@@ -50,8 +50,8 @@ Description-Content-Type: text/markdown
50
50
 
51
51
  <a href="https://github.com/1kbgz/spaday-regular-layout">
52
52
  <picture>
53
- <source media="(prefers-color-scheme: dark)" srcset="https://github.com/1kbgz/spaday-regular-layout/raw/main/docs/img/logo-dark.png?raw=true">
54
- <img alt="spaday-regular-layout logo, a picnic blanket and basket inside a browser window" src="https://github.com/1kbgz/spaday-regular-layout/raw/main/docs/img/logo-light.png?raw=true" width="800">
53
+ <source media="(prefers-color-scheme: dark)" srcset="https://github.com/1kbgz/spaday-regular-layout/raw/main/docs/img/logo-dark.webp?raw=true">
54
+ <img alt="spaday-regular-layout logo, a picnic blanket and basket inside a browser window" src="https://github.com/1kbgz/spaday-regular-layout/raw/main/docs/img/logo-light.webp?raw=true" width="800">
55
55
  </picture>
56
56
  </a>
57
57
 
@@ -100,6 +100,8 @@ Each frame's content area (`regular-layout-frame::part(container)`) is `display:
100
100
 
101
101
  ## Run the local example
102
102
 
103
+ [Run the complete example in your browser](https://1kbgz.github.io/spaday-regular-layout/lite/) — Python runs locally through Pyodide; no install or server required.
104
+
103
105
  ```bash
104
106
  python -m pip install -e ".[examples]"
105
107
  python -m spaday_regular_layout.example
@@ -108,3 +110,6 @@ python -m spaday_regular_layout.example
108
110
  Open `http://127.0.0.1:8013` to resize and rearrange the [complete three-panel example](spaday_regular_layout/example.py). It
109
111
  receives a new server layout every five seconds and persists browser rearrangements back to Python. It passes
110
112
  the local package descriptor directly, so it does not install or resolve the integration from GitHub.
113
+
114
+ > [!NOTE]
115
+ > This library was generated using [copier](https://copier.readthedocs.io/en/stable/) from the [Base Python Project Template repository](https://github.com/python-project-templates/base).
@@ -1,7 +1,7 @@
1
1
  <a href="https://github.com/1kbgz/spaday-regular-layout">
2
2
  <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="https://github.com/1kbgz/spaday-regular-layout/raw/main/docs/img/logo-dark.png?raw=true">
4
- <img alt="spaday-regular-layout logo, a picnic blanket and basket inside a browser window" src="https://github.com/1kbgz/spaday-regular-layout/raw/main/docs/img/logo-light.png?raw=true" width="800">
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://github.com/1kbgz/spaday-regular-layout/raw/main/docs/img/logo-dark.webp?raw=true">
4
+ <img alt="spaday-regular-layout logo, a picnic blanket and basket inside a browser window" src="https://github.com/1kbgz/spaday-regular-layout/raw/main/docs/img/logo-light.webp?raw=true" width="800">
5
5
  </picture>
6
6
  </a>
7
7
 
@@ -50,6 +50,8 @@ Each frame's content area (`regular-layout-frame::part(container)`) is `display:
50
50
 
51
51
  ## Run the local example
52
52
 
53
+ [Run the complete example in your browser](https://1kbgz.github.io/spaday-regular-layout/lite/) — Python runs locally through Pyodide; no install or server required.
54
+
53
55
  ```bash
54
56
  python -m pip install -e ".[examples]"
55
57
  python -m spaday_regular_layout.example
@@ -58,3 +60,6 @@ python -m spaday_regular_layout.example
58
60
  Open `http://127.0.0.1:8013` to resize and rearrange the [complete three-panel example](spaday_regular_layout/example.py). It
59
61
  receives a new server layout every five seconds and persists browser rearrangements back to Python. It passes
60
62
  the local package descriptor directly, so it does not install or resolve the integration from GitHub.
63
+
64
+ > [!NOTE]
65
+ > This library was generated using [copier](https://copier.readthedocs.io/en/stable/) from the [Base Python Project Template repository](https://github.com/python-project-templates/base).
@@ -44,6 +44,23 @@ async function build() {
44
44
 
45
45
  await Promise.all(BUNDLES.map(bundle)).catch(() => process.exit(1));
46
46
 
47
+ // the exact version of every library this package serves, read by the Python package as its
48
+ // ComponentPackage.provides, so spaday can reconcile it with the other packages on a page
49
+ const { dependencies = {} } = JSON.parse(
50
+ fs.readFileSync("package.json", "utf8"),
51
+ );
52
+ const served = Object.fromEntries(
53
+ Object.keys(dependencies).map((name) => [
54
+ name,
55
+ JSON.parse(fs.readFileSync(`node_modules/${name}/package.json`, "utf8"))
56
+ .version,
57
+ ]),
58
+ );
59
+ fs.writeFileSync(
60
+ "dist/versions.json",
61
+ `${JSON.stringify(served, null, 2)}\n`,
62
+ );
63
+
47
64
  // Copy servable assets to python extension (exclude esm/)
48
65
  fs.mkdirSync("../spaday_regular_layout/extension", { recursive: true });
49
66
  await cpy("dist/**/*", "../spaday_regular_layout/extension", {
@@ -0,0 +1,50 @@
1
+ """Assemble the static Pyodide example from locally built and downloaded wheels."""
2
+
3
+ import json
4
+ import shutil
5
+ import sys
6
+ import zipfile
7
+ from pathlib import Path
8
+
9
+
10
+ def copy_wheel(wheel: Path, output: Path) -> str:
11
+ target = output / "pypi" / wheel.name
12
+ target.parent.mkdir(parents=True, exist_ok=True)
13
+ shutil.copy2(wheel, target)
14
+ return f"pypi/{wheel.name}"
15
+
16
+
17
+ def extract_assets(wheel: Path, package: str, output: Path) -> None:
18
+ prefix = f"{package}/extension/"
19
+ with zipfile.ZipFile(wheel) as archive:
20
+ assets = [name for name in archive.namelist() if name.startswith(prefix) and not name.endswith("/")]
21
+ if not assets:
22
+ raise RuntimeError(f"{wheel.name} contains no browser assets")
23
+ for name in assets:
24
+ target = output / name.removeprefix(prefix)
25
+ target.parent.mkdir(parents=True, exist_ok=True)
26
+ target.write_bytes(archive.read(name))
27
+
28
+
29
+ def one_wheel(directory: Path, distribution: str) -> Path:
30
+ wheels = list(directory.glob(f"{distribution}-*.whl"))
31
+ if len(wheels) != 1:
32
+ raise RuntimeError(f"expected one {distribution} wheel in {directory}, found {len(wheels)}")
33
+ return wheels[0]
34
+
35
+
36
+ def main(output: Path, layout_wheel: Path, dependency_dir: Path) -> None:
37
+ spaday_wheel = one_wheel(dependency_dir, "spaday")
38
+ transports_wheel = one_wheel(dependency_dir, "transports")
39
+ wheels = {
40
+ "layout": copy_wheel(layout_wheel, output),
41
+ "spaday": copy_wheel(spaday_wheel, output),
42
+ "transports": copy_wheel(transports_wheel, output),
43
+ }
44
+ extract_assets(layout_wheel, "spaday_regular_layout", output / "components" / "regular-layout")
45
+ extract_assets(spaday_wheel, "spaday", output / "runtime" / "spaday")
46
+ (output / "wheels.json").write_text(json.dumps(wheels, indent=2) + "\n")
47
+
48
+
49
+ if __name__ == "__main__":
50
+ main(*(Path(value) for value in sys.argv[1:]))
@@ -0,0 +1,105 @@
1
+ const PYODIDE_VERSION = "314.0.4";
2
+ let pyodide;
3
+
4
+ const ready = (async () => {
5
+ self.postMessage({ type: "status", message: "Loading Pyodide…" });
6
+ const { loadPyodide } = await import(
7
+ `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/pyodide.mjs`
8
+ );
9
+ pyodide = await loadPyodide();
10
+ await pyodide.loadPackage(["micropip", "anyio"]);
11
+
12
+ self.postMessage({ type: "status", message: "Installing example…" });
13
+ const response = await fetch(new URL("./wheels.json", self.location.href));
14
+ if (!response.ok)
15
+ throw new Error(`wheel manifest returned ${response.status}`);
16
+ const wheels = await response.json();
17
+ const urls = Object.fromEntries(
18
+ Object.entries(wheels).map(([name, path]) => [
19
+ name,
20
+ new URL(path, self.location.href).href,
21
+ ]),
22
+ );
23
+ pyodide.globals.set("wheels_json", JSON.stringify(urls));
24
+ return pyodide.runPythonAsync(`
25
+ import asyncio
26
+ import json
27
+ import micropip
28
+
29
+ wheels = json.loads(wheels_json)
30
+ await micropip.install([
31
+ wheels["spaday"],
32
+ wheels["transports"],
33
+ "starlette",
34
+ "uvicorn",
35
+ ])
36
+ await micropip.install(wheels["layout"], deps=False)
37
+
38
+ from spaday_regular_layout import example
39
+
40
+ connection = "browser"
41
+
42
+ def local_wires(messages):
43
+ return list(messages.get(connection, []))
44
+
45
+ def receive_wire(frame):
46
+ return json.dumps(local_wires(example.server.recv(connection, frame)))
47
+
48
+ def flush_server():
49
+ return json.dumps(local_wires(example.server.flush()))
50
+
51
+ class LocalRequest:
52
+ def __init__(self, body):
53
+ self._body = body
54
+
55
+ async def json(self):
56
+ return self._body
57
+
58
+ async def call_endpoint(body_json):
59
+ response = await example.save_layout(LocalRequest(json.loads(body_json)))
60
+ return json.dumps({
61
+ "status": response.status_code,
62
+ "body": json.loads(bytes(response.body).decode()),
63
+ "wires": local_wires(example.server.flush()),
64
+ })
65
+
66
+ opening = example.server.open(connection, "json")
67
+ asyncio.create_task(example.rotate_layout())
68
+
69
+ json.dumps({
70
+ "tree": example.page.to_node(),
71
+ "style": "",
72
+ "wires": opening,
73
+ })
74
+ `);
75
+ })();
76
+
77
+ let queue = Promise.resolve();
78
+
79
+ async function handle(message) {
80
+ const snapshot = await ready;
81
+ if (message.type === "start") {
82
+ self.postMessage({ type: "snapshot", payload: JSON.parse(snapshot) });
83
+ } else if (message.type === "wire") {
84
+ pyodide.globals.set("wire_frame", message.frame);
85
+ const wires = JSON.parse(pyodide.runPython("receive_wire(wire_frame)"));
86
+ if (wires.length) self.postMessage({ type: "wires", wires });
87
+ } else if (message.type === "flush") {
88
+ const wires = JSON.parse(pyodide.runPython("flush_server()"));
89
+ if (wires.length) self.postMessage({ type: "wires", wires });
90
+ } else if (message.type === "endpoint") {
91
+ pyodide.globals.set("body_json", JSON.stringify(message.body));
92
+ const result = JSON.parse(
93
+ await pyodide.runPythonAsync("await call_endpoint(body_json)"),
94
+ );
95
+ self.postMessage({ type: "endpoint", id: message.id, ...result });
96
+ }
97
+ }
98
+
99
+ self.addEventListener("message", (event) => {
100
+ queue = queue
101
+ .then(() => handle(event.data))
102
+ .catch((error) => {
103
+ self.postMessage({ type: "error", message: String(error) });
104
+ });
105
+ });
@@ -0,0 +1,136 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>spaday-regular-layout in Pyodide</title>
7
+ <link rel="stylesheet" href="./components/regular-layout/css/index.css" />
8
+ <link rel="stylesheet" href="./components/regular-layout/css/spa.css" />
9
+ <style>
10
+ #pyodide-status {
11
+ position: sticky;
12
+ z-index: 10000;
13
+ top: 0;
14
+ box-sizing: border-box;
15
+ min-height: 3rem;
16
+ padding: 0.75rem 1rem;
17
+ border-bottom: 1px solid #cbd5e1;
18
+ color: #475569;
19
+ background: #fffffff2;
20
+ font:
21
+ 14px/1.4 system-ui,
22
+ sans-serif;
23
+ backdrop-filter: blur(12px);
24
+ }
25
+ #pyodide-error {
26
+ margin: 1rem;
27
+ color: #b91c1c;
28
+ white-space: pre-wrap;
29
+ }
30
+ </style>
31
+ </head>
32
+ <body>
33
+ <div id="pyodide-status">Loading Python in the browser…</div>
34
+ <div id="example-root"></div>
35
+ <pre id="pyodide-error" hidden></pre>
36
+ <script type="module">
37
+ import {
38
+ connectStore,
39
+ init,
40
+ mount,
41
+ Store,
42
+ } from "./runtime/spaday/cdn/index.js";
43
+ import "./components/regular-layout/cdn/index.js";
44
+ import * as transports from "./runtime/spaday/transports/cdn/index.js";
45
+
46
+ const status = document.querySelector("#pyodide-status");
47
+ const pendingEndpoints = new Map();
48
+ let nextEndpointId = 0;
49
+ let receiveWires = () => {};
50
+
51
+ function fail(error) {
52
+ status.textContent = "Unable to start";
53
+ const output = document.querySelector("#pyodide-error");
54
+ output.hidden = false;
55
+ output.textContent = String(error);
56
+ }
57
+
58
+ try {
59
+ await Promise.all([
60
+ init({ module_or_path: "./runtime/spaday/pkg/spaday_bg.wasm" }),
61
+ transports.wasm.default({
62
+ module_or_path:
63
+ "./runtime/spaday/transports/pkg/transports_bg.wasm",
64
+ }),
65
+ ]);
66
+
67
+ const worker = new Worker("./pyodide-worker.js", { type: "module" });
68
+ const nativeFetch = globalThis.fetch.bind(globalThis);
69
+ globalThis.fetch = async (input, options = {}) => {
70
+ const url = new URL(
71
+ input instanceof Request ? input.url : input,
72
+ location.href,
73
+ );
74
+ if (url.pathname !== "/api/layout")
75
+ return nativeFetch(input, options);
76
+
77
+ const id = ++nextEndpointId;
78
+ const response = new Promise((resolve) => {
79
+ pendingEndpoints.set(id, resolve);
80
+ });
81
+ worker.postMessage({
82
+ type: "endpoint",
83
+ id,
84
+ body: options.body ? JSON.parse(String(options.body)) : null,
85
+ });
86
+ return response;
87
+ };
88
+
89
+ worker.addEventListener("message", (event) => {
90
+ if (event.data.type === "status")
91
+ status.textContent = event.data.message;
92
+ if (event.data.type === "error") fail(event.data.message);
93
+ if (event.data.type === "wires") receiveWires(event.data.wires);
94
+ if (event.data.type === "endpoint") {
95
+ receiveWires(event.data.wires);
96
+ const resolve = pendingEndpoints.get(event.data.id);
97
+ if (resolve) {
98
+ pendingEndpoints.delete(event.data.id);
99
+ resolve(
100
+ new Response(JSON.stringify(event.data.body), {
101
+ status: event.data.status,
102
+ headers: { "content-type": "application/json" },
103
+ }),
104
+ );
105
+ }
106
+ }
107
+ if (event.data.type === "snapshot") {
108
+ const { tree, wires } = event.data.payload;
109
+ const store = new Store();
110
+ const client = new transports.Client();
111
+ const link = connectStore(
112
+ store,
113
+ client,
114
+ (frame) => worker.postMessage({ type: "wire", frame }),
115
+ transports,
116
+ );
117
+ receiveWires = (frames) => {
118
+ for (const frame of frames) link.receive(frame);
119
+ };
120
+ receiveWires(wires);
121
+ mount(document.querySelector("#example-root"), tree, store);
122
+ window.setInterval(
123
+ () => worker.postMessage({ type: "flush" }),
124
+ 100,
125
+ );
126
+ status.textContent = "Ready · Python model running in this browser";
127
+ document.documentElement.dataset.ready = "true";
128
+ }
129
+ });
130
+ worker.postMessage({ type: "start" });
131
+ } catch (error) {
132
+ fail(error);
133
+ }
134
+ </script>
135
+ </body>
136
+ </html>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spaday-regular-layout",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "description": "regular-layout for spaday",
5
5
  "repository": "git@github.com:1kbgz/spaday-regular-layout.git",
6
6
  "author": "1kbgz <dev@1kbgz.com>",
@@ -31,8 +31,8 @@
31
31
  "build": "npm-run-all build:prod build:types",
32
32
  "clean": "rm -rf dist playwright-report ../spaday_regular_layout/extension",
33
33
  "dev": "npm-run-all -p start watch",
34
- "lint": "oxlint . && oxfmt --check \"src/**/*.{js,ts,jsx,tsx,css}\" \"tests/**/*.{js,ts,jsx,tsx}\" \"*.mjs\" \"*.json\"",
35
- "fix": "oxlint --fix . && oxfmt --write \"src/**/*.{js,ts,jsx,tsx,css}\" \"tests/**/*.{js,ts,jsx,tsx}\" \"*.mjs\" \"*.json\"",
34
+ "lint": "oxlint . && oxfmt --check \"examples/**/*.{js,html}\" \"src/**/*.{js,ts,jsx,tsx,css}\" \"tests/**/*.{js,ts,jsx,tsx}\" \"*.mjs\" \"*.json\"",
35
+ "fix": "oxlint --fix . && oxfmt --write \"examples/**/*.{js,html}\" \"src/**/*.{js,ts,jsx,tsx,css}\" \"tests/**/*.{js,ts,jsx,tsx}\" \"*.mjs\" \"*.json\"",
36
36
  "preinstall": "npx only-allow pnpm",
37
37
  "prepack": "npm run build",
38
38
  "start": "http-server -p 3000 -o examples/",
@@ -1,5 +1,7 @@
1
1
  import { defineConfig, devices } from "@playwright/test";
2
2
 
3
+ const pyodideOnly = process.env.SPADAY_REGULAR_LAYOUT_PYODIDE_ONLY === "1";
4
+
3
5
  export default defineConfig({
4
6
  testDir: "tests",
5
7
  fullyParallel: true,
@@ -28,11 +30,15 @@ export default defineConfig({
28
30
  reuseExistingServer: !process.env.CI,
29
31
  timeout: 120 * 1000,
30
32
  },
31
- {
32
- command: "python -m spaday_regular_layout.example",
33
- url: "http://127.0.0.1:8013",
34
- reuseExistingServer: !process.env.CI,
35
- timeout: 120 * 1000,
36
- },
33
+ ...(pyodideOnly
34
+ ? []
35
+ : [
36
+ {
37
+ command: "python -m spaday_regular_layout.example",
38
+ url: "http://127.0.0.1:8013",
39
+ reuseExistingServer: !process.env.CI,
40
+ timeout: 120 * 1000,
41
+ },
42
+ ]),
37
43
  ],
38
44
  });
@@ -0,0 +1,48 @@
1
+ /** Tolerate another bundle having already registered the engine's custom elements.
2
+ *
3
+ * Perspective 5's viewer bundles the same `regular-layout` engine and registers
4
+ * `regular-layout` / `regular-layout-frame` / `regular-layout-tab` at import. Whichever
5
+ * bundle loads second would throw from `customElements.define` and die entirely —
6
+ * taking the `spaday-regular-layout` wrapper with it. Importing this module FIRST makes
7
+ * `define` idempotent (skip names that already exist); `restoreDefine()` puts the real
8
+ * one back immediately after the engine import, so the guard never leaks to other
9
+ * scripts. Both bundles pin the engine to the same exact version, so adopting the other
10
+ * copy's registration is adopting identical code.
11
+ */
12
+
13
+ const define = customElements.define.bind(customElements);
14
+ const lookup = customElements.get.bind(customElements);
15
+ // names this bundle registered itself
16
+ const ours = new Set<string>();
17
+
18
+ customElements.define = (
19
+ name: string,
20
+ ctor: CustomElementConstructor,
21
+ options?: ElementDefinitionOptions,
22
+ ) => {
23
+ if (lookup(name)) return;
24
+ define(name, ctor, options);
25
+ ours.add(name);
26
+ };
27
+
28
+ /** Put the real `define` back, and warn if another copy had already registered any of `tags`, the
29
+ * elements this bundle serves: the page keeps that copy's, which need not match the version this
30
+ * package serves and its catalog describes. `served` names that version, e.g.
31
+ * "@awesome.me/webawesome 3.12.0". A tag that is registered, but not by this bundle, is another
32
+ * copy's -- whether the library skipped it just now or defines its elements later. */
33
+ export function restoreDefine(
34
+ served: string,
35
+ tags: readonly string[] = [],
36
+ ): void {
37
+ customElements.define = define;
38
+ const taken = tags.filter((tag) => lookup(tag) && !ours.has(tag));
39
+ if (!taken.length) return;
40
+ const shown = taken
41
+ .slice(0, 3)
42
+ .map((tag) => `<${tag}>`)
43
+ .join(", ");
44
+ const more = taken.length > 3 ? ` and ${taken.length - 3} more` : "";
45
+ console.warn(
46
+ `${served}: another copy on the page already registered ${shown}${more}; the page keeps that copy's elements, which may not match this version`,
47
+ );
48
+ }
@@ -1,11 +1,16 @@
1
1
  // the guard must execute before the engine module registers its elements
2
2
  import { restoreDefine } from "./define-guard";
3
+ import { version } from "regular-layout/package.json";
3
4
  import {
4
5
  RegularLayout as BaseRegularLayout,
5
6
  type Layout,
6
7
  } from "regular-layout";
7
8
 
8
- restoreDefine();
9
+ restoreDefine(`regular-layout ${version}`, [
10
+ "regular-layout",
11
+ "regular-layout-frame",
12
+ "regular-layout-tab",
13
+ ]);
9
14
 
10
15
  /** A regular-layout whose saved layout tree can be assigned as a spaday prop. */
11
16
  export class SpadayRegularLayout extends BaseRegularLayout {
@@ -350,3 +350,23 @@ test("openPanel inserts a missing panel and selects an existing one", async ({
350
350
  expect([...r.afterSelect.tabs].sort()).toEqual(["extra", "home"]); // no duplicate insert
351
351
  expect(r.afterSelect.tabs[r.afterSelect.selected]).toBe("home"); // selected only
352
352
  });
353
+
354
+ test("warns, naming what it serves, when another copy registered its elements first", async ({
355
+ page,
356
+ }) => {
357
+ // the page keeps the first registration, so the loser says which elements are not its own
358
+ const warnings = [];
359
+ page.on("console", (message) => {
360
+ if (message.type() === "warning") warnings.push(message.text());
361
+ });
362
+ await page.addInitScript(() => {
363
+ customElements.define("regular-layout", class extends HTMLElement {});
364
+ });
365
+ await page.goto("/dist/index.html");
366
+ await expect
367
+ .poll(() => warnings.find((text) => text.includes("<regular-layout>")))
368
+ .toMatch(/ \d+\.\d+\.\d+\S*: another copy on the page already registered /);
369
+ expect(warnings.find((text) => text.includes("<regular-layout>"))).toContain(
370
+ "regular-layout ",
371
+ );
372
+ });
@@ -0,0 +1,48 @@
1
+ import fs from "fs";
2
+ import { expect, test } from "@playwright/test";
3
+
4
+ const built = fs.existsSync("dist/lite/index.html");
5
+
6
+ test("runs the complete example in Pyodide", async ({ page }) => {
7
+ test.skip(!built, "run `make pyodide-example` first");
8
+ test.setTimeout(180_000);
9
+
10
+ await page.goto("/dist/lite/index.html");
11
+ await page.waitForFunction(
12
+ () =>
13
+ document.documentElement.dataset.ready === "true" ||
14
+ document.querySelector("#pyodide-status")?.textContent ===
15
+ "Unable to start",
16
+ undefined,
17
+ { timeout: 150_000 },
18
+ );
19
+ await expect(page.locator("html")).toHaveAttribute("data-ready", "true");
20
+ await expect(page.getByText("Navigation", { exact: true })).toBeVisible();
21
+ await expect(page.getByText("Workspace", { exact: true })).toBeVisible();
22
+ await expect(page.getByText("Activity", { exact: true })).toBeVisible();
23
+ const layout = page.locator("spaday-regular-layout");
24
+ await expect(layout).toBeVisible();
25
+
26
+ const savedLayout = {
27
+ type: "tab-layout",
28
+ tabs: ["workspace", "navigation", "activity"],
29
+ selected: 0,
30
+ };
31
+ const response = await page.evaluate(async (body) => {
32
+ const result = await fetch("/api/layout", {
33
+ method: "POST",
34
+ body: JSON.stringify(body),
35
+ });
36
+ return result.json();
37
+ }, savedLayout);
38
+ expect(response).toEqual({ saved: true });
39
+ await expect
40
+ .poll(() => layout.evaluate((element) => element.layout))
41
+ .toEqual(savedLayout);
42
+
43
+ await expect
44
+ .poll(() => layout.evaluate((element) => element.layout?.orientation), {
45
+ timeout: 10_000,
46
+ })
47
+ .toBe("vertical");
48
+ });
@@ -13,7 +13,7 @@ authors = [
13
13
  description = "regular-layout for spaday"
14
14
  readme = "README.md"
15
15
  license = { text = "Apache-2.0" }
16
- version = "0.2.7"
16
+ version = "0.3.0"
17
17
  requires-python = ">=3.11"
18
18
  keywords = []
19
19
 
@@ -30,7 +30,7 @@ classifiers = [
30
30
  ]
31
31
 
32
32
  dependencies = [
33
- "spaday>=0.6.0,<1.0",
33
+ "spaday>=0.11.1,<1.0",
34
34
  ]
35
35
 
36
36
  [project.entry-points."spaday.component_packages"]
@@ -75,7 +75,7 @@ Repository = "https://github.com/1kbgz/spaday-regular-layout"
75
75
  Homepage = "https://github.com/1kbgz/spaday-regular-layout"
76
76
 
77
77
  [tool.bumpversion]
78
- current_version = "0.2.7"
78
+ current_version = "0.3.0"
79
79
  commit = true
80
80
  tag = true
81
81
  commit_args = "-s"
@@ -0,0 +1,34 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from spaday import ComponentPackage, Token
5
+
6
+ from .components import RegularLayoutFrame, SpadayRegularLayout
7
+
8
+ __version__ = "0.3.0"
9
+
10
+ # the exact version of each JS library the package serves, written by its JS build
11
+ _VERSIONS = Path(__file__).parent / "extension" / "versions.json"
12
+
13
+ package = ComponentPackage(
14
+ name="regular-layout",
15
+ assets_dir=Path(__file__).parent / "extension",
16
+ assets=(("css", "css/lorax.css"), ("css", "css/spa.css"), ("js", "cdn/index.js")),
17
+ components=(SpadayRegularLayout, RegularLayoutFrame),
18
+ provides=json.loads(_VERSIONS.read_text(encoding="utf-8")) if _VERSIONS.exists() else {},
19
+ )
20
+
21
+ RegularLayout = SpadayRegularLayout
22
+
23
+ #: ``css()`` kwarg → (CSS custom property, what it controls). Applies to layouts wearing the ``spa`` theme
24
+ #: (``classes("spa")``); each token defaults to the shell token it belongs to, so re-theming the
25
+ #: shell carries the panel chrome with it. The ``--spa-rl-*`` spelling still works as an alias.
26
+ TOKENS = {
27
+ "spa_regular_layout_surface": Token("--spa-regular-layout-surface", "panel / frame background", fallback="--spa-surface"),
28
+ "spa_regular_layout_surface_2": Token("--spa-regular-layout-surface-2", "titlebar / tab-strip background", fallback="--spa-surface-2"),
29
+ "spa_regular_layout_border": Token("--spa-regular-layout-border", "panel and tab borders", fallback="--spa-border"),
30
+ "spa_regular_layout_muted": Token("--spa-regular-layout-muted", "inactive tab / titlebar text", fallback="--spa-muted"),
31
+ "spa_regular_layout_shadow": Token("--spa-regular-layout-shadow", "panel drop shadow color"),
32
+ }
33
+
34
+ __all__ = ["TOKENS", "RegularLayout", "RegularLayoutFrame", "package"]