mkui 0.3.2__tar.gz → 0.3.3__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 (57) hide show
  1. {mkui-0.3.2 → mkui-0.3.3}/PKG-INFO +4 -2
  2. {mkui-0.3.2 → mkui-0.3.3}/README.md +3 -1
  3. {mkui-0.3.2 → mkui-0.3.3}/mkui/__init__.py +1 -1
  4. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/examples/history/config/client.toml +16 -4
  5. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/core.js +1 -1
  6. {mkui-0.3.2 → mkui-0.3.3}/mkui.egg-info/PKG-INFO +4 -2
  7. {mkui-0.3.2 → mkui-0.3.3}/pyproject.toml +1 -1
  8. {mkui-0.3.2 → mkui-0.3.3}/tests/test_examples.py +177 -0
  9. {mkui-0.3.2 → mkui-0.3.3}/LICENSE +0 -0
  10. {mkui-0.3.2 → mkui-0.3.3}/mkui/__main__.py +0 -0
  11. {mkui-0.3.2 → mkui-0.3.3}/mkui/control.py +0 -0
  12. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/examples/history/seed.py +0 -0
  13. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/examples/history/server.toml +0 -0
  14. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/examples/history/static/index.html +0 -0
  15. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/examples/library-js/index.html +0 -0
  16. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/examples/mkio-table/config/client.toml +22 -22
  17. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/examples/mkio-table/control.py +0 -0
  18. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/examples/mkio-table/seed.csv +0 -0
  19. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/examples/mkio-table/seed.py +0 -0
  20. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/examples/mkio-table/server.toml +0 -0
  21. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/examples/mkio-table/static/app.js +0 -0
  22. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/examples/mkio-table/static/index.html +0 -0
  23. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/examples/standalone-json/client.json +0 -0
  24. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/examples/standalone-json/index.html +0 -0
  25. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/auth.js +0 -0
  26. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/components/app.js +0 -0
  27. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/components/frame.js +0 -0
  28. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/components/menubar.js +0 -0
  29. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/components/statusbar.js +0 -0
  30. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/components/workspace.js +0 -0
  31. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/index.js +0 -0
  32. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/layout/drag.js +0 -0
  33. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/layout/tree.js +0 -0
  34. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/layouts.js +0 -0
  35. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/lib/copy.js +0 -0
  36. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/lib/expr.js +0 -0
  37. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/lib/expressions.js +0 -0
  38. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/lib/history.js +0 -0
  39. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/lib/icons.js +0 -0
  40. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/lib/layouts.js +0 -0
  41. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/lib/links.js +0 -0
  42. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/lib/rich.js +0 -0
  43. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/lib/timeparse.js +0 -0
  44. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/mkio-bridge.js +0 -0
  45. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/widgets/button.js +0 -0
  46. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/widgets/mkio-history.js +0 -0
  47. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/widgets/mkio-table.js +0 -0
  48. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/widgets/mkui-dialog.js +0 -0
  49. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/src/widgets/text.js +0 -0
  50. {mkui-0.3.2 → mkui-0.3.3}/mkui/static/styles/mkui.css +0 -0
  51. {mkui-0.3.2 → mkui-0.3.3}/mkui.egg-info/SOURCES.txt +0 -0
  52. {mkui-0.3.2 → mkui-0.3.3}/mkui.egg-info/dependency_links.txt +0 -0
  53. {mkui-0.3.2 → mkui-0.3.3}/mkui.egg-info/entry_points.txt +0 -0
  54. {mkui-0.3.2 → mkui-0.3.3}/mkui.egg-info/top_level.txt +0 -0
  55. {mkui-0.3.2 → mkui-0.3.3}/setup.cfg +0 -0
  56. {mkui-0.3.2 → mkui-0.3.3}/tests/test_cli.py +0 -0
  57. {mkui-0.3.2 → mkui-0.3.3}/tests/test_control.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkui
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Config-driven, zero-dependency web GUI framework with dockable panes
5
5
  Author: Mark Kim
6
6
  License-Expression: Apache-2.0
@@ -909,5 +909,7 @@ tests/
909
909
  test_control.py ControlService: push actions to browsers (unittest)
910
910
  test_examples.py Each example config against its server: the
911
911
  [mkio.expect] pins, pane services, history
912
- blocks, menu and frame pane ids (unittest)
912
+ blocks, menu and frame pane ids, and every
913
+ menu and button action against the ones the
914
+ app registers (unittest)
913
915
  ```
@@ -889,5 +889,7 @@ tests/
889
889
  test_control.py ControlService: push actions to browsers (unittest)
890
890
  test_examples.py Each example config against its server: the
891
891
  [mkio.expect] pins, pane services, history
892
- blocks, menu and frame pane ids (unittest)
892
+ blocks, menu and frame pane ids, and every
893
+ menu and button action against the ones the
894
+ app registers (unittest)
893
895
  ```
@@ -2,7 +2,7 @@
2
2
 
3
3
  from pathlib import Path
4
4
 
5
- __version__ = "0.3.2"
5
+ __version__ = "0.3.3"
6
6
 
7
7
  static_dir = Path(__file__).parent / "static"
8
8
  """Path to the directory containing mkui's JS, CSS, and example files.
@@ -25,6 +25,14 @@ items = [
25
25
  { label = "Redo record", action = "edit.redo" },
26
26
  ]
27
27
 
28
+ [[menubar]]
29
+ label = "Edit"
30
+ items = [
31
+ { label = "Copy", action = "edit.copy", shortcut = "mod+C" },
32
+ { label = "Select All", action = "edit.selectAll", shortcut = "mod+A" },
33
+ { label = "Find…", action = "edit.find", shortcut = "mod+F" },
34
+ ]
35
+
28
36
  [[menubar]]
29
37
  label = "View"
30
38
  items = [
@@ -37,11 +45,15 @@ items = [
37
45
  ]
38
46
 
39
47
  [[menubar]]
40
- label = "Edit"
48
+ label = "Window"
41
49
  items = [
42
- { label = "Copy", action = "edit.copy", shortcut = "mod+C" },
43
- { label = "Select All", action = "edit.selectAll", shortcut = "mod+A" },
44
- { label = "Find…", action = "edit.find", shortcut = "mod+F" },
50
+ { label = "Tile Horizontally", action = "window.tileH" },
51
+ { label = "Tile Vertically", action = "window.tileV" },
52
+ { label = "Grid", action = "window.grid" },
53
+ { label = "Cascade", action = "window.cascade" },
54
+ { sep = true },
55
+ # One item per open pane, rebuilt each time the menu opens.
56
+ { windows = true },
45
57
  ]
46
58
 
47
59
  # ─── The live table ───────────────────────────────────────────────────
@@ -4,7 +4,7 @@
4
4
  // The state store is intentionally tiny: it's a Proxy over a plain object,
5
5
  // supports dot-path get/set, and notifies subscribers per path.
6
6
 
7
- export const VERSION = "0.3.2";
7
+ export const VERSION = "0.3.3";
8
8
  export function version() { return VERSION; }
9
9
 
10
10
  import { registerExprFunction, registerExprLibrary, registerExprType, expr } from "./lib/expressions.js";
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkui
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Config-driven, zero-dependency web GUI framework with dockable panes
5
5
  Author: Mark Kim
6
6
  License-Expression: Apache-2.0
@@ -909,5 +909,7 @@ tests/
909
909
  test_control.py ControlService: push actions to browsers (unittest)
910
910
  test_examples.py Each example config against its server: the
911
911
  [mkio.expect] pins, pane services, history
912
- blocks, menu and frame pane ids (unittest)
912
+ blocks, menu and frame pane ids, and every
913
+ menu and button action against the ones the
914
+ app registers (unittest)
913
915
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mkui"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "Config-driven, zero-dependency web GUI framework with dockable panes"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -23,6 +23,7 @@ inert — so they are caught by reading the two files against each other.
23
23
 
24
24
  import asyncio
25
25
  import json
26
+ import re
26
27
  import time
27
28
  import unittest
28
29
  from pathlib import Path
@@ -237,6 +238,52 @@ class TestClientServerWiring(unittest.TestCase):
237
238
  )
238
239
 
239
240
 
241
+ SRC = Path(__file__).resolve().parent.parent / "mkui" / "static" / "src"
242
+
243
+ # `app.quit` is the one action `fireAction` handles itself (core.js); every
244
+ # other built-in is registered by name, so the source is the list.
245
+ QUIT = "app.quit"
246
+
247
+
248
+ def registered_actions(root):
249
+ """Every name passed to `registerAction("<name>", …)` under a JS tree."""
250
+ names = set()
251
+ for path in sorted(root.rglob("*.js")):
252
+ names |= set(re.findall(r'registerAction\(\s*"([^"]+)"', path.read_text()))
253
+ return names
254
+
255
+
256
+ BUILTIN_ACTIONS = registered_actions(SRC) | {QUIT}
257
+
258
+
259
+ def example_actions(name):
260
+ """Built-ins plus whatever this example's own JS registers."""
261
+ return BUILTIN_ACTIONS | registered_actions(EXAMPLES / name / "static")
262
+
263
+
264
+ def menus(client):
265
+ return client.get("menubar", []) or []
266
+
267
+
268
+ def button_actions(client):
269
+ """(pane id, button, action name) for every `type = "action"` button."""
270
+ out = []
271
+ for pid, spec in panes(client).items():
272
+ for btn in (spec.get("buttons") if isinstance(spec, dict) else None) or []:
273
+ act = btn.get("action")
274
+ if isinstance(act, dict) and act.get("type") == "action":
275
+ out.append((pid, btn, act.get("name")))
276
+ return out
277
+
278
+
279
+ def opened_panes(client):
280
+ """The panes the config's frames open at startup."""
281
+ out = set()
282
+ for frame in client.get("frames", []) or []:
283
+ out |= set(layout_panes(frame.get("layout")))
284
+ return out
285
+
286
+
240
287
  class TestPaneWiring(unittest.TestCase):
241
288
  """Every service, pane id, and table name a client config names.
242
289
 
@@ -456,5 +503,135 @@ class TestHistoryWiring(unittest.TestCase):
456
503
  self.assertIn(hist.get("versions"), declared)
457
504
 
458
505
 
506
+
507
+ class TestMenubarWiring(unittest.TestCase):
508
+ """What a menu item promises about the app behind it.
509
+
510
+ A menubar fails quietly in exactly the way the rest of this file is about:
511
+ `fireAction` warns to a console nobody is reading and the item does
512
+ nothing, so a renamed action, a `layout.*` item in an app with no
513
+ `[layouts]` block, or a pane that no menu can reach all look like a menu
514
+ that is simply broken. Reading the config against the source that
515
+ registers the actions catches them here instead.
516
+ """
517
+
518
+ def test_at_least_one_example_has_a_menubar(self):
519
+ """Guards the rest of this class against passing vacuously."""
520
+ self.assertTrue(
521
+ [e for e in examples() if menus(e[2])],
522
+ "no example declares a menubar any more",
523
+ )
524
+
525
+ def test_the_builtin_action_list_was_actually_found(self):
526
+ """The negative control for the scrape: it is a regex over the source."""
527
+ for name in ("pane.show", "window.grid", "edit.copy", "table.history"):
528
+ self.assertIn(name, BUILTIN_ACTIONS, f"{name!r} is no longer registered")
529
+ self.assertNotIn("table.nope", BUILTIN_ACTIONS)
530
+
531
+ def test_every_menu_action_is_one_the_app_registers(self):
532
+ seen = 0
533
+ for name, _server, client in examples():
534
+ known = example_actions(name)
535
+ for item in menu_items(client):
536
+ action = item.get("action")
537
+ if action is None:
538
+ continue
539
+ seen += 1
540
+ with self.subTest(example=name, item=item.get("label")):
541
+ self.assertIn(
542
+ action, known,
543
+ f"{name}: menu item {item.get('label')!r} fires "
544
+ f"{action!r}, which nothing registers",
545
+ )
546
+ self.assertTrue(seen, "no menu item fires an action any more")
547
+
548
+ def test_every_button_action_is_one_the_app_registers(self):
549
+ for name, _server, client in examples():
550
+ known = example_actions(name)
551
+ for pid, btn, action in button_actions(client):
552
+ with self.subTest(example=name, pane=pid, button=btn.get("label")):
553
+ self.assertIn(
554
+ action, known,
555
+ f"{name}: {pid}'s {btn.get('label')!r} button fires "
556
+ f"{action!r}, which nothing registers",
557
+ )
558
+
559
+ def test_layout_items_require_a_layouts_block(self):
560
+ """`layout.*` and `layouts = true` are inert without `[layouts]`."""
561
+ for name, _server, client in examples():
562
+ has_layouts = bool(client.get("layouts"))
563
+ for item in menu_items(client):
564
+ wants = (item.get("action") or "").startswith("layout.") or item.get("layouts")
565
+ if not wants:
566
+ continue
567
+ with self.subTest(example=name, item=item.get("label")):
568
+ self.assertTrue(
569
+ has_layouts,
570
+ f"{name}: menu item {item.get('label')!r} saves or "
571
+ f"restores layouts, but the config has no [layouts] block",
572
+ )
573
+
574
+ def test_an_expanding_item_carries_nothing_of_its_own(self):
575
+ """`windows` / `layouts` items are replaced wholesale when the menu
576
+ opens, so an `action` or `items` on one is silently dropped."""
577
+ seen = 0
578
+ for name, _server, client in examples():
579
+ for item in menu_items(client):
580
+ kinds = [k for k in ("windows", "layouts") if item.get(k)]
581
+ if not kinds:
582
+ continue
583
+ seen += 1
584
+ with self.subTest(example=name, item=item.get("label"), kind=kinds[0]):
585
+ self.assertEqual(len(kinds), 1, "an item expands one way")
586
+ self.assertIsNone(
587
+ item.get("action"),
588
+ f"{name}: {kinds[0]} item also names an action",
589
+ )
590
+ self.assertIsNone(
591
+ item.get("items"),
592
+ f"{name}: {kinds[0]} item also names its own submenu",
593
+ )
594
+ self.assertTrue(seen, "no menu item expands any more")
595
+
596
+ def test_every_menubar_lists_the_open_panes(self):
597
+ """A frame raised behind another, or a pane sharing a frame's tab bar,
598
+ is reachable only through the `windows = true` expansion."""
599
+ for name, _server, client in examples():
600
+ with self.subTest(example=name):
601
+ self.assertTrue(
602
+ any(it.get("windows") for it in menu_items(client)),
603
+ f"{name}: no menu expands into the open panes "
604
+ f"({{ windows = true }}), so a buried pane cannot be raised",
605
+ )
606
+
607
+ def test_a_parked_pane_has_a_menu_item_of_its_own(self):
608
+ """`windows = true` lists *open* panes, so one no frame opens needs an
609
+ explicit `pane.show` or nothing can ever open it."""
610
+ for name, _server, client in examples():
611
+ shown = {
612
+ it.get("args") for it in menu_items(client)
613
+ if it.get("action") == "pane.show" and isinstance(it.get("args"), str)
614
+ }
615
+ # A history pane is opened by `table.history`, not by id.
616
+ for pid in sorted(set(panes(client)) - opened_panes(client)):
617
+ with self.subTest(example=name, pane=pid):
618
+ self.assertIn(
619
+ pid, shown,
620
+ f"{name}: pane {pid!r} is in no frame and in no menu, "
621
+ f"so nothing can open it",
622
+ )
623
+
624
+ def test_menu_labels_are_unique(self):
625
+ """Two dropdowns with one name is a config mistake, not a feature."""
626
+ for name, _server, client in examples():
627
+ labels = [m.get("label") for m in menus(client)]
628
+ with self.subTest(example=name):
629
+ self.assertEqual(
630
+ len(labels), len(set(labels)),
631
+ f"{name}: duplicate menubar labels {labels}",
632
+ )
633
+ self.assertTrue(all(labels), f"{name}: a menu has no label")
634
+
635
+
459
636
  if __name__ == "__main__":
460
637
  unittest.main()
File without changes
File without changes
File without changes
@@ -9,15 +9,6 @@ background = "#b8b8b8"
9
9
 
10
10
  # ─── Menubar ──────────────────────────────────────────────────────────
11
11
 
12
- [[menubar]]
13
- label = "Edit"
14
- items = [
15
- { label = "Copy", action = "edit.copy", shortcut = "mod+C" },
16
- { sep = true },
17
- { label = "Select All", action = "edit.selectAll", shortcut = "mod+A" },
18
- { label = "Find…", action = "edit.find", shortcut = "mod+F" },
19
- ]
20
-
21
12
  [[menubar]]
22
13
  label = "Orders"
23
14
  items = [
@@ -28,21 +19,12 @@ items = [
28
19
  ]
29
20
 
30
21
  [[menubar]]
31
- label = "Record"
22
+ label = "Edit"
32
23
  items = [
33
- # The version timeline for whatever All Orders has selected: the chain
34
- # as a table, with a diff and a blame view under it. One history pane
35
- # per table, re-pointed as the selection moves rather than re-opened.
36
- { label = "History…", action = "table.history", args = { pane = "all-orders" } },
37
- # `keys` names the record instead, so a menu item can be a deep link.
38
- { label = "History of order 1", action = "table.history", args = { pane = "all-orders", keys = ["1"] } },
39
- { sep = true },
40
- # The steps the All Orders toolbar's Undo and Redo take, on the focused
41
- # pane. Deliberately without a shortcut: this writes to shared state.
42
- { label = "Undo record", action = "edit.undo" },
43
- { label = "Redo record", action = "edit.redo" },
24
+ { label = "Copy", action = "edit.copy", shortcut = "mod+C" },
44
25
  { sep = true },
45
- { label = "Audit Tape", action = "pane.show", args = "audit-tape" },
26
+ { label = "Select All", action = "edit.selectAll", shortcut = "mod+A" },
27
+ { label = "Find…", action = "edit.find", shortcut = "mod+F" },
46
28
  ]
47
29
 
48
30
  [[menubar]]
@@ -73,6 +55,24 @@ items = [
73
55
  { label = "Order Tree: collapse all", action = "table.expand", args = { pane = "order-tree" } },
74
56
  ]
75
57
 
58
+ [[menubar]]
59
+ label = "Record"
60
+ items = [
61
+ # The version timeline for whatever All Orders has selected: the chain
62
+ # as a table, with a diff and a blame view under it. One history pane
63
+ # per table, re-pointed as the selection moves rather than re-opened.
64
+ { label = "History…", action = "table.history", args = { pane = "all-orders" } },
65
+ # `keys` names the record instead, so a menu item can be a deep link.
66
+ { label = "History of order 1", action = "table.history", args = { pane = "all-orders", keys = ["1"] } },
67
+ { sep = true },
68
+ # The steps the All Orders toolbar's Undo and Redo take, on the focused
69
+ # pane. Deliberately without a shortcut: this writes to shared state.
70
+ { label = "Undo record", action = "edit.undo" },
71
+ { label = "Redo record", action = "edit.redo" },
72
+ { sep = true },
73
+ { label = "Audit Tape", action = "pane.show", args = "audit-tape" },
74
+ ]
75
+
76
76
  [[menubar]]
77
77
  label = "Trades"
78
78
  items = [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes