dr-widget 0.1.3__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.
- dr_widget/__init__.py +5 -0
- dr_widget/py.typed +0 -0
- dr_widget/widgets/__init__.py +5 -0
- dr_widget/widgets/config_file_manager/.gitignore +24 -0
- dr_widget/widgets/config_file_manager/.vscode/extensions.json +3 -0
- dr_widget/widgets/config_file_manager/README.md +89 -0
- dr_widget/widgets/config_file_manager/__init__.py +283 -0
- dr_widget/widgets/config_file_manager/components.json +16 -0
- dr_widget/widgets/config_file_manager/index.html +12 -0
- dr_widget/widgets/config_file_manager/jsrepo.json +18 -0
- dr_widget/widgets/config_file_manager/package.json +49 -0
- dr_widget/widgets/config_file_manager/postcss.config.js +6 -0
- dr_widget/widgets/config_file_manager/public/fonts/Inter-roman.var.woff2 +0 -0
- dr_widget/widgets/config_file_manager/public/vite.svg +1 -0
- dr_widget/widgets/config_file_manager/src/App.svelte +62 -0
- dr_widget/widgets/config_file_manager/src/ConfigFileManager.svelte +605 -0
- dr_widget/widgets/config_file_manager/src/app.css +134 -0
- dr_widget/widgets/config_file_manager/src/index.js +5 -0
- dr_widget/widgets/config_file_manager/src/lib/@test_state.json +20 -0
- dr_widget/widgets/config_file_manager/src/lib/Counter.svelte +10 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/BrowseConfigsPanel.svelte +137 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/ComplexJsonViewer.svelte +94 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/ConfigViewerPanel.svelte +282 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/LoadedConfigPreview.svelte +74 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/SaveConfigPanel.svelte +449 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/SelectedFileRow.svelte +38 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/SelectedFilesList.svelte +30 -0
- dr_widget/widgets/config_file_manager/src/lib/components/file-drop/SimpleJsonViewer.svelte +405 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/badge/badge.svelte +50 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/badge/index.ts +2 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/button/button.svelte +128 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/button/index.ts +27 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/card-action.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/card-content.svelte +15 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/card-description.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/card-footer.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/card-header.svelte +23 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/card-title.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/card.svelte +23 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/card/index.ts +25 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-close.svelte +11 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-content.svelte +47 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-description.svelte +21 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-footer.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-header.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-overlay.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-title.svelte +21 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/dialog/index.ts +41 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-close.svelte +11 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-content.svelte +41 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-description.svelte +21 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-footer.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-header.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-nested.svelte +16 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-overlay.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-title.svelte +21 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer-trigger.svelte +11 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/drawer.svelte +16 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/drawer/index.ts +45 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/empty/empty-content.svelte +23 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/empty/empty-description.svelte +23 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/empty/empty-header.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/empty/empty-media.svelte +41 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/empty/empty-title.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/empty/empty.svelte +23 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/empty/index.ts +22 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-content.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-description.svelte +25 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-error.svelte +58 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-group.svelte +23 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-label.svelte +26 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-legend.svelte +29 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-separator.svelte +38 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-set.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field-title.svelte +23 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/field.svelte +53 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/field/index.ts +33 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/file-drop-zone/file-drop-zone.svelte +178 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/file-drop-zone/index.ts +29 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/file-drop-zone/types.ts +51 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/index.ts +34 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-actions.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-content.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-description.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-footer.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-group.svelte +21 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-header.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-media.svelte +42 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-separator.svelte +19 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item-title.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/item/item.svelte +60 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/label/index.ts +7 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/label/label.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/index.ts +13 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal-content.svelte +29 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal-description.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal-footer.svelte +29 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal-header.svelte +29 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal-title.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal-trigger.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal.svelte +24 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/modal/modal.svelte.ts +32 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/separator/index.ts +7 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/separator/separator.svelte +21 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/tabs/index.ts +16 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/tabs/tabs-content.svelte +17 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/tabs/tabs-list.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/tabs/tabs-trigger.svelte +20 -0
- dr_widget/widgets/config_file_manager/src/lib/components/ui/tabs/tabs.svelte +19 -0
- dr_widget/widgets/config_file_manager/src/lib/hooks/use-file-bindings.ts +189 -0
- dr_widget/widgets/config_file_manager/src/lib/react/JsonTreeCanvas.tsx +207 -0
- dr_widget/widgets/config_file_manager/src/lib/utils/config-format.ts +113 -0
- dr_widget/widgets/config_file_manager/src/lib/utils/utils.ts +21 -0
- dr_widget/widgets/config_file_manager/src/lib/utils.ts +17 -0
- dr_widget/widgets/config_file_manager/src/main.js +7 -0
- dr_widget/widgets/config_file_manager/static/fonts/Inter-roman.var.woff2 +0 -0
- dr_widget/widgets/config_file_manager/static/index.js +9719 -0
- dr_widget/widgets/config_file_manager/static/style.css +1 -0
- dr_widget/widgets/config_file_manager/static/vite.svg +1 -0
- dr_widget/widgets/config_file_manager/svelte.config.js +8 -0
- dr_widget/widgets/config_file_manager/tailwind.config.js +12 -0
- dr_widget/widgets/config_file_manager/tsconfig.json +28 -0
- dr_widget/widgets/config_file_manager/vite.config.js +36 -0
- dr_widget-0.1.3.dist-info/METADATA +62 -0
- dr_widget-0.1.3.dist-info/RECORD +127 -0
- dr_widget-0.1.3.dist-info/WHEEL +4 -0
dr_widget/__init__.py
ADDED
dr_widget/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
node_modules
|
|
11
|
+
dist
|
|
12
|
+
dist-ssr
|
|
13
|
+
*.local
|
|
14
|
+
|
|
15
|
+
# Editor directories and files
|
|
16
|
+
.vscode/*
|
|
17
|
+
!.vscode/extensions.json
|
|
18
|
+
.idea
|
|
19
|
+
.DS_Store
|
|
20
|
+
*.suo
|
|
21
|
+
*.ntvs*
|
|
22
|
+
*.njsproj
|
|
23
|
+
*.sln
|
|
24
|
+
*.sw?
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Config File Manager Widget
|
|
2
|
+
|
|
3
|
+
The `dr_widget` package ships an AnyWidget-powered config file manager so notebooks can load, inspect, edit, and save JSON configuration blobs without leaving the browser. The frontend lives under `src/dr_widget/widgets/config_file_manager`, is built with Svelte + Vite via Bun, and syncs its state to Python traitlets.
|
|
4
|
+
|
|
5
|
+
## Repository Layout
|
|
6
|
+
|
|
7
|
+
- `src/dr_widget/widgets/config_file_manager/__init__.py` – AnyWidget class with initialization helpers and traitlet contracts.
|
|
8
|
+
- `src/dr_widget/widgets/config_file_manager/src/` – Svelte workspace (components live under `lib/`).
|
|
9
|
+
- `src/dr_widget/widgets/config_file_manager/static/` – Built bundle consumed by AnyWidget.
|
|
10
|
+
- `notebooks/config_file_manager_widget.py` – Marimo demo that exercises the widget end-to-end.
|
|
11
|
+
|
|
12
|
+
## Traitlets
|
|
13
|
+
|
|
14
|
+
| Traitlet | Direction | Description |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| `current_state` | ↔ | JSON string representing **user data only** (no metadata). |
|
|
17
|
+
| `baseline_state` | ↔ | Last saved value of `current_state`, used for dirty detection/diffs. |
|
|
18
|
+
| `version` | ↔ | String metadata displayed in the UI and written to disk alongside `current_state` (now nested under `metadata.version`). |
|
|
19
|
+
| `config_file` | ↔ | Path to the backing file (may be relative today). |
|
|
20
|
+
| `config_file_display` | ↔ | UI-friendly label derived from `config_file`. |
|
|
21
|
+
| `files` | ↔ | JSON array of uploaded files (`{ name, size, type }`). |
|
|
22
|
+
| `file_count` | ← | Derived from `files.length`; read-only in the UI. |
|
|
23
|
+
| `error` | ↔ | User-facing error message cleared automatically on recovery. |
|
|
24
|
+
|
|
25
|
+
Python helper properties (`current_data`, `baseline_data`, `is_dirty`) expose parsed state for notebooks.
|
|
26
|
+
|
|
27
|
+
## Initialization Patterns
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
ConfigFileManager() # empty widget, user loads file via UI
|
|
31
|
+
|
|
32
|
+
ConfigFileManager(config_dict={"orchard": ["Basin"]}, version="exp_v1")
|
|
33
|
+
# - current_state populated with data dict
|
|
34
|
+
# - baseline_state empty → dirty until saved
|
|
35
|
+
# - config_file defaults to "exp_v1.json"
|
|
36
|
+
|
|
37
|
+
ConfigFileManager(config_file="/path/to/existing.json")
|
|
38
|
+
# - loads and migrates the file into new format
|
|
39
|
+
# - baseline_state matches current_state (clean)
|
|
40
|
+
# - version pulled from file metadata
|
|
41
|
+
|
|
42
|
+
ConfigFileManager(
|
|
43
|
+
config_file="/tmp/new.json",
|
|
44
|
+
config_dict={"selections": {"foo": True}},
|
|
45
|
+
version="v2",
|
|
46
|
+
)
|
|
47
|
+
# - writes wrapped payload {metadata:{version,saved_at},data}
|
|
48
|
+
# - baseline_state matches current_state
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Files saved through the UI (or via `config_file` + `config_dict`) are always written as:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"metadata": {
|
|
56
|
+
"version": "v1",
|
|
57
|
+
"saved_at": "2025-11-12T10:30:00Z"
|
|
58
|
+
},
|
|
59
|
+
"data": { ... user data ... }
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Older files that only contain `selections` or embed metadata at the top level are migrated into this structure when loaded, with legacy `version`/`saved_at` values relocated under `metadata`.
|
|
64
|
+
|
|
65
|
+
## Frontend Behavior
|
|
66
|
+
|
|
67
|
+
- `use-file-bindings.ts` manages read/write loops for every synced traitlet so Marimo reactivity stays intact.
|
|
68
|
+
- `ConfigFileManager.svelte` derives dirty state by comparing `current_state` vs `baseline_state`, shows the currently loaded file name/version, and exposes Browse + Save panels.
|
|
69
|
+
- `SaveConfigPanel.svelte` wraps the current data with metadata before writing to disk (File System Access API when available, otherwise download).
|
|
70
|
+
|
|
71
|
+
## Build & Test
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
bun install
|
|
75
|
+
npx svelte-check --tsconfig src/dr_widget/widgets/config_file_manager/tsconfig.json
|
|
76
|
+
bun run build:config-file-manager
|
|
77
|
+
bun run build # aggregates widgets (currently same as line above)
|
|
78
|
+
uv build # packages the Python wheel with fresh static assets
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Manual validation: run `marimo run notebooks/config_file_manager_widget.py`, load/upload JSON configs (including legacy "selections" files), edit values, and save to disk. Confirm dirty badge toggles correctly and version changes propagate between the sidebar and save panel.
|
|
82
|
+
|
|
83
|
+
## Contributing Tips
|
|
84
|
+
|
|
85
|
+
- Shared UI lives in `src/dr_widget/widgets/config_file_manager/src/lib/{components,hooks}`; prefer reusing hooks like `use-file-bindings`.
|
|
86
|
+
- Keep Tailwind utility classes grouped logically (layout → spacing → color → effects).
|
|
87
|
+
- Treat `node_modules/` as generated; never edit or commit them.
|
|
88
|
+
- Run `bunx prettier --write src/dr_widget/widgets/config_file_manager/src` before opening a PR.
|
|
89
|
+
- Document new traitlets or metadata fields in `docs/architecture.md` and notebook demos to keep Python + Svelte in sync.
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""AnyWidget bindings for the config file manager widget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
import anywidget
|
|
11
|
+
import traitlets
|
|
12
|
+
|
|
13
|
+
__all__ = ["ConfigFileManager"]
|
|
14
|
+
|
|
15
|
+
_STATIC_DIR = Path(__file__).parent / "static"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _normalize_version(value: Optional[str]) -> str:
|
|
19
|
+
if value is None:
|
|
20
|
+
return "default_v0"
|
|
21
|
+
value = str(value).strip()
|
|
22
|
+
return value or "default_v0"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _default_config_name(version: str) -> str:
|
|
26
|
+
safe = ''.join(ch if ch.isalnum() or ch in {"-", "_"} else "_" for ch in version)
|
|
27
|
+
safe = safe or "default_v0"
|
|
28
|
+
return f"{safe}.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_config_path(value: str | Path | None) -> str:
|
|
32
|
+
if value is None:
|
|
33
|
+
return ""
|
|
34
|
+
|
|
35
|
+
raw = str(value).strip()
|
|
36
|
+
if not raw:
|
|
37
|
+
return ""
|
|
38
|
+
|
|
39
|
+
candidate = Path(raw).expanduser()
|
|
40
|
+
if not candidate.is_absolute():
|
|
41
|
+
candidate = Path.cwd() / candidate
|
|
42
|
+
|
|
43
|
+
return str(candidate.resolve())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _serialize_user_state(data: Dict[str, Any]) -> str:
|
|
47
|
+
"""Return a JSON string for the user-facing state or an empty string."""
|
|
48
|
+
|
|
49
|
+
if not data:
|
|
50
|
+
return ""
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
return json.dumps(data, sort_keys=True, separators=(",", ":"))
|
|
54
|
+
except (TypeError, ValueError) as exc: # pragma: no cover - defensive
|
|
55
|
+
raise ValueError("Config state must be JSON serializable") from exc
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _ensure_mapping(value: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
59
|
+
if value is None:
|
|
60
|
+
return {}
|
|
61
|
+
if not isinstance(value, dict):
|
|
62
|
+
raise TypeError("config_dict must be a mapping of keys to values")
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _utc_timestamp() -> str:
|
|
67
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _normalize_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
71
|
+
"""Ensure the payload follows the new metadata/data contract."""
|
|
72
|
+
|
|
73
|
+
normalized: Dict[str, Any] = dict(payload)
|
|
74
|
+
data = normalized.get("data")
|
|
75
|
+
|
|
76
|
+
metadata_candidate = normalized.get("metadata")
|
|
77
|
+
if isinstance(metadata_candidate, dict):
|
|
78
|
+
metadata: Dict[str, Any] = dict(metadata_candidate)
|
|
79
|
+
else:
|
|
80
|
+
metadata = {}
|
|
81
|
+
|
|
82
|
+
for legacy_key in ("version", "saved_at"):
|
|
83
|
+
if legacy_key in normalized and legacy_key not in metadata:
|
|
84
|
+
metadata[legacy_key] = normalized.pop(legacy_key)
|
|
85
|
+
|
|
86
|
+
if not isinstance(data, dict):
|
|
87
|
+
user_data: Dict[str, Any] = {}
|
|
88
|
+
|
|
89
|
+
selections = normalized.pop("selections", None)
|
|
90
|
+
if isinstance(selections, dict):
|
|
91
|
+
user_data.setdefault("selections", selections)
|
|
92
|
+
|
|
93
|
+
for key in list(normalized.keys()):
|
|
94
|
+
if key in {"version", "saved_at", "data", "metadata"}:
|
|
95
|
+
continue
|
|
96
|
+
user_data[key] = normalized.pop(key)
|
|
97
|
+
|
|
98
|
+
data = user_data
|
|
99
|
+
|
|
100
|
+
normalized["metadata"] = metadata
|
|
101
|
+
normalized["data"] = data if isinstance(data, dict) else {}
|
|
102
|
+
return normalized
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _load_config_from_file(path: Path) -> Dict[str, Any]:
|
|
106
|
+
try:
|
|
107
|
+
raw = path.read_text(encoding="utf-8")
|
|
108
|
+
except FileNotFoundError:
|
|
109
|
+
raise
|
|
110
|
+
except OSError as exc: # pragma: no cover - filesystem specific
|
|
111
|
+
raise IOError(f"Unable to read config file: {path}") from exc
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
parsed = json.loads(raw)
|
|
115
|
+
except json.JSONDecodeError as exc:
|
|
116
|
+
raise ValueError(f"Config file must contain valid JSON: {path}") from exc
|
|
117
|
+
|
|
118
|
+
if not isinstance(parsed, dict):
|
|
119
|
+
raise ValueError("Config file root must be a JSON object")
|
|
120
|
+
|
|
121
|
+
return _normalize_payload(parsed)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _write_config_to_file(path: Path, *, data: Dict[str, Any], version: str) -> str:
|
|
125
|
+
saved_at = _utc_timestamp()
|
|
126
|
+
payload = {
|
|
127
|
+
"metadata": {
|
|
128
|
+
"version": version,
|
|
129
|
+
"saved_at": saved_at,
|
|
130
|
+
"save_path": str(path),
|
|
131
|
+
},
|
|
132
|
+
"data": data,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
serialized = json.dumps(payload, indent=2, sort_keys=True)
|
|
136
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
path.write_text(serialized + "\n", encoding="utf-8")
|
|
138
|
+
return saved_at
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _file_binding_entry(path: Path) -> Dict[str, Any]:
|
|
142
|
+
try:
|
|
143
|
+
size = path.stat().st_size
|
|
144
|
+
except OSError:
|
|
145
|
+
size = 0
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"name": path.name,
|
|
149
|
+
"size": size,
|
|
150
|
+
"type": "application/json",
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class ConfigFileManager(anywidget.AnyWidget):
|
|
155
|
+
"""Config file manager widget for notebooks."""
|
|
156
|
+
|
|
157
|
+
# AnyWidget expects module references pointing at the built assets on disk.
|
|
158
|
+
_esm = _STATIC_DIR / "index.js"
|
|
159
|
+
_css = _STATIC_DIR / "style.css"
|
|
160
|
+
|
|
161
|
+
current_state = traitlets.Unicode("").tag(sync=True)
|
|
162
|
+
baseline_state = traitlets.Unicode("").tag(sync=True)
|
|
163
|
+
config_file = traitlets.Unicode("").tag(sync=True)
|
|
164
|
+
config_file_display = traitlets.Unicode("").tag(sync=True)
|
|
165
|
+
version = traitlets.Unicode("default_v0").tag(sync=True)
|
|
166
|
+
saved_at = traitlets.Unicode("").tag(sync=True)
|
|
167
|
+
files = traitlets.Unicode("[]").tag(sync=True)
|
|
168
|
+
file_count = traitlets.Int(0).tag(sync=True)
|
|
169
|
+
error = traitlets.Unicode("").tag(sync=True)
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
config_file: str | Path | None = None,
|
|
174
|
+
config_dict: Optional[Dict[str, Any]] = None,
|
|
175
|
+
version: str = "default_v0",
|
|
176
|
+
**kwargs: Any,
|
|
177
|
+
) -> None:
|
|
178
|
+
super().__init__(**kwargs)
|
|
179
|
+
|
|
180
|
+
normalized_version = _normalize_version(version)
|
|
181
|
+
self.version = normalized_version
|
|
182
|
+
self.current_state = ""
|
|
183
|
+
self.baseline_state = ""
|
|
184
|
+
self.config_file = ""
|
|
185
|
+
self.saved_at = ""
|
|
186
|
+
|
|
187
|
+
if config_file is None and config_dict is None:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
if config_file is None:
|
|
191
|
+
user_data = _ensure_mapping(config_dict)
|
|
192
|
+
self.current_state = _serialize_user_state(user_data)
|
|
193
|
+
# No baseline until the data is persisted via the UI.
|
|
194
|
+
self.baseline_state = ""
|
|
195
|
+
default_name = _default_config_name(self.version)
|
|
196
|
+
default_path = _resolve_config_path(default_name)
|
|
197
|
+
self.config_file = default_path
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
resolved_path = _resolve_config_path(config_file)
|
|
201
|
+
path = Path(resolved_path)
|
|
202
|
+
|
|
203
|
+
if config_dict is not None:
|
|
204
|
+
if path.exists():
|
|
205
|
+
raise FileExistsError(
|
|
206
|
+
f"Config file already exists: {path}. Refusing to overwrite."
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
user_data = _ensure_mapping(config_dict)
|
|
210
|
+
_write_config_to_file(path, data=user_data, version=self.version)
|
|
211
|
+
elif not path.exists():
|
|
212
|
+
raise FileNotFoundError(f"Config file does not exist: {path}")
|
|
213
|
+
|
|
214
|
+
payload = _load_config_from_file(path)
|
|
215
|
+
file_data = payload.get("data")
|
|
216
|
+
user_state = file_data if isinstance(file_data, dict) else {}
|
|
217
|
+
serialized_state = _serialize_user_state(user_state)
|
|
218
|
+
|
|
219
|
+
metadata = payload.get("metadata")
|
|
220
|
+
if not isinstance(metadata, dict):
|
|
221
|
+
metadata = {}
|
|
222
|
+
|
|
223
|
+
payload_version = metadata.get("version")
|
|
224
|
+
if payload_version is not None:
|
|
225
|
+
version_str = _normalize_version(str(payload_version))
|
|
226
|
+
self.version = version_str
|
|
227
|
+
|
|
228
|
+
self.config_file = str(path)
|
|
229
|
+
self.current_state = serialized_state
|
|
230
|
+
self.baseline_state = serialized_state
|
|
231
|
+
saved_at_value = metadata.get("saved_at")
|
|
232
|
+
if saved_at_value:
|
|
233
|
+
self.saved_at = str(saved_at_value)
|
|
234
|
+
else:
|
|
235
|
+
self.saved_at = ""
|
|
236
|
+
|
|
237
|
+
file_entry = _file_binding_entry(path)
|
|
238
|
+
self.files = json.dumps([file_entry])
|
|
239
|
+
self.file_count = 1
|
|
240
|
+
|
|
241
|
+
def _parse_state(self, value: str) -> Dict[str, Any]:
|
|
242
|
+
if not value:
|
|
243
|
+
return {}
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
parsed = json.loads(value)
|
|
247
|
+
except json.JSONDecodeError:
|
|
248
|
+
return {}
|
|
249
|
+
|
|
250
|
+
if isinstance(parsed, dict):
|
|
251
|
+
return parsed
|
|
252
|
+
|
|
253
|
+
return {}
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def current_data(self) -> Dict[str, Any]:
|
|
257
|
+
"""Return the parsed current_state JSON payload as a dict."""
|
|
258
|
+
|
|
259
|
+
return self._parse_state(self.current_state)
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def baseline_data(self) -> Dict[str, Any]:
|
|
263
|
+
"""Return the parsed baseline_state JSON payload as a dict."""
|
|
264
|
+
|
|
265
|
+
return self._parse_state(self.baseline_state)
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def is_dirty(self) -> bool:
|
|
269
|
+
"""True if the current state differs from the last saved baseline."""
|
|
270
|
+
|
|
271
|
+
return self.current_data != self.baseline_data
|
|
272
|
+
|
|
273
|
+
@traitlets.validate("config_file")
|
|
274
|
+
def _validate_config_file(self, proposal: traitlets.Bunch) -> str:
|
|
275
|
+
return _resolve_config_path(proposal["value"])
|
|
276
|
+
|
|
277
|
+
@traitlets.observe("config_file")
|
|
278
|
+
def _observe_config_file(self, change: traitlets.Bunch) -> None:
|
|
279
|
+
value = change["new"]
|
|
280
|
+
if value:
|
|
281
|
+
self.config_file_display = Path(value).name
|
|
282
|
+
else:
|
|
283
|
+
self.config_file_display = ""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://shadcn-svelte.com/schema.json",
|
|
3
|
+
"tailwind": {
|
|
4
|
+
"css": "src/app.css",
|
|
5
|
+
"baseColor": "slate"
|
|
6
|
+
},
|
|
7
|
+
"aliases": {
|
|
8
|
+
"components": "$lib/components",
|
|
9
|
+
"utils": "$lib/utils",
|
|
10
|
+
"ui": "$lib/components/ui",
|
|
11
|
+
"hooks": "$lib/hooks",
|
|
12
|
+
"lib": "$lib"
|
|
13
|
+
},
|
|
14
|
+
"typescript": true,
|
|
15
|
+
"registry": "https://shadcn-svelte.com/registry"
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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.0" />
|
|
6
|
+
<title>File Drop Widget - Dev Preview</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.js"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/jsrepo@2.5.1/schemas/project-config.json",
|
|
3
|
+
"repos": ["@ieedan/shadcn-svelte-extras"],
|
|
4
|
+
"includeTests": false,
|
|
5
|
+
"includeDocs": false,
|
|
6
|
+
"watermark": true,
|
|
7
|
+
"formatter": "prettier",
|
|
8
|
+
"configFiles": {
|
|
9
|
+
"Shadcn Svelte Extras Cursor Rules": "./../../../../.cursor/rules/shadcn-svelte-extras.mdc"
|
|
10
|
+
},
|
|
11
|
+
"paths": {
|
|
12
|
+
"*": "./src/lib",
|
|
13
|
+
"ui": "./src/lib/components/ui",
|
|
14
|
+
"actions": "./src/lib/actions",
|
|
15
|
+
"hooks": "./src/lib/hooks",
|
|
16
|
+
"utils": "./src/lib/utils"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dr-widget/config-file-manager",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"dev": "vite dev",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"check": "svelte-check --watch --tsconfig ./tsconfig.json"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@anywidget/svelte": "^0.1.0",
|
|
14
|
+
"@zerodevx/svelte-json-view": "^1.0.11",
|
|
15
|
+
"elkjs": "^0.11.0",
|
|
16
|
+
"lucide-svelte": "^0.552.0",
|
|
17
|
+
"react": "18.2.0",
|
|
18
|
+
"react-dom": "18.2.0",
|
|
19
|
+
"react-zoom-pan-pinch": "^3.7.0",
|
|
20
|
+
"reaflow": "^5.4.1",
|
|
21
|
+
"svelte": "^5.43.2"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@internationalized/date": "^3.8.1",
|
|
25
|
+
"@lucide/svelte": "^0.544.0",
|
|
26
|
+
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
27
|
+
"@tailwindcss/postcss": "^4.0.9",
|
|
28
|
+
"@types/bun": "latest",
|
|
29
|
+
"@types/react": "18.2.74",
|
|
30
|
+
"@types/react-dom": "18.2.25",
|
|
31
|
+
"@vitejs/plugin-react-swc": "3.7.0",
|
|
32
|
+
"autoprefixer": "^10.4.20",
|
|
33
|
+
"bits-ui": "^2.11.0",
|
|
34
|
+
"clsx": "^2.1.1",
|
|
35
|
+
"postcss": "^8.4.49",
|
|
36
|
+
"prettier": "^3.6.2",
|
|
37
|
+
"runed": "^0.31.1",
|
|
38
|
+
"svelte-check": "^4.3.3",
|
|
39
|
+
"tailwind-merge": "^3.3.1",
|
|
40
|
+
"tailwind-variants": "^3.1.1",
|
|
41
|
+
"tailwindcss": "^4.1.16",
|
|
42
|
+
"tw-animate-css": "^1.4.0",
|
|
43
|
+
"vaul-svelte": "1.0.0-next.7",
|
|
44
|
+
"vite": "^5.4.11"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"typescript": "^5.9.3"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { Separator } from "./lib/components/ui/separator/";
|
|
3
|
+
import ConfigFileManager from "./ConfigFileManager.svelte";
|
|
4
|
+
|
|
5
|
+
// Simulate anywidget bindings for testing
|
|
6
|
+
let bindings = $state({
|
|
7
|
+
files: "",
|
|
8
|
+
file_count: 0,
|
|
9
|
+
error: "",
|
|
10
|
+
});
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<main class="mx-auto max-w-5xl p-8 font-sans">
|
|
14
|
+
<div class="flex flex-col gap-8">
|
|
15
|
+
<div class="space-y-2">
|
|
16
|
+
<h1 class="text-3xl font-semibold text-zinc-900 dark:text-zinc-100">
|
|
17
|
+
Widget
|
|
18
|
+
</h1>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<ConfigFileManager {bindings} />
|
|
22
|
+
|
|
23
|
+
<div
|
|
24
|
+
class="mt-4 rounded-lg border border-zinc-200 bg-zinc-50 p-6 dark:border-zinc-700 dark:bg-zinc-900"
|
|
25
|
+
>
|
|
26
|
+
<h2 class="text-lg font-medium text-zinc-800 dark:text-zinc-100">
|
|
27
|
+
Debug Output
|
|
28
|
+
</h2>
|
|
29
|
+
|
|
30
|
+
<div
|
|
31
|
+
class="mt-4 rounded-md bg-white p-3 text-sm shadow-sm dark:bg-zinc-800"
|
|
32
|
+
>
|
|
33
|
+
<strong class="font-semibold text-zinc-700 dark:text-zinc-200"
|
|
34
|
+
>File Count:</strong
|
|
35
|
+
>
|
|
36
|
+
<span class="ml-2 text-zinc-600 dark:text-zinc-300"
|
|
37
|
+
>{bindings.file_count}</span
|
|
38
|
+
>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div
|
|
42
|
+
class="mt-4 rounded-md bg-white p-3 text-sm shadow-sm dark:bg-zinc-800"
|
|
43
|
+
>
|
|
44
|
+
<strong class="font-semibold text-zinc-700 dark:text-zinc-200"
|
|
45
|
+
>Files Data:</strong
|
|
46
|
+
>
|
|
47
|
+
<pre
|
|
48
|
+
class="mt-2 overflow-x-auto rounded-md bg-zinc-900/10 p-2 font-mono text-xs dark:bg-zinc-50/10">
|
|
49
|
+
{bindings.files || "(empty)"}</pre>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{#if bindings.error}
|
|
53
|
+
<div
|
|
54
|
+
class="mt-4 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-600 dark:border-red-500/40 dark:bg-red-950/40 dark:text-red-300"
|
|
55
|
+
>
|
|
56
|
+
<strong class="font-semibold">Error:</strong>
|
|
57
|
+
<span class="ml-2">{bindings.error}</span>
|
|
58
|
+
</div>
|
|
59
|
+
{/if}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</main>
|