manywidgets 0.1.0__py3-none-any.whl
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.
- manywidgets/__init__.py +49 -0
- manywidgets/__main__.py +107 -0
- manywidgets/_base.py +73 -0
- manywidgets/_version.py +1 -0
- manywidgets/binder/__init__.py +3 -0
- manywidgets/binder/dist/widget.js +171 -0
- manywidgets/binder/doc.md +38 -0
- manywidgets/binder/src/index.ts +81 -0
- manywidgets/binder/widget.py +81 -0
- manywidgets/button/__init__.py +3 -0
- manywidgets/button/dist/widget.js +31 -0
- manywidgets/button/doc.md +46 -0
- manywidgets/button/src/index.ts +27 -0
- manywidgets/button/style.css +22 -0
- manywidgets/button/widget.py +46 -0
- manywidgets/chart/__init__.py +3 -0
- manywidgets/chart/dist/widget.js +14663 -0
- manywidgets/chart/doc.md +50 -0
- manywidgets/chart/src/index.ts +201 -0
- manywidgets/chart/style.css +10 -0
- manywidgets/chart/widget.py +126 -0
- manywidgets/column/__init__.py +3 -0
- manywidgets/column/dist/widget.js +72 -0
- manywidgets/column/doc.md +31 -0
- manywidgets/column/src/index.ts +50 -0
- manywidgets/column/style.css +7 -0
- manywidgets/column/widget.py +35 -0
- manywidgets/dropdown/__init__.py +3 -0
- manywidgets/dropdown/dist/widget.js +60 -0
- manywidgets/dropdown/doc.md +36 -0
- manywidgets/dropdown/src/index.ts +72 -0
- manywidgets/dropdown/style.css +28 -0
- manywidgets/dropdown/widget.py +41 -0
- manywidgets/grid/__init__.py +3 -0
- manywidgets/grid/dist/widget.js +72 -0
- manywidgets/grid/doc.md +35 -0
- manywidgets/grid/src/index.ts +50 -0
- manywidgets/grid/style.css +7 -0
- manywidgets/grid/widget.py +33 -0
- manywidgets/legend/__init__.py +3 -0
- manywidgets/legend/dist/widget.js +55 -0
- manywidgets/legend/doc.md +32 -0
- manywidgets/legend/src/index.ts +62 -0
- manywidgets/legend/style.css +46 -0
- manywidgets/legend/widget.py +42 -0
- manywidgets/lonboard/__init__.py +24 -0
- manywidgets/lonboard/conftest.py +20 -0
- manywidgets/lonboard/filter_binder/__init__.py +3 -0
- manywidgets/lonboard/filter_binder/dist/widget.js +177 -0
- manywidgets/lonboard/filter_binder/doc.md +52 -0
- manywidgets/lonboard/filter_binder/src/index.ts +77 -0
- manywidgets/lonboard/filter_binder/widget.py +72 -0
- manywidgets/lonboard/layer_filter/__init__.py +3 -0
- manywidgets/lonboard/layer_filter/dist/widget.js +190 -0
- manywidgets/lonboard/layer_filter/doc.md +41 -0
- manywidgets/lonboard/layer_filter/src/index.ts +94 -0
- manywidgets/lonboard/layer_filter/style.css +37 -0
- manywidgets/lonboard/layer_filter/widget.py +43 -0
- manywidgets/lonboard/layer_toggle/__init__.py +3 -0
- manywidgets/lonboard/layer_toggle/dist/widget.js +177 -0
- manywidgets/lonboard/layer_toggle/doc.md +36 -0
- manywidgets/lonboard/layer_toggle/src/index.ts +78 -0
- manywidgets/lonboard/layer_toggle/style.css +54 -0
- manywidgets/lonboard/layer_toggle/widget.py +31 -0
- manywidgets/lonboard/map_compare/__init__.py +3 -0
- manywidgets/lonboard/map_compare/dist/widget.js +317 -0
- manywidgets/lonboard/map_compare/doc.md +65 -0
- manywidgets/lonboard/map_compare/src/index.ts +243 -0
- manywidgets/lonboard/map_compare/style.css +99 -0
- manywidgets/lonboard/map_compare/widget.py +111 -0
- manywidgets/lonboard/map_flyer/__init__.py +3 -0
- manywidgets/lonboard/map_flyer/dist/widget.js +177 -0
- manywidgets/lonboard/map_flyer/doc.md +43 -0
- manywidgets/lonboard/map_flyer/src/index.ts +78 -0
- manywidgets/lonboard/map_flyer/style.css +41 -0
- manywidgets/lonboard/map_flyer/widget.py +47 -0
- manywidgets/number_display/__init__.py +3 -0
- manywidgets/number_display/dist/widget.js +80 -0
- manywidgets/number_display/doc.md +28 -0
- manywidgets/number_display/src/index.ts +100 -0
- manywidgets/number_display/style.css +23 -0
- manywidgets/number_display/widget.py +31 -0
- manywidgets/number_input/__init__.py +3 -0
- manywidgets/number_input/dist/widget.js +58 -0
- manywidgets/number_input/doc.md +34 -0
- manywidgets/number_input/src/index.ts +55 -0
- manywidgets/number_input/style.css +28 -0
- manywidgets/number_input/widget.py +24 -0
- manywidgets/range_slider/__init__.py +3 -0
- manywidgets/range_slider/dist/widget.js +84 -0
- manywidgets/range_slider/doc.md +36 -0
- manywidgets/range_slider/src/index.ts +89 -0
- manywidgets/range_slider/style.css +40 -0
- manywidgets/range_slider/widget.py +26 -0
- manywidgets/row/__init__.py +3 -0
- manywidgets/row/dist/widget.js +73 -0
- manywidgets/row/doc.md +32 -0
- manywidgets/row/src/index.ts +51 -0
- manywidgets/row/style.css +7 -0
- manywidgets/row/widget.py +38 -0
- manywidgets/skill/SKILL.md +71 -0
- manywidgets/skill/references/authoring.md +113 -0
- manywidgets/skill/references/usage.md +143 -0
- manywidgets/skill/references/widgets-api.md +358 -0
- manywidgets/slider/__init__.py +3 -0
- manywidgets/slider/dist/widget.js +61 -0
- manywidgets/slider/doc.md +34 -0
- manywidgets/slider/src/index.ts +66 -0
- manywidgets/slider/style.css +35 -0
- manywidgets/slider/widget.py +25 -0
- manywidgets/stat/__init__.py +3 -0
- manywidgets/stat/dist/widget.js +51 -0
- manywidgets/stat/doc.md +28 -0
- manywidgets/stat/src/index.ts +57 -0
- manywidgets/stat/style.css +55 -0
- manywidgets/stat/widget.py +26 -0
- manywidgets/text/__init__.py +3 -0
- manywidgets/text/dist/widget.js +2104 -0
- manywidgets/text/doc.md +31 -0
- manywidgets/text/src/index.ts +31 -0
- manywidgets/text/style.css +23 -0
- manywidgets/text/widget.py +22 -0
- manywidgets/toggle/__init__.py +3 -0
- manywidgets/toggle/dist/widget.js +40 -0
- manywidgets/toggle/doc.md +34 -0
- manywidgets/toggle/src/index.ts +43 -0
- manywidgets/toggle/style.css +54 -0
- manywidgets/toggle/widget.py +17 -0
- manywidgets-0.1.0.dist-info/METADATA +168 -0
- manywidgets-0.1.0.dist-info/RECORD +133 -0
- manywidgets-0.1.0.dist-info/WHEEL +4 -0
- manywidgets-0.1.0.dist-info/entry_points.txt +2 -0
- manywidgets-0.1.0.dist-info/licenses/LICENSE +21 -0
manywidgets/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""manywidgets — a composable set of anywidget-based widgets for notebooks.
|
|
2
|
+
|
|
3
|
+
Widgets are plain anywidgets: they work in a live Jupyter kernel and are
|
|
4
|
+
authored to also render correctly when statically exported (no kernel) via the
|
|
5
|
+
``myst-anywidget-static-export`` plugin. Link them with ``ipywidgets.jslink`` /
|
|
6
|
+
``jsdlink`` for the simple cases, or the :class:`~manywidgets.Binder` widget for
|
|
7
|
+
transforms and nested-path targets that jslink can't express.
|
|
8
|
+
|
|
9
|
+
The optional ``manywidgets.lonboard`` subpackage adds control widgets for lonboard
|
|
10
|
+
maps; install it with ``pip install "manywidgets[lonboard]"``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from ._base import BaseWidget
|
|
14
|
+
from ._version import __version__
|
|
15
|
+
from .binder import Binder
|
|
16
|
+
from .button import Button
|
|
17
|
+
from .chart import Chart
|
|
18
|
+
from .column import Column
|
|
19
|
+
from .dropdown import Dropdown
|
|
20
|
+
from .grid import Grid
|
|
21
|
+
from .legend import Legend
|
|
22
|
+
from .number_display import NumberDisplay
|
|
23
|
+
from .number_input import NumberInput
|
|
24
|
+
from .range_slider import RangeSlider
|
|
25
|
+
from .row import Row
|
|
26
|
+
from .slider import Slider
|
|
27
|
+
from .stat import Stat
|
|
28
|
+
from .text import Text
|
|
29
|
+
from .toggle import Toggle
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"BaseWidget",
|
|
33
|
+
"Binder",
|
|
34
|
+
"Button",
|
|
35
|
+
"Chart",
|
|
36
|
+
"Column",
|
|
37
|
+
"Dropdown",
|
|
38
|
+
"Grid",
|
|
39
|
+
"Legend",
|
|
40
|
+
"NumberDisplay",
|
|
41
|
+
"NumberInput",
|
|
42
|
+
"RangeSlider",
|
|
43
|
+
"Row",
|
|
44
|
+
"Slider",
|
|
45
|
+
"Stat",
|
|
46
|
+
"Text",
|
|
47
|
+
"Toggle",
|
|
48
|
+
"__version__",
|
|
49
|
+
]
|
manywidgets/__main__.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""``manywidgets`` command-line entry point.
|
|
2
|
+
|
|
3
|
+
Currently exposes a single subcommand, ``install-skill``, which copies the
|
|
4
|
+
bundled agent skill (``manywidgets/skill/``) into a location your coding agent
|
|
5
|
+
discovers. Claude Code looks in ``.claude/skills/`` (project) and
|
|
6
|
+
``~/.claude/skills/`` (personal), but the skill is plain Markdown — use
|
|
7
|
+
``--path`` to drop it wherever another agent expects it.
|
|
8
|
+
|
|
9
|
+
manywidgets install-skill # ./.claude/skills/manywidgets/
|
|
10
|
+
manywidgets install-skill --user # ~/.claude/skills/manywidgets/
|
|
11
|
+
manywidgets install-skill --path DIR # DIR/manywidgets/
|
|
12
|
+
manywidgets install-skill --force # overwrite an existing copy
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import pathlib
|
|
19
|
+
import shutil
|
|
20
|
+
import sys
|
|
21
|
+
from importlib import resources
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _bundled_skill_dir() -> pathlib.Path:
|
|
25
|
+
"""Path to the skill files shipped inside the installed package."""
|
|
26
|
+
return pathlib.Path(str(resources.files("manywidgets") / "skill"))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _resolve_target(args: argparse.Namespace) -> pathlib.Path:
|
|
30
|
+
"""Where the ``manywidgets/`` skill folder should be written."""
|
|
31
|
+
if args.path:
|
|
32
|
+
base = pathlib.Path(args.path).expanduser()
|
|
33
|
+
elif args.user:
|
|
34
|
+
base = pathlib.Path.home() / ".claude" / "skills"
|
|
35
|
+
else:
|
|
36
|
+
base = pathlib.Path.cwd() / ".claude" / "skills"
|
|
37
|
+
return base / "manywidgets"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def install_skill(args: argparse.Namespace) -> int:
|
|
41
|
+
src = _bundled_skill_dir()
|
|
42
|
+
if not (src / "SKILL.md").is_file():
|
|
43
|
+
print(
|
|
44
|
+
f"error: bundled skill not found at {src}. "
|
|
45
|
+
"Reinstall manywidgets, or run from a source checkout after "
|
|
46
|
+
"`npm run skill:gen`.",
|
|
47
|
+
file=sys.stderr,
|
|
48
|
+
)
|
|
49
|
+
return 1
|
|
50
|
+
|
|
51
|
+
dest = _resolve_target(args)
|
|
52
|
+
if dest.exists():
|
|
53
|
+
if not args.force:
|
|
54
|
+
print(
|
|
55
|
+
f"error: {dest} already exists. Re-run with --force to overwrite.",
|
|
56
|
+
file=sys.stderr,
|
|
57
|
+
)
|
|
58
|
+
return 1
|
|
59
|
+
shutil.rmtree(dest)
|
|
60
|
+
|
|
61
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
# Copy only the skill content (SKILL.md + references/*.md), not stray files.
|
|
63
|
+
shutil.copytree(
|
|
64
|
+
src,
|
|
65
|
+
dest,
|
|
66
|
+
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
|
|
67
|
+
)
|
|
68
|
+
print(f"Installed manywidgets skill to {dest}")
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
73
|
+
parser = argparse.ArgumentParser(prog="manywidgets")
|
|
74
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
75
|
+
|
|
76
|
+
p = sub.add_parser(
|
|
77
|
+
"install-skill",
|
|
78
|
+
help="Copy the bundled agent skill into a discoverable location.",
|
|
79
|
+
description=install_skill.__doc__,
|
|
80
|
+
)
|
|
81
|
+
where = p.add_mutually_exclusive_group()
|
|
82
|
+
where.add_argument(
|
|
83
|
+
"--user",
|
|
84
|
+
action="store_true",
|
|
85
|
+
help="Install to ~/.claude/skills/ instead of the current project.",
|
|
86
|
+
)
|
|
87
|
+
where.add_argument(
|
|
88
|
+
"--path",
|
|
89
|
+
metavar="DIR",
|
|
90
|
+
help="Install under an arbitrary directory (for agents other than Claude).",
|
|
91
|
+
)
|
|
92
|
+
p.add_argument(
|
|
93
|
+
"--force",
|
|
94
|
+
action="store_true",
|
|
95
|
+
help="Overwrite an existing installation.",
|
|
96
|
+
)
|
|
97
|
+
p.set_defaults(func=install_skill)
|
|
98
|
+
return parser
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main(argv: list[str] | None = None) -> int:
|
|
102
|
+
args = build_parser().parse_args(argv)
|
|
103
|
+
return args.func(args)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
raise SystemExit(main())
|
manywidgets/_base.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Shared base class and small helpers for every manywidgets widget.
|
|
2
|
+
|
|
3
|
+
``BaseWidget`` is a thin ``anywidget.AnyWidget`` subclass — no magic; widgets
|
|
4
|
+
remain plain anywidgets. Its one job is to give every widget a stable, unique
|
|
5
|
+
``widget_id`` trait, which is the handle that ``Binder`` and the shared
|
|
6
|
+
``@manywidgets/core`` registry use to find a widget across the live-kernel /
|
|
7
|
+
static-export boundary.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import pathlib
|
|
13
|
+
import threading
|
|
14
|
+
from collections import defaultdict
|
|
15
|
+
|
|
16
|
+
import anywidget
|
|
17
|
+
import traitlets
|
|
18
|
+
|
|
19
|
+
# Per-class monotonic counters so auto-generated ids are stable and readable
|
|
20
|
+
# (``chart_1``, ``chart_2``, ``slider_1``, …) within a kernel session.
|
|
21
|
+
_id_counters: dict[str, int] = defaultdict(int)
|
|
22
|
+
_id_lock = threading.Lock()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _next_id(prefix: str) -> str:
|
|
26
|
+
with _id_lock:
|
|
27
|
+
_id_counters[prefix] += 1
|
|
28
|
+
return f"{prefix}_{_id_counters[prefix]}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _flatten(children) -> list:
|
|
32
|
+
"""Flatten one level of list/tuple args into a flat list of children.
|
|
33
|
+
|
|
34
|
+
Lets layout widgets accept a mix of widgets and lists of widgets, e.g.
|
|
35
|
+
``Column(slider, binders, m)`` where ``binders`` is itself a list.
|
|
36
|
+
"""
|
|
37
|
+
flat: list = []
|
|
38
|
+
for child in children:
|
|
39
|
+
if isinstance(child, (list, tuple)):
|
|
40
|
+
flat.extend(child)
|
|
41
|
+
else:
|
|
42
|
+
flat.append(child)
|
|
43
|
+
return flat
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def asset(module_file: str, *parts: str) -> pathlib.Path:
|
|
47
|
+
"""Resolve a path next to a widget's ``widget.py``.
|
|
48
|
+
|
|
49
|
+
Usage inside a widget module::
|
|
50
|
+
|
|
51
|
+
_esm = asset(__file__, "dist", "widget.js")
|
|
52
|
+
_css = asset(__file__, "style.css")
|
|
53
|
+
"""
|
|
54
|
+
return pathlib.Path(module_file).parent.joinpath(*parts)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BaseWidget(anywidget.AnyWidget):
|
|
58
|
+
"""Base class for all manywidgets widgets.
|
|
59
|
+
|
|
60
|
+
Adds an auto-populated ``widget_id`` (``f"{classname_lower}_{n}"``) used for
|
|
61
|
+
cross-widget linking. Subclasses set their own ``_esm`` / ``_css`` (relative
|
|
62
|
+
to their own file, via :func:`asset`) exactly like the golden-example
|
|
63
|
+
structure.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
widget_id = traitlets.Unicode(
|
|
67
|
+
help="Stable unique id used for cross-widget linking (auto-assigned)."
|
|
68
|
+
).tag(sync=True)
|
|
69
|
+
|
|
70
|
+
def __init__(self, **kwargs):
|
|
71
|
+
super().__init__(**kwargs)
|
|
72
|
+
if not self.widget_id:
|
|
73
|
+
self.widget_id = _next_id(type(self).__name__.lower())
|
manywidgets/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// packages/core/src/index.ts
|
|
2
|
+
function safeSaveChanges(model) {
|
|
3
|
+
try {
|
|
4
|
+
model?.save_changes?.();
|
|
5
|
+
} catch {
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
function stripIpy(id) {
|
|
9
|
+
return id ? String(id).replace(/^IPY_MODEL_/, "") : "";
|
|
10
|
+
}
|
|
11
|
+
function asNumber(value, fallback = 0) {
|
|
12
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
13
|
+
return Number.isFinite(n) ? n : fallback;
|
|
14
|
+
}
|
|
15
|
+
function setByPath(model, path, value) {
|
|
16
|
+
const parts = path.split(".");
|
|
17
|
+
if (parts.length === 1) {
|
|
18
|
+
model.set(parts[0], value);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const topKey = parts[0];
|
|
22
|
+
const existing = model.get(topKey);
|
|
23
|
+
const next = existing && typeof existing === "object" ? { ...existing } : {};
|
|
24
|
+
let cursor = next;
|
|
25
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
26
|
+
const k = parts[i];
|
|
27
|
+
const child = cursor[k];
|
|
28
|
+
cursor[k] = child && typeof child === "object" ? { ...child } : {};
|
|
29
|
+
cursor = cursor[k];
|
|
30
|
+
}
|
|
31
|
+
cursor[parts[parts.length - 1]] = value;
|
|
32
|
+
model.set(topKey, next);
|
|
33
|
+
}
|
|
34
|
+
function deliverCustomMessage(model, content, buffers = []) {
|
|
35
|
+
if (typeof model.receiveCustomMessage === "function") {
|
|
36
|
+
model.receiveCustomMessage(content, buffers);
|
|
37
|
+
} else if (typeof model.trigger === "function") {
|
|
38
|
+
model.trigger("msg:custom", content, buffers);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function getStaticRegistry() {
|
|
42
|
+
const hosts = globalThis.__myst_anywidget_hosts;
|
|
43
|
+
if (!hosts || typeof hosts.values !== "function") return null;
|
|
44
|
+
for (const v of hosts.values()) return v;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function findAllInRegistry(reg, id) {
|
|
48
|
+
const out = [];
|
|
49
|
+
const seen = /* @__PURE__ */ new Set();
|
|
50
|
+
const push = (m) => {
|
|
51
|
+
if (m && !seen.has(m)) {
|
|
52
|
+
seen.add(m);
|
|
53
|
+
out.push(m);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
if (typeof reg.get === "function") push(reg.get(id));
|
|
57
|
+
const all = typeof reg.filter === "function" ? reg.filter((w) => !!w && w.model_id === id) : typeof reg.all === "function" ? reg.all().filter((w) => !!w && w.model_id === id) : [];
|
|
58
|
+
for (const m of all) push(m);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function makeHandle(getModels) {
|
|
62
|
+
return {
|
|
63
|
+
get models() {
|
|
64
|
+
return getModels();
|
|
65
|
+
},
|
|
66
|
+
get(field) {
|
|
67
|
+
const ms = getModels();
|
|
68
|
+
return ms.length ? ms[0].get(field) : void 0;
|
|
69
|
+
},
|
|
70
|
+
set(field, value) {
|
|
71
|
+
for (const m of getModels()) m.set(field, value);
|
|
72
|
+
},
|
|
73
|
+
setByPath(path, value) {
|
|
74
|
+
for (const m of getModels()) setByPath(m, path, value);
|
|
75
|
+
},
|
|
76
|
+
save() {
|
|
77
|
+
for (const m of getModels()) safeSaveChanges(m);
|
|
78
|
+
},
|
|
79
|
+
on(field, fn) {
|
|
80
|
+
for (const m of getModels()) m.on?.(`change:${field}`, () => fn(m.get(field)));
|
|
81
|
+
},
|
|
82
|
+
sendCustom(content, buffers = []) {
|
|
83
|
+
for (const m of getModels()) deliverCustomMessage(m, content, buffers);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function resolveModel(model, ref, { timeout = 5e3 } = {}) {
|
|
88
|
+
const id = stripIpy(ref);
|
|
89
|
+
const reg = getStaticRegistry();
|
|
90
|
+
if (reg) {
|
|
91
|
+
return makeHandle(() => findAllInRegistry(reg, id));
|
|
92
|
+
}
|
|
93
|
+
if (model.widget_manager?.get_model) {
|
|
94
|
+
const resolved = await withTimeout(model.widget_manager.get_model(id), timeout);
|
|
95
|
+
let current = resolved ?? null;
|
|
96
|
+
return makeHandle(() => current ? [current] : []);
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`[manywidgets] cannot resolve model "${id}": no host registry and no widget_manager`);
|
|
99
|
+
}
|
|
100
|
+
function withTimeout(promise, ms) {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const t = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
|
|
103
|
+
promise.then(
|
|
104
|
+
(v) => {
|
|
105
|
+
clearTimeout(t);
|
|
106
|
+
resolve(v);
|
|
107
|
+
},
|
|
108
|
+
(e) => {
|
|
109
|
+
clearTimeout(t);
|
|
110
|
+
reject(e);
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/manywidgets/binder/src/index.ts
|
|
117
|
+
var POLL_MS = 100;
|
|
118
|
+
var MAX_RUN_MS = 30 * 60 * 1e3;
|
|
119
|
+
async function render({ model, el }) {
|
|
120
|
+
const sourceId = model.get("source_widget_id");
|
|
121
|
+
const sourceField = model.get("source_field") || "value";
|
|
122
|
+
const targetId = model.get("target_widget_id");
|
|
123
|
+
const targetField = model.get("target_field");
|
|
124
|
+
const multiplier = model.get("multiplier");
|
|
125
|
+
const offset = model.get("offset");
|
|
126
|
+
const label = model.get("label") || `${sourceId}.${sourceField} \u2192 ${targetId}.${targetField}`;
|
|
127
|
+
el.className = "manywidgets-binder";
|
|
128
|
+
el.style.cssText = "font:12px/1.4 ui-monospace,SFMono-Regular,monospace;color:#24292e;padding:6px 10px;border:1px solid #e1e4e8;border-radius:6px;background:#f6f8fa;max-width:fit-content;margin:6px 0;";
|
|
129
|
+
const status = document.createElement("div");
|
|
130
|
+
status.textContent = `\u{1F517} binder: connecting\u2026 (${label})`;
|
|
131
|
+
el.appendChild(status);
|
|
132
|
+
let source;
|
|
133
|
+
let target;
|
|
134
|
+
try {
|
|
135
|
+
[source, target] = await Promise.all([
|
|
136
|
+
resolveModel(model, sourceId),
|
|
137
|
+
resolveModel(model, targetId)
|
|
138
|
+
]);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
status.textContent = `\u274C binder failed: ${err.message}`;
|
|
141
|
+
console.error("[manywidgets:binder] resolve failed", err, { sourceId, targetId });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
let lastKey = null;
|
|
145
|
+
const apply = () => {
|
|
146
|
+
const raw = source.get(sourceField);
|
|
147
|
+
const next = typeof raw === "number" ? asNumber(raw) * multiplier + offset : raw;
|
|
148
|
+
const key = JSON.stringify(next ?? null);
|
|
149
|
+
if (key === lastKey) return;
|
|
150
|
+
lastKey = key;
|
|
151
|
+
target.setByPath(targetField, next);
|
|
152
|
+
target.save();
|
|
153
|
+
};
|
|
154
|
+
source.on(sourceField, apply);
|
|
155
|
+
apply();
|
|
156
|
+
status.textContent = `\u2705 ${label}`;
|
|
157
|
+
const stopAt = Date.now() + MAX_RUN_MS;
|
|
158
|
+
const loop = () => {
|
|
159
|
+
try {
|
|
160
|
+
apply();
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.warn("[manywidgets:binder] tick error", err);
|
|
163
|
+
}
|
|
164
|
+
if (Date.now() < stopAt) setTimeout(loop, POLL_MS);
|
|
165
|
+
};
|
|
166
|
+
setTimeout(loop, POLL_MS);
|
|
167
|
+
}
|
|
168
|
+
var index_default = { render };
|
|
169
|
+
export {
|
|
170
|
+
index_default as default
|
|
171
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Binder
|
|
2
|
+
|
|
3
|
+
Link a source widget's trait into a target's, with an optional linear transform
|
|
4
|
+
(`multiplier`/`offset`) or a nested dotted-path target.
|
|
5
|
+
|
|
6
|
+
## Import
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from manywidgets import Binder
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Example
|
|
13
|
+
|
|
14
|
+
Drag the slider — the chart's `width` follows `value × 100`:
|
|
15
|
+
|
|
16
|
+
```{code-cell} python
|
|
17
|
+
import numpy as np
|
|
18
|
+
from manywidgets import Slider, Chart, Binder
|
|
19
|
+
|
|
20
|
+
x = np.linspace(0, 10, 100)
|
|
21
|
+
chart = Chart(title="sin")
|
|
22
|
+
chart.add_series(x=x, y=np.sin(x), name="sin")
|
|
23
|
+
|
|
24
|
+
width = Slider(label="Width", min=4, max=12, value=8)
|
|
25
|
+
binder = Binder(source=width, source_field="value",
|
|
26
|
+
target=chart, target_field="width", multiplier=100)
|
|
27
|
+
|
|
28
|
+
display(width, chart, binder)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## API
|
|
32
|
+
|
|
33
|
+
{api-table}
|
|
34
|
+
|
|
35
|
+
`Binder` covers what `jslink`/`jsdlink` can't: a linear transform on the value, or
|
|
36
|
+
writing a nested **dotted-path** target (e.g. `target_field="view_state.zoom"`),
|
|
37
|
+
merged into the parent dict. For plain pass-through links, prefer `jslink` — see the
|
|
38
|
+
[linking guide](../guides/linking.md).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { RenderProps } from "@anywidget/types";
|
|
2
|
+
import { asNumber, resolveModel, type ModelHandle } from "@manywidgets/core";
|
|
3
|
+
|
|
4
|
+
interface BinderModel {
|
|
5
|
+
source_widget_id: string;
|
|
6
|
+
source_field: string;
|
|
7
|
+
target_widget_id: string;
|
|
8
|
+
target_field: string;
|
|
9
|
+
multiplier: number;
|
|
10
|
+
offset: number;
|
|
11
|
+
label: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const POLL_MS = 100;
|
|
15
|
+
const MAX_RUN_MS = 30 * 60 * 1000;
|
|
16
|
+
|
|
17
|
+
async function render({ model, el }: RenderProps<BinderModel>): Promise<void> {
|
|
18
|
+
const sourceId = model.get("source_widget_id");
|
|
19
|
+
const sourceField = model.get("source_field") || "value";
|
|
20
|
+
const targetId = model.get("target_widget_id");
|
|
21
|
+
const targetField = model.get("target_field");
|
|
22
|
+
const multiplier = model.get("multiplier");
|
|
23
|
+
const offset = model.get("offset");
|
|
24
|
+
const label =
|
|
25
|
+
model.get("label") ||
|
|
26
|
+
`${sourceId}.${sourceField} → ${targetId}.${targetField}`;
|
|
27
|
+
|
|
28
|
+
el.className = "manywidgets-binder";
|
|
29
|
+
el.style.cssText =
|
|
30
|
+
"font:12px/1.4 ui-monospace,SFMono-Regular,monospace;color:#24292e;" +
|
|
31
|
+
"padding:6px 10px;border:1px solid #e1e4e8;border-radius:6px;" +
|
|
32
|
+
"background:#f6f8fa;max-width:fit-content;margin:6px 0;";
|
|
33
|
+
const status = document.createElement("div");
|
|
34
|
+
status.textContent = `🔗 binder: connecting… (${label})`;
|
|
35
|
+
el.appendChild(status);
|
|
36
|
+
|
|
37
|
+
let source: ModelHandle;
|
|
38
|
+
let target: ModelHandle;
|
|
39
|
+
try {
|
|
40
|
+
[source, target] = await Promise.all([
|
|
41
|
+
resolveModel(model, sourceId),
|
|
42
|
+
resolveModel(model, targetId),
|
|
43
|
+
]);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
status.textContent = `❌ binder failed: ${(err as Error).message}`;
|
|
46
|
+
console.error("[manywidgets:binder] resolve failed", err, { sourceId, targetId });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let lastKey: string | null = null;
|
|
51
|
+
const apply = (): void => {
|
|
52
|
+
const raw = source.get(sourceField);
|
|
53
|
+
const next = typeof raw === "number" ? asNumber(raw) * multiplier + offset : raw;
|
|
54
|
+
const key = JSON.stringify(next ?? null);
|
|
55
|
+
if (key === lastKey) return;
|
|
56
|
+
lastKey = key;
|
|
57
|
+
target.setByPath(targetField, next);
|
|
58
|
+
target.save();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Live kernel: change events fire on the canonical source model.
|
|
62
|
+
source.on(sourceField, apply);
|
|
63
|
+
// Apply once so the target reflects the current source state immediately.
|
|
64
|
+
apply();
|
|
65
|
+
status.textContent = `✅ ${label}`;
|
|
66
|
+
|
|
67
|
+
// Static export (and a safety net live): proxies may not emit change events
|
|
68
|
+
// and the target proxy can register late, so poll and re-apply on change.
|
|
69
|
+
const stopAt = Date.now() + MAX_RUN_MS;
|
|
70
|
+
const loop = (): void => {
|
|
71
|
+
try {
|
|
72
|
+
apply();
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.warn("[manywidgets:binder] tick error", err);
|
|
75
|
+
}
|
|
76
|
+
if (Date.now() < stopAt) setTimeout(loop, POLL_MS);
|
|
77
|
+
};
|
|
78
|
+
setTimeout(loop, POLL_MS);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default { render };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Binder — a tiny widget that wires one widget's trait to another's, in JS.
|
|
2
|
+
|
|
3
|
+
The cleaned-up successor to the ``widget_binder`` experiment. ``jslink`` /
|
|
4
|
+
``jsdlink`` is the canonical way to link manywidgets (see the linking guide);
|
|
5
|
+
``Binder`` covers the cases jslink can't express:
|
|
6
|
+
|
|
7
|
+
* a linear transform on the value (``multiplier`` / ``offset``), and
|
|
8
|
+
* writing a **nested / dotted-path** target (e.g. lonboard ``view_state.zoom``),
|
|
9
|
+
which is merged into the parent dict so listeners see one coherent update.
|
|
10
|
+
|
|
11
|
+
For ergonomics it accepts widget **instances** and reads their ``widget_id``::
|
|
12
|
+
|
|
13
|
+
Binder(source=slider, source_field="value",
|
|
14
|
+
target=chart, target_field="x_max",
|
|
15
|
+
multiplier=1000)
|
|
16
|
+
|
|
17
|
+
It works in a live kernel and in static export — the JS side resolves both
|
|
18
|
+
widgets through ``@manywidgets/core``'s ``resolveModel`` (which handles the
|
|
19
|
+
multi-proxy / late-registration hazards).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import traitlets
|
|
25
|
+
|
|
26
|
+
from .._base import BaseWidget, asset
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _id_of(widget_or_id):
|
|
30
|
+
"""Accept a widget instance (read its ``widget_id``) or a raw id string."""
|
|
31
|
+
if widget_or_id is None:
|
|
32
|
+
return ""
|
|
33
|
+
if isinstance(widget_or_id, str):
|
|
34
|
+
return widget_or_id
|
|
35
|
+
wid = getattr(widget_or_id, "widget_id", None)
|
|
36
|
+
if not wid:
|
|
37
|
+
raise TypeError(
|
|
38
|
+
f"{widget_or_id!r} has no 'widget_id'; pass a manywidgets widget "
|
|
39
|
+
"instance or an explicit id string."
|
|
40
|
+
)
|
|
41
|
+
return wid
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Binder(BaseWidget):
|
|
45
|
+
"""Link a source widget's trait to a target widget's trait, in the browser."""
|
|
46
|
+
|
|
47
|
+
_esm = asset(__file__, "dist", "widget.js")
|
|
48
|
+
|
|
49
|
+
source_widget_id = traitlets.Unicode("", help="widget_id of the source widget.").tag(sync=True)
|
|
50
|
+
source_field = traitlets.Unicode("value", help="Source trait to read.").tag(sync=True)
|
|
51
|
+
target_widget_id = traitlets.Unicode("", help="widget_id of the target widget.").tag(sync=True)
|
|
52
|
+
target_field = traitlets.Unicode(
|
|
53
|
+
"", help="Target trait/dotted-path to write (e.g. 'view_state.zoom')."
|
|
54
|
+
).tag(sync=True)
|
|
55
|
+
multiplier = traitlets.Float(1.0, help="Linear transform multiplier.").tag(sync=True)
|
|
56
|
+
offset = traitlets.Float(0.0, help="Linear transform offset.").tag(sync=True)
|
|
57
|
+
label = traitlets.Unicode("", help="Optional status label.").tag(sync=True)
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
source=None,
|
|
62
|
+
target=None,
|
|
63
|
+
*,
|
|
64
|
+
source_field="value",
|
|
65
|
+
target_field=None,
|
|
66
|
+
multiplier=1.0,
|
|
67
|
+
offset=0.0,
|
|
68
|
+
**kwargs,
|
|
69
|
+
):
|
|
70
|
+
if source is not None and "source_widget_id" not in kwargs:
|
|
71
|
+
kwargs["source_widget_id"] = _id_of(source)
|
|
72
|
+
if target is not None and "target_widget_id" not in kwargs:
|
|
73
|
+
kwargs["target_widget_id"] = _id_of(target)
|
|
74
|
+
kwargs.setdefault("source_field", source_field)
|
|
75
|
+
if target_field is not None:
|
|
76
|
+
kwargs.setdefault("target_field", target_field)
|
|
77
|
+
kwargs.setdefault("multiplier", multiplier)
|
|
78
|
+
kwargs.setdefault("offset", offset)
|
|
79
|
+
super().__init__(**kwargs)
|
|
80
|
+
if not self.target_field:
|
|
81
|
+
raise ValueError("Binder requires a 'target_field'.")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// packages/core/src/index.ts
|
|
2
|
+
function safeSaveChanges(model) {
|
|
3
|
+
try {
|
|
4
|
+
model?.save_changes?.();
|
|
5
|
+
} catch {
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
function asNumber(value, fallback = 0) {
|
|
9
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
10
|
+
return Number.isFinite(n) ? n : fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/manywidgets/button/src/index.ts
|
|
14
|
+
function render({ model, el }) {
|
|
15
|
+
const button = document.createElement("button");
|
|
16
|
+
button.className = "manywidgets-button";
|
|
17
|
+
button.type = "button";
|
|
18
|
+
button.textContent = model.get("label");
|
|
19
|
+
el.appendChild(button);
|
|
20
|
+
button.addEventListener("click", () => {
|
|
21
|
+
model.set("clicks", asNumber(model.get("clicks")) + 1);
|
|
22
|
+
safeSaveChanges(model);
|
|
23
|
+
});
|
|
24
|
+
model.on("change:label", () => {
|
|
25
|
+
button.textContent = model.get("label");
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
var index_default = { render };
|
|
29
|
+
export {
|
|
30
|
+
index_default as default
|
|
31
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Button
|
|
2
|
+
|
|
3
|
+
A click button with a `clicks` counter and Python `on_click` callbacks.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from manywidgets import Button
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Example
|
|
12
|
+
|
|
13
|
+
```{code-cell} python
|
|
14
|
+
from manywidgets import Button
|
|
15
|
+
|
|
16
|
+
btn = Button(label="Run")
|
|
17
|
+
btn
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## API
|
|
21
|
+
|
|
22
|
+
{api-table}
|
|
23
|
+
|
|
24
|
+
`on_click(callback, remove=False)` registers/removes a `callback(button)` fired
|
|
25
|
+
when `clicks` increases (live kernel only).
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
btn = Button(label="Run")
|
|
29
|
+
|
|
30
|
+
@btn.on_click
|
|
31
|
+
def _(b):
|
|
32
|
+
print("clicked", b.clicks, "times")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Linking
|
|
36
|
+
|
|
37
|
+
Link `clicks` like any trait (see the [linking guide](../guides/linking.md)):
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from ipywidgets import jsdlink
|
|
41
|
+
from manywidgets import Button, NumberDisplay
|
|
42
|
+
|
|
43
|
+
btn = Button(label="+1")
|
|
44
|
+
count = NumberDisplay(label="Clicks", duration=0)
|
|
45
|
+
jsdlink((btn, "clicks"), (count, "value"))
|
|
46
|
+
```
|