spaday-regular-table 0.2.2__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 (46) hide show
  1. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/PKG-INFO +11 -4
  2. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/README.md +9 -2
  3. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/build.mjs +17 -0
  4. spaday_regular_table-0.3.0/js/examples/build_pyodide_example.py +48 -0
  5. spaday_regular_table-0.3.0/js/examples/pyodide-worker.js +100 -0
  6. spaday_regular_table-0.3.0/js/examples/pyodide.html +151 -0
  7. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/package.json +3 -3
  8. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/playwright.config.js +12 -6
  9. spaday_regular_table-0.3.0/js/src/ts/define-guard.ts +42 -0
  10. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/src/ts/index.ts +2 -1
  11. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/tests/index.spec.js +20 -0
  12. spaday_regular_table-0.3.0/js/tests/pyodide.spec.js +29 -0
  13. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/pyproject.toml +3 -3
  14. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/spaday_regular_table/__init__.py +14 -10
  15. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/spaday_regular_table/example.py +3 -1
  16. spaday_regular_table-0.3.0/spaday_regular_table/extension/cdn/index.js +2 -0
  17. spaday_regular_table-0.3.0/spaday_regular_table/extension/cdn/index.js.map +7 -0
  18. spaday_regular_table-0.3.0/spaday_regular_table/extension/versions.json +3 -0
  19. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/spaday_regular_table/tests/test_all.py +13 -1
  20. spaday_regular_table-0.3.0/spaday_regular_table/tests/test_versions.py +18 -0
  21. spaday_regular_table-0.2.2/js/src/ts/define-guard.ts +0 -19
  22. spaday_regular_table-0.2.2/spaday_regular_table/extension/cdn/index.js +0 -2
  23. spaday_regular_table-0.2.2/spaday_regular_table/extension/cdn/index.js.map +0 -7
  24. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/.gitignore +0 -0
  25. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/LICENSE +0 -0
  26. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/.oxfmtrc.json +0 -0
  27. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/.oxlintrc.json +0 -0
  28. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/pnpm-lock.yaml +0 -0
  29. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/pnpm-workspace.yaml +0 -0
  30. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/src/css/index.css +0 -0
  31. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/src/css/theme.css +0 -0
  32. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/src/html/index.html +0 -0
  33. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/src/ts/css.d.ts +0 -0
  34. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/tools/bundle.mjs +0 -0
  35. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/tools/css.mjs +0 -0
  36. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/tools/externals.mjs +0 -0
  37. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/tools/getarg.mjs +0 -0
  38. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/tools/html.mjs +0 -0
  39. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/js/tsconfig.json +0 -0
  40. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/spaday_regular_table/components.cem.json +0 -0
  41. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/spaday_regular_table/components.py +0 -0
  42. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/spaday_regular_table/extension/css/index.css +0 -0
  43. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/spaday_regular_table/extension/css/material.css +0 -0
  44. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/spaday_regular_table/extension/css/theme.css +0 -0
  45. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/spaday_regular_table/extension/index.html +0 -0
  46. {spaday_regular_table-0.2.2 → spaday_regular_table-0.3.0}/spaday_regular_table/tests/test_example.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: spaday-regular-table
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: regular-table for spaday
5
5
  Project-URL: Repository, https://github.com/1kbgz/spaday-regular-table
6
6
  Project-URL: Homepage, https://github.com/1kbgz/spaday-regular-table
@@ -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'
@@ -44,8 +44,8 @@ Description-Content-Type: text/markdown
44
44
 
45
45
  <a href="https://github.com/1kbgz/spaday-regular-table">
46
46
  <picture>
47
- <source media="(prefers-color-scheme: dark)" srcset="https://github.com/1kbgz/spaday-regular-table/raw/main/docs/img/logo-dark.png?raw=true">
48
- <img alt="spaday-regular-table logo, a table with a data-grid top inside a browser window" src="https://github.com/1kbgz/spaday-regular-table/raw/main/docs/img/logo-light.png?raw=true" width="800">
47
+ <source media="(prefers-color-scheme: dark)" srcset="https://github.com/1kbgz/spaday-regular-table/raw/main/docs/img/logo-dark.webp?raw=true">
48
+ <img alt="spaday-regular-table logo, a table with a data-grid top inside a browser window" src="https://github.com/1kbgz/spaday-regular-table/raw/main/docs/img/logo-light.webp?raw=true" width="800">
49
49
  </picture>
50
50
  </a>
51
51
 
@@ -56,6 +56,8 @@ Viewport-virtualized data tables for spaday, powered by `regular-table`.
56
56
  [![License](https://img.shields.io/github/license/1kbgz/spaday-regular-table)](https://github.com/1kbgz/spaday-regular-table)
57
57
  [![PyPI](https://img.shields.io/pypi/v/spaday-regular-table.svg)](https://pypi.python.org/pypi/spaday-regular-table)
58
58
 
59
+ [![Preview of regular-table page](https://raw.githubusercontent.com/1kbgz/spaday-regular-table/main/docs/img/preview.webp)](./spaday_regular_table/example.py)
60
+
59
61
  ## Documentation
60
62
 
61
63
  - [Build a 100,000-row table](docs/src/tutorial.md) — guided virtual-table example.
@@ -104,6 +106,8 @@ browser methods remain available to integration authors, but applications need n
104
106
 
105
107
  ## Run the local example
106
108
 
109
+ [Run the complete example in your browser](https://1kbgz.github.io/spaday-regular-table/lite/) — Python runs locally through Pyodide; no install or server required.
110
+
107
111
  ```bash
108
112
  python -m pip install -e ".[examples]"
109
113
  python -m spaday_regular_table.example
@@ -114,3 +118,6 @@ virtualized rows, a server row stream, full-width sizing, rich cells, and server
114
118
  insert, remove, and click operations. All interaction is authored in Python; there is no companion
115
119
  `example.js`. It passes the local package descriptor directly, so it does not install or resolve the
116
120
  integration from GitHub.
121
+
122
+ > [!NOTE]
123
+ > 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-table">
2
2
  <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="https://github.com/1kbgz/spaday-regular-table/raw/main/docs/img/logo-dark.png?raw=true">
4
- <img alt="spaday-regular-table logo, a table with a data-grid top inside a browser window" src="https://github.com/1kbgz/spaday-regular-table/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-table/raw/main/docs/img/logo-dark.webp?raw=true">
4
+ <img alt="spaday-regular-table logo, a table with a data-grid top inside a browser window" src="https://github.com/1kbgz/spaday-regular-table/raw/main/docs/img/logo-light.webp?raw=true" width="800">
5
5
  </picture>
6
6
  </a>
7
7
 
@@ -12,6 +12,8 @@ Viewport-virtualized data tables for spaday, powered by `regular-table`.
12
12
  [![License](https://img.shields.io/github/license/1kbgz/spaday-regular-table)](https://github.com/1kbgz/spaday-regular-table)
13
13
  [![PyPI](https://img.shields.io/pypi/v/spaday-regular-table.svg)](https://pypi.python.org/pypi/spaday-regular-table)
14
14
 
15
+ [![Preview of regular-table page](https://raw.githubusercontent.com/1kbgz/spaday-regular-table/main/docs/img/preview.webp)](./spaday_regular_table/example.py)
16
+
15
17
  ## Documentation
16
18
 
17
19
  - [Build a 100,000-row table](docs/src/tutorial.md) — guided virtual-table example.
@@ -60,6 +62,8 @@ browser methods remain available to integration authors, but applications need n
60
62
 
61
63
  ## Run the local example
62
64
 
65
+ [Run the complete example in your browser](https://1kbgz.github.io/spaday-regular-table/lite/) — Python runs locally through Pyodide; no install or server required.
66
+
63
67
  ```bash
64
68
  python -m pip install -e ".[examples]"
65
69
  python -m spaday_regular_table.example
@@ -70,3 +74,6 @@ virtualized rows, a server row stream, full-width sizing, rich cells, and server
70
74
  insert, remove, and click operations. All interaction is authored in Python; there is no companion
71
75
  `example.js`. It passes the local package descriptor directly, so it does not install or resolve the
72
76
  integration from GitHub.
77
+
78
+ > [!NOTE]
79
+ > 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).
@@ -42,6 +42,23 @@ async function build() {
42
42
 
43
43
  await Promise.all(BUNDLES.map(bundle)).catch(() => process.exit(1));
44
44
 
45
+ // the exact version of every library this package serves, read by the Python package as its
46
+ // ComponentPackage.provides, so spaday can reconcile it with the other packages on a page
47
+ const { dependencies = {} } = JSON.parse(
48
+ fs.readFileSync("package.json", "utf8"),
49
+ );
50
+ const served = Object.fromEntries(
51
+ Object.keys(dependencies).map((name) => [
52
+ name,
53
+ JSON.parse(fs.readFileSync(`node_modules/${name}/package.json`, "utf8"))
54
+ .version,
55
+ ]),
56
+ );
57
+ fs.writeFileSync(
58
+ "dist/versions.json",
59
+ `${JSON.stringify(served, null, 2)}\n`,
60
+ );
61
+
45
62
  // Copy servable assets to python extension (exclude esm/)
46
63
  fs.mkdirSync("../spaday_regular_table/extension", { recursive: true });
47
64
  await cpy("dist/**/*", "../spaday_regular_table/extension", {
@@ -0,0 +1,48 @@
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, table_wheel: Path, dependency_dir: Path) -> None:
37
+ spaday_wheel = one_wheel(dependency_dir, "spaday")
38
+ wheels = {
39
+ "table": copy_wheel(table_wheel, output),
40
+ "spaday": copy_wheel(spaday_wheel, output),
41
+ }
42
+ extract_assets(table_wheel, "spaday_regular_table", output / "components" / "regular-table")
43
+ extract_assets(spaday_wheel, "spaday", output / "runtime" / "spaday")
44
+ (output / "wheels.json").write_text(json.dumps(wheels, indent=2) + "\n")
45
+
46
+
47
+ if __name__ == "__main__":
48
+ main(*(Path(value) for value in sys.argv[1:]))
@@ -0,0 +1,100 @@
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([wheels["spaday"], "starlette", "uvicorn"])
31
+ await micropip.install(wheels["table"], deps=False)
32
+
33
+ from spaday_regular_table import example
34
+
35
+ pending_patches = []
36
+ native_publish = example.publish
37
+
38
+ def publish(*operations):
39
+ patch = native_publish(*operations)
40
+ pending_patches.append(patch)
41
+ return patch
42
+
43
+ example.publish = publish
44
+
45
+ def flush_stream():
46
+ patches = list(pending_patches)
47
+ pending_patches.clear()
48
+ return json.dumps(patches)
49
+
50
+ class LocalRequest:
51
+ def __init__(self, action, body):
52
+ self.path_params = {"action": action}
53
+ self._body = body
54
+
55
+ async def json(self):
56
+ return self._body
57
+
58
+ async def call_endpoint(action, body_json):
59
+ response = await example.table_action(LocalRequest(action, json.loads(body_json)))
60
+ return json.dumps({
61
+ "status": response.status_code,
62
+ "body": json.loads(bytes(response.body).decode()),
63
+ })
64
+
65
+ asyncio.create_task(example.stream_rows())
66
+
67
+ json.dumps({
68
+ "tree": example.page.to_node(),
69
+ "style": example.styles,
70
+ "store": example.initial_store,
71
+ })
72
+ `);
73
+ })();
74
+
75
+ let queue = Promise.resolve();
76
+
77
+ async function handle(message) {
78
+ const snapshot = await ready;
79
+ if (message.type === "start") {
80
+ self.postMessage({ type: "snapshot", payload: JSON.parse(snapshot) });
81
+ } else if (message.type === "flush") {
82
+ const patches = JSON.parse(pyodide.runPython("flush_stream()"));
83
+ if (patches.length) self.postMessage({ type: "stream", patches });
84
+ } else if (message.type === "endpoint") {
85
+ pyodide.globals.set("action", message.action);
86
+ pyodide.globals.set("body_json", JSON.stringify(message.body));
87
+ const result = JSON.parse(
88
+ await pyodide.runPythonAsync("await call_endpoint(action, body_json)"),
89
+ );
90
+ self.postMessage({ type: "endpoint", id: message.id, ...result });
91
+ }
92
+ }
93
+
94
+ self.addEventListener("message", (event) => {
95
+ queue = queue
96
+ .then(() => handle(event.data))
97
+ .catch((error) => {
98
+ self.postMessage({ type: "error", message: String(error) });
99
+ });
100
+ });
@@ -0,0 +1,151 @@
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-table in Pyodide</title>
7
+ <link rel="stylesheet" href="./components/regular-table/css/index.css" />
8
+ <link rel="stylesheet" href="./components/regular-table/css/material.css" />
9
+ <link rel="stylesheet" href="./components/regular-table/css/theme.css" />
10
+ <style>
11
+ #pyodide-status {
12
+ position: sticky;
13
+ z-index: 10000;
14
+ top: 0;
15
+ box-sizing: border-box;
16
+ min-height: 3rem;
17
+ padding: 0.75rem 1rem;
18
+ border-bottom: 1px solid #cbd5e1;
19
+ color: #475569;
20
+ background: #fffffff2;
21
+ font:
22
+ 14px/1.4 system-ui,
23
+ sans-serif;
24
+ backdrop-filter: blur(12px);
25
+ }
26
+ #pyodide-error {
27
+ margin: 1rem;
28
+ color: #b91c1c;
29
+ white-space: pre-wrap;
30
+ }
31
+ </style>
32
+ </head>
33
+ <body>
34
+ <div id="pyodide-status">Loading Python in the browser…</div>
35
+ <div id="example-root"></div>
36
+ <pre id="pyodide-error" hidden></pre>
37
+ <script type="module">
38
+ import { init, mount, Store } from "./runtime/spaday/cdn/index.js";
39
+ import "./components/regular-table/cdn/index.js";
40
+
41
+ const status = document.querySelector("#pyodide-status");
42
+ const pendingEndpoints = new Map();
43
+ const streamSources = new Set();
44
+ let nextEndpointId = 0;
45
+
46
+ class LocalEventSource extends EventTarget {
47
+ static CLOSED = 2;
48
+ static CONNECTING = 0;
49
+ static OPEN = 1;
50
+
51
+ constructor(url) {
52
+ super();
53
+ const path = new URL(url, location.href).pathname;
54
+ if (path !== "/api/table/stream")
55
+ throw new Error(`unsupported local event stream: ${path}`);
56
+ this.readyState = LocalEventSource.CONNECTING;
57
+ this.url = new URL(url, location.href).href;
58
+ streamSources.add(this);
59
+ queueMicrotask(() => {
60
+ this.readyState = LocalEventSource.OPEN;
61
+ this.dispatchEvent(new Event("open"));
62
+ });
63
+ }
64
+
65
+ close() {
66
+ this.readyState = LocalEventSource.CLOSED;
67
+ streamSources.delete(this);
68
+ }
69
+ }
70
+
71
+ globalThis.EventSource = LocalEventSource;
72
+
73
+ function fail(error) {
74
+ status.textContent = "Unable to start";
75
+ const output = document.querySelector("#pyodide-error");
76
+ output.hidden = false;
77
+ output.textContent = String(error);
78
+ }
79
+
80
+ try {
81
+ await init({ module_or_path: "./runtime/spaday/pkg/spaday_bg.wasm" });
82
+
83
+ const worker = new Worker("./pyodide-worker.js", { type: "module" });
84
+ const nativeFetch = globalThis.fetch.bind(globalThis);
85
+ globalThis.fetch = async (input, options = {}) => {
86
+ const url = new URL(
87
+ input instanceof Request ? input.url : input,
88
+ location.href,
89
+ );
90
+ const match = url.pathname.match(
91
+ /^\/api\/table\/(click|update|insert|remove)$/,
92
+ );
93
+ if (!match) return nativeFetch(input, options);
94
+
95
+ const id = ++nextEndpointId;
96
+ const response = new Promise((resolve) => {
97
+ pendingEndpoints.set(id, resolve);
98
+ });
99
+ worker.postMessage({
100
+ type: "endpoint",
101
+ id,
102
+ action: match[1],
103
+ body: options.body ? JSON.parse(String(options.body)) : null,
104
+ });
105
+ return response;
106
+ };
107
+
108
+ worker.addEventListener("message", (event) => {
109
+ if (event.data.type === "status")
110
+ status.textContent = event.data.message;
111
+ if (event.data.type === "error") fail(event.data.message);
112
+ if (event.data.type === "stream") {
113
+ for (const patch of event.data.patches) {
114
+ const message = new MessageEvent("message", {
115
+ data: JSON.stringify(patch),
116
+ });
117
+ for (const source of streamSources) source.dispatchEvent(message);
118
+ }
119
+ }
120
+ if (event.data.type === "endpoint") {
121
+ const resolve = pendingEndpoints.get(event.data.id);
122
+ if (resolve) {
123
+ pendingEndpoints.delete(event.data.id);
124
+ resolve(
125
+ new Response(JSON.stringify(event.data.body), {
126
+ status: event.data.status,
127
+ headers: { "content-type": "application/json" },
128
+ }),
129
+ );
130
+ }
131
+ }
132
+ if (event.data.type === "snapshot") {
133
+ const { tree, style, store: initialStore } = event.data.payload;
134
+ document.head.insertAdjacentHTML("beforeend", style);
135
+ const store = new Store(initialStore);
136
+ mount(document.querySelector("#example-root"), tree, store);
137
+ window.setInterval(
138
+ () => worker.postMessage({ type: "flush" }),
139
+ 100,
140
+ );
141
+ status.textContent = "Ready · Python model running in this browser";
142
+ document.documentElement.dataset.ready = "true";
143
+ }
144
+ });
145
+ worker.postMessage({ type: "start" });
146
+ } catch (error) {
147
+ fail(error);
148
+ }
149
+ </script>
150
+ </body>
151
+ </html>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spaday-regular-table",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "regular-table for spaday",
5
5
  "repository": "git@github.com:1kbgz/spaday-regular-table.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_table/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_TABLE_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_table.example",
33
- url: "http://127.0.0.1:8014",
34
- reuseExistingServer: !process.env.CI,
35
- timeout: 120 * 1000,
36
- },
33
+ ...(pyodideOnly
34
+ ? []
35
+ : [
36
+ {
37
+ command: "python -m spaday_regular_table.example",
38
+ url: "http://127.0.0.1:8014",
39
+ reuseExistingServer: !process.env.CI,
40
+ timeout: 120 * 1000,
41
+ },
42
+ ]),
37
43
  ],
38
44
  });
@@ -0,0 +1,42 @@
1
+ // The regular-table engine registers the <regular-table> custom element as an import
2
+ // side effect, and other bundles inline their own copy of the engine (notably
3
+ // spaday-perspective, whose viewer-datagrid plugin bundles regular-table). Whichever
4
+ // bundle loads second would throw NotSupportedError from customElements.define and die
5
+ // wholesale. Make define idempotent while our engine import runs so the first
6
+ // registration of a tag wins and later duplicates are ignored instead of fatal.
7
+ const define = customElements.define.bind(customElements);
8
+ const lookup = customElements.get.bind(customElements);
9
+ // names this bundle registered itself
10
+ const ours = new Set<string>();
11
+
12
+ customElements.define = (
13
+ name: string,
14
+ ctor: CustomElementConstructor,
15
+ options?: ElementDefinitionOptions,
16
+ ) => {
17
+ if (lookup(name)) return;
18
+ define(name, ctor, options);
19
+ ours.add(name);
20
+ };
21
+
22
+ /** Put the real `define` back, and warn if another copy had already registered any of `tags`, the
23
+ * elements this bundle serves: the page keeps that copy's, which need not match the version this
24
+ * package serves and its catalog describes. `served` names that version, e.g.
25
+ * "@awesome.me/webawesome 3.12.0". A tag that is registered, but not by this bundle, is another
26
+ * copy's -- whether the library skipped it just now or defines its elements later. */
27
+ export function restoreDefine(
28
+ served: string,
29
+ tags: readonly string[] = [],
30
+ ): void {
31
+ customElements.define = define;
32
+ const taken = tags.filter((tag) => lookup(tag) && !ours.has(tag));
33
+ if (!taken.length) return;
34
+ const shown = taken
35
+ .slice(0, 3)
36
+ .map((tag) => `<${tag}>`)
37
+ .join(", ");
38
+ const more = taken.length > 3 ? ` and ${taken.length - 3} more` : "";
39
+ console.warn(
40
+ `${served}: another copy on the page already registered ${shown}${more}; the page keeps that copy's elements, which may not match this version`,
41
+ );
42
+ }
@@ -1,9 +1,10 @@
1
1
  // the guard must execute before the engine import registers its element
2
2
  import { restoreDefine } from "./define-guard";
3
+ import { version } from "regular-table/package.json";
3
4
  import "regular-table";
4
5
  import type { RegularTableElement } from "regular-table";
5
6
 
6
- restoreDefine();
7
+ restoreDefine(`regular-table ${version}`, ["regular-table"]);
7
8
 
8
9
  type Row = Record<string, unknown>;
9
10
  type CellRenderer = {
@@ -241,3 +241,23 @@ test("survives another bundle registering regular-table first", async ({
241
241
  expect(r.wrapperDefined).toBe(true);
242
242
  expect(r.defineRestored).toBe(true); // the guard did not leak past the engine import
243
243
  });
244
+
245
+ test("warns, naming what it serves, when another copy registered its elements first", async ({
246
+ page,
247
+ }) => {
248
+ // the page keeps the first registration, so the loser says which elements are not its own
249
+ const warnings = [];
250
+ page.on("console", (message) => {
251
+ if (message.type() === "warning") warnings.push(message.text());
252
+ });
253
+ await page.addInitScript(() => {
254
+ customElements.define("regular-table", class extends HTMLElement {});
255
+ });
256
+ await page.goto("/dist/index.html");
257
+ await expect
258
+ .poll(() => warnings.find((text) => text.includes("<regular-table>")))
259
+ .toMatch(/ \d+\.\d+\.\d+\S*: another copy on the page already registered /);
260
+ expect(warnings.find((text) => text.includes("<regular-table>"))).toContain(
261
+ "regular-table ",
262
+ );
263
+ });
@@ -0,0 +1,29 @@
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("Regular Table", { exact: true })).toBeVisible();
21
+ await expect(page.getByText("100,000", { exact: true })).toBeVisible();
22
+ await expect(page.locator("spaday-regular-table")).toBeVisible();
23
+
24
+ await page.getByRole("button", { name: "Update first row" }).click();
25
+ await expect(page.locator(".action-status")).toContainText("Server updated");
26
+ await expect(page.locator("#stream-status")).toHaveText(
27
+ "Applied server row patch",
28
+ );
29
+ });
@@ -13,7 +13,7 @@ authors = [
13
13
  description = "regular-table for spaday"
14
14
  readme = "README.md"
15
15
  license = { text = "Apache-2.0" }
16
- version = "0.2.2"
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"]
@@ -69,7 +69,7 @@ Repository = "https://github.com/1kbgz/spaday-regular-table"
69
69
  Homepage = "https://github.com/1kbgz/spaday-regular-table"
70
70
 
71
71
  [tool.bumpversion]
72
- current_version = "0.2.2"
72
+ current_version = "0.3.0"
73
73
  commit = true
74
74
  tag = true
75
75
  commit_args = "-s"
@@ -1,12 +1,13 @@
1
+ import json
1
2
  from pathlib import Path
2
3
  from typing import Any
3
4
 
4
- from spaday import ComponentPackage
5
+ from spaday import ComponentPackage, Token
5
6
  from spaday.component import Child
6
7
 
7
8
  from .components import SpadayRegularTable
8
9
 
9
- __version__ = "0.2.2"
10
+ __version__ = "0.3.0"
10
11
 
11
12
 
12
13
  class RegularTable(SpadayRegularTable):
@@ -39,24 +40,27 @@ class RegularTable(SpadayRegularTable):
39
40
  )
40
41
 
41
42
 
43
+ # the exact version of each JS library the package serves, written by its JS build
44
+ _VERSIONS = Path(__file__).parent / "extension" / "versions.json"
45
+
42
46
  package = ComponentPackage(
43
47
  name="regular-table",
44
48
  assets_dir=Path(__file__).parent / "extension",
45
49
  assets=(("css", "css/material.css"), ("css", "css/theme.css"), ("js", "cdn/index.js")),
46
50
  components=(RegularTable,),
51
+ provides=json.loads(_VERSIONS.read_text(encoding="utf-8")) if _VERSIONS.exists() else {},
47
52
  )
48
53
 
49
54
 
50
- #: ``css()`` kwarg → (CSS custom property, what it controls), in the shape of
51
- #: :data:`spaday.theme.SHELL_TOKENS`. Each defaults to the shell token it belongs to, so re-theming
55
+ #: ``css()`` kwarg → (CSS custom property, what it controls). Each defaults to the shell token it belongs to, so re-theming
52
56
  #: the shell carries the table with it; set these to theme the table alone.
53
57
  TOKENS = {
54
- "spa_regular_table_text": ("--spa-regular-table-text", "cell text color (defaults to --spa-muted)"),
55
- "spa_regular_table_border": ("--spa-regular-table-border", "header rule under the last header row (defaults to --spa-border)"),
56
- "spa_regular_table_row_hover": ("--spa-regular-table-row-hover", "hovered row background (defaults to --spa-surface-2)"),
57
- "spa_regular_table_row_hover_text": ("--spa-regular-table-row-hover-text", "hovered row text color"),
58
- "spa_regular_table_scrollbar": ("--spa-regular-table-scrollbar", "scrollbar thumb color"),
59
- "spa_regular_table_scrollbar_hover": ("--spa-regular-table-scrollbar-hover", "scrollbar thumb color while hovered"),
58
+ "spa_regular_table_text": Token("--spa-regular-table-text", "cell text color", fallback="--spa-muted"),
59
+ "spa_regular_table_border": Token("--spa-regular-table-border", "header rule under the last header row", fallback="--spa-border"),
60
+ "spa_regular_table_row_hover": Token("--spa-regular-table-row-hover", "hovered row background", fallback="--spa-surface-2"),
61
+ "spa_regular_table_row_hover_text": Token("--spa-regular-table-row-hover-text", "hovered row text color"),
62
+ "spa_regular_table_scrollbar": Token("--spa-regular-table-scrollbar", "scrollbar thumb color"),
63
+ "spa_regular_table_scrollbar_hover": Token("--spa-regular-table-scrollbar-hover", "scrollbar thumb color while hovered"),
60
64
  }
61
65
 
62
66
  __all__ = ["TOKENS", "RegularTable", "package"]