wapyt 0.2.0.dev2__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 (167) hide show
  1. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/PKG-INFO +1 -1
  2. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/pyproject.toml +1 -1
  3. wapyt-0.3.0/tests/test_config_none.py +74 -0
  4. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_new_widget_configs.py +5 -0
  5. wapyt-0.3.0/tests/test_quiet_imports.py +18 -0
  6. wapyt-0.3.0/tests/test_theme_system.py +64 -0
  7. wapyt-0.3.0/tests/test_themeeditor.py +120 -0
  8. wapyt-0.3.0/tests/test_themeeditor_js.py +112 -0
  9. wapyt-0.3.0/tests/test_widget_base.py +220 -0
  10. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/__init__.py +8 -1
  11. wapyt-0.3.0/wapyt/_base.py +116 -0
  12. wapyt-0.3.0/wapyt/_config.py +21 -0
  13. wapyt-0.3.0/wapyt/assets/__pycache__/__init__.cpython-313.pyc +0 -0
  14. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/layout.js +44 -0
  15. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/terminal.js +58 -9
  16. wapyt-0.3.0/wapyt/assets/themeeditor.js +923 -0
  17. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/wapyt.css +480 -234
  18. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/cardpanel/cardpanel.py +10 -51
  19. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/chart/chart.py +6 -27
  20. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/chart/chart_config.py +1 -3
  21. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/chat/chat.py +7 -38
  22. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/chat/chat_config.py +1 -6
  23. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/contextmenu/contextmenu.py +6 -9
  24. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/contextmenu/contextmenu_config.py +1 -3
  25. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/datatable/datatable.py +7 -27
  26. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/datatable/datatable_config.py +1 -3
  27. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/form/form.py +4 -24
  28. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/form/form_config.py +3 -4
  29. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/kanban/kanban.py +6 -31
  30. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/kanban/kanban_config.py +1 -3
  31. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/layout/layout.py +21 -1
  32. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/layout/layout_config.py +1 -2
  33. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/listbox/listbox.py +6 -31
  34. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/listbox/listbox_config.py +1 -3
  35. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/mediaplayer/mediaplayer.py +7 -35
  36. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/mediaplayer/mediaplayer_config.py +1 -3
  37. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/menubar/menubar.py +6 -31
  38. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/modal/modal.py +7 -16
  39. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/pagination/pagination.py +6 -31
  40. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/pagination/pagination_config.py +1 -3
  41. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/popup/popup.py +7 -17
  42. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/popup/popup_config.py +1 -3
  43. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/progressbar/progressbar.py +5 -17
  44. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/progressbar/progressbar_config.py +1 -3
  45. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/pytincture-assets.json +9 -4
  46. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/resourceboard/resourceboard.py +5 -23
  47. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/scheduler/scheduler.py +8 -26
  48. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/scheduler/scheduler_config.py +1 -3
  49. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/sidebar/sidebar.py +7 -33
  50. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/sidebar/sidebar_config.py +8 -5
  51. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/tabwidget/tabwidget.py +6 -27
  52. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/terminal/terminal.py +9 -28
  53. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/terminal/terminal_config.py +8 -3
  54. wapyt-0.3.0/wapyt/themeeditor/__init__.py +14 -0
  55. wapyt-0.3.0/wapyt/themeeditor/themeeditor.py +149 -0
  56. wapyt-0.3.0/wapyt/themeeditor/themeeditor_config.py +173 -0
  57. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/toolbar/toolbar.py +6 -24
  58. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/toolbar/toolbar_config.py +1 -3
  59. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/tree/tree.py +7 -27
  60. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/tree/tree_config.py +1 -3
  61. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/window/window.py +8 -19
  62. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt.egg-info/PKG-INFO +1 -1
  63. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt.egg-info/SOURCES.txt +13 -0
  64. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/MANIFEST.in +0 -0
  65. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/README.md +0 -0
  66. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/setup.cfg +0 -0
  67. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_chart.py +0 -0
  68. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_chat_stream.py +0 -0
  69. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_contextmenu.py +0 -0
  70. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_datatable_classes.py +0 -0
  71. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_datatable_csv.py +0 -0
  72. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_datatable_editing.py +0 -0
  73. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_datatable_frozen.py +0 -0
  74. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_filetransfer_csrf.py +0 -0
  75. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_form_buttons.py +0 -0
  76. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_form_clear.py +0 -0
  77. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_form_fields.py +0 -0
  78. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_form_fieldsets.py +0 -0
  79. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_form_files.py +0 -0
  80. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_form_focus.py +0 -0
  81. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_form_options_icons.py +0 -0
  82. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_form_properties.py +0 -0
  83. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_form_range.py +0 -0
  84. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_form_validators.py +0 -0
  85. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_jsnull.py +0 -0
  86. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_kanban.py +0 -0
  87. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_layout_splitters.py +0 -0
  88. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_listbox.py +0 -0
  89. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_mediaplayer.py +0 -0
  90. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_menubar.py +0 -0
  91. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_message.py +0 -0
  92. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_modal_events.py +0 -0
  93. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_pagination.py +0 -0
  94. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_popup.py +0 -0
  95. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_progressbar.py +0 -0
  96. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_release_metadata.py +0 -0
  97. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_scheduler.py +0 -0
  98. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_sidebar_groups.py +0 -0
  99. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_source_warnings.py +0 -0
  100. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_toolbar.py +0 -0
  101. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_toolbar_dropdowns.py +0 -0
  102. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_tree_check_move.py +0 -0
  103. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/tests/test_window.py +0 -0
  104. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/_runtime.py +0 -0
  105. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/__init__.py +0 -0
  106. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/cardpanel.js +0 -0
  107. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/chart.js +0 -0
  108. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/chat.js +0 -0
  109. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/contextmenu.js +0 -0
  110. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/datatable.js +0 -0
  111. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/filetransfer.js +0 -0
  112. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/fonts/.keep +0 -0
  113. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/form.js +0 -0
  114. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/icons.js +0 -0
  115. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/kanban.js +0 -0
  116. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/listbox.js +0 -0
  117. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/mediaplayer.js +0 -0
  118. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/menubar.js +0 -0
  119. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/message.js +0 -0
  120. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/modal.js +0 -0
  121. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/pagination.js +0 -0
  122. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/popup.js +0 -0
  123. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/progressbar.js +0 -0
  124. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/resourceboard.js +0 -0
  125. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/scheduler.js +0 -0
  126. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/sidebar.js +0 -0
  127. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/tabwidget.js +0 -0
  128. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/toolbar.js +0 -0
  129. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/tooltip.js +0 -0
  130. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/tree.js +0 -0
  131. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/vendor-chartjs.LICENSE.md +0 -0
  132. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/vendor-chartjs.min.js +0 -0
  133. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/vendor-hlsjs.LICENSE.md +0 -0
  134. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/vendor-hlsjs.light.min.js +0 -0
  135. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/assets/window.js +0 -0
  136. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/cardpanel/__init__.py +0 -0
  137. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/cardpanel/cardpanel_config.py +0 -0
  138. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/chart/__init__.py +0 -0
  139. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/chat/__init__.py +0 -0
  140. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/contextmenu/__init__.py +0 -0
  141. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/datatable/__init__.py +0 -0
  142. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/filetransfer/__init__.py +0 -0
  143. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/filetransfer/filetransfer.py +0 -0
  144. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/form/__init__.py +0 -0
  145. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/kanban/__init__.py +0 -0
  146. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/layout/__init__.py +0 -0
  147. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/listbox/__init__.py +0 -0
  148. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/mediaplayer/__init__.py +0 -0
  149. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/menubar/__init__.py +0 -0
  150. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/menubar/menubar_config.py +0 -0
  151. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/message/__init__.py +0 -0
  152. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/message/message.py +0 -0
  153. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/modal/__init__.py +0 -0
  154. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/pagination/__init__.py +0 -0
  155. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/popup/__init__.py +0 -0
  156. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/progressbar/__init__.py +0 -0
  157. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/resourceboard/__init__.py +0 -0
  158. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/resourceboard/resourceboard_config.py +0 -0
  159. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/scheduler/__init__.py +0 -0
  160. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/sidebar/__init__.py +0 -0
  161. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/tabwidget/__init__.py +0 -0
  162. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/terminal/__init__.py +0 -0
  163. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/toolbar/__init__.py +0 -0
  164. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/tree/__init__.py +0 -0
  165. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt/window/__init__.py +0 -0
  166. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt.egg-info/dependency_links.txt +0 -0
  167. {wapyt-0.2.0.dev2 → wapyt-0.3.0}/wapyt.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wapyt
3
- Version: 0.2.0.dev2
3
+ Version: 0.3.0
4
4
  Summary: DHTMLX-free pyTincture widgetset: layouts, forms, tables, trees, chat, terminal, menus, Kanban and more, declared in Python and rendered in the browser.
5
5
  License: MIT
6
6
  Project-URL: homepage, https://github.com/WAwesome-AI/wa_pytincture_widgetset
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wapyt"
7
- version = "0.2.0.dev2"
7
+ version = "0.3.0"
8
8
  description = "DHTMLX-free pyTincture widgetset: layouts, forms, tables, trees, chat, terminal, menus, Kanban and more, declared in Python and rendered in the browser."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,74 @@
1
+ """Every config follows one None convention: unset means "not sent"."""
2
+ from __future__ import annotations
3
+
4
+ import dataclasses
5
+ import importlib
6
+ import inspect
7
+ import pkgutil
8
+
9
+ import pytest
10
+
11
+ import wapyt
12
+ from wapyt._config import drop_none
13
+
14
+ # Fields whose None means "none" (hide it), so JS must see null. Keep short.
15
+ ALLOWED_NULLS = {
16
+ ("FormConfig", "cancelText"),
17
+ ("FormConfig", "submitText"),
18
+ ("SidebarConfig", "title"),
19
+ }
20
+
21
+ # Classes that cannot be built from defaults alone get minimal arguments.
22
+ ARGS = {
23
+ "MenuItem": {"id": "m"},
24
+ "ScheduleItem": {"id": "s", "start": "2026-10-08T09:00"},
25
+ }
26
+
27
+
28
+ def _config_classes():
29
+ for info in pkgutil.walk_packages(wapyt.__path__, "wapyt."):
30
+ module = importlib.import_module(info.name)
31
+ for name, cls in inspect.getmembers(module, inspect.isclass):
32
+ if cls.__module__ == module.__name__ and dataclasses.is_dataclass(cls) and hasattr(cls, "to_dict"):
33
+ yield cls
34
+
35
+
36
+ def _build(cls):
37
+ if cls.__name__ in ARGS:
38
+ return cls(**ARGS[cls.__name__])
39
+ required = {
40
+ f.name: "x"
41
+ for f in dataclasses.fields(cls)
42
+ if f.init and f.default is dataclasses.MISSING and f.default_factory is dataclasses.MISSING
43
+ }
44
+ return cls(**required)
45
+
46
+
47
+ def _nulls(value, path=""):
48
+ if isinstance(value, dict):
49
+ for key, item in value.items():
50
+ if item is None:
51
+ yield path + key
52
+ else:
53
+ yield from _nulls(item, f"{path}{key}.")
54
+ elif isinstance(value, list):
55
+ for index, item in enumerate(value):
56
+ yield from _nulls(item, f"{path}[{index}].")
57
+
58
+
59
+ CLASSES = sorted(set(_config_classes()), key=lambda c: (c.__module__, c.__name__))
60
+
61
+
62
+ def test_found_the_configs():
63
+ assert len(CLASSES) > 40
64
+
65
+
66
+ @pytest.mark.parametrize("cls", CLASSES, ids=lambda c: c.__name__)
67
+ def test_defaults_send_no_nulls(cls):
68
+ payload = _build(cls).to_dict()
69
+ unexpected = [p for p in _nulls(payload) if (cls.__name__, p) not in ALLOWED_NULLS]
70
+ assert unexpected == []
71
+
72
+
73
+ def test_drop_none_is_shallow():
74
+ assert drop_none({"a": None, "b": 0, "c": False, "d": {"e": None}}) == {"b": 0, "c": False, "d": {"e": None}}
@@ -56,6 +56,11 @@ def test_terminal_clipboard_is_on_by_default_and_can_be_turned_off():
56
56
  assert roundtrip(TerminalConfig(clipboard=False))["clipboard"] is False
57
57
 
58
58
 
59
+ def test_terminal_osc52_is_on_by_default_and_can_be_turned_off():
60
+ assert roundtrip(TerminalConfig())["osc52"] is True
61
+ assert roundtrip(TerminalConfig(osc52=False))["osc52"] is False
62
+
63
+
59
64
  def test_terminal_extra_passes_through():
60
65
  payload = roundtrip(TerminalConfig(extra={"customFlag": 7}))
61
66
  assert payload["customFlag"] == 7
@@ -0,0 +1,18 @@
1
+ """Importing a widget module must not write to stdout or log above DEBUG."""
2
+ import importlib
3
+ import logging
4
+ import sys
5
+
6
+ import pytest
7
+
8
+ MODULES = ["wapyt.chat.chat", "wapyt.cardpanel.cardpanel"]
9
+
10
+
11
+ @pytest.mark.parametrize("name", MODULES)
12
+ def test_import_is_quiet(name, capsys, caplog):
13
+ sys.modules.pop(name, None)
14
+ with caplog.at_level(logging.INFO):
15
+ importlib.import_module(name)
16
+ out = capsys.readouterr()
17
+ assert out.out == "" and out.err == ""
18
+ assert not [r for r in caplog.records if r.name == name]
@@ -0,0 +1,64 @@
1
+ """
2
+ wapyt.setTheme in layout.js: "system" follows prefers-color-scheme. Run the
3
+ real script under Node with a fake document and matchMedia (skipped when
4
+ node is missing).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import shutil
10
+ import subprocess
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+ SCRIPT = Path(__file__).resolve().parents[1] / "wapyt" / "assets" / "layout.js"
16
+
17
+ HARNESS = """
18
+ const attrs = {};
19
+ globalThis.document = { documentElement: {
20
+ setAttribute: (k, v) => { attrs[k] = String(v); },
21
+ getAttribute: (k) => (k in attrs ? attrs[k] : null),
22
+ } };
23
+ const listeners = [];
24
+ const query = { matches: process.argv[2] === "dark",
25
+ addEventListener: (type, fn) => listeners.push(fn) };
26
+ globalThis.matchMedia = () => query;
27
+ require("vm").runInThisContext(require("fs").readFileSync(process.argv[1], "utf8"));
28
+ const log = [];
29
+ const snap = (label) => log.push([label, attrs["data-wapyt-theme"], attrs["data-wapyt-theme-mode"],
30
+ globalThis.wapyt.getTheme()]);
31
+ const osTo = (dark) => { query.matches = dark; listeners.forEach((fn) => fn({ matches: dark })); };
32
+ for (const step of JSON.parse(process.argv[3])) {
33
+ if (step[0] === "set") globalThis.wapyt.setTheme(step[1]);
34
+ else osTo(step[1] === "dark");
35
+ snap(step.join(" "));
36
+ }
37
+ console.log(JSON.stringify(log));
38
+ """
39
+
40
+
41
+ def run(os_scheme: str, steps):
42
+ node = shutil.which("node")
43
+ if not node:
44
+ pytest.skip("node is not installed")
45
+ out = subprocess.run([node, "-e", HARNESS, str(SCRIPT), os_scheme, json.dumps(steps)],
46
+ capture_output=True, text=True, check=True)
47
+ return [(theme, mode, info) for _label, theme, mode, info in json.loads(out.stdout)]
48
+
49
+
50
+ def test_system_resolves_and_follows_the_os():
51
+ result = run("dark", [["set", "system"], ["os", "light"], ["os", "dark"]])
52
+ assert [r[0] for r in result] == ["dark", "light", "dark"]
53
+ assert all(r[1] == "system" for r in result)
54
+ assert result[1][2] == {"mode": "system", "resolved": "light"}
55
+
56
+
57
+ def test_explicit_theme_ignores_the_os():
58
+ result = run("light", [["set", "system"], ["set", "light"], ["os", "dark"]])
59
+ assert result[-1][:2] == ("light", "light")
60
+
61
+
62
+ def test_back_to_system_picks_up_the_current_os_setting():
63
+ result = run("light", [["set", "dark"], ["os", "light"], ["set", "System"]])
64
+ assert result[-1][:2] == ("light", "system")
@@ -0,0 +1,120 @@
1
+ """ThemeEditor config, theme validation and the wrapper's calls."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from types import SimpleNamespace
6
+
7
+ import pytest
8
+
9
+ import wapyt.themeeditor.themeeditor as te_module
10
+ from wapyt import ThemeEditor, ThemeEditorConfig, ThemePalette
11
+ from wapyt.themeeditor import check_theme
12
+ from wapyt.themeeditor.themeeditor_config import normalize_hex
13
+
14
+
15
+ def test_default_config():
16
+ assert ThemeEditorConfig().to_dict() == {
17
+ "previewMode": True, "apply": True, "showShape": True, "showFont": True,
18
+ "showExport": True, "title": "Theme",
19
+ }
20
+
21
+
22
+ def test_title_none_is_sent_as_null():
23
+ assert ThemeEditorConfig(title=None).to_dict()["title"] is None
24
+
25
+
26
+ def test_palettes_mix_built_ins_and_custom():
27
+ brand = ThemePalette("brand", "Brand", light={"accent": "#0A7"}, dark={"accent": "#0c9"})
28
+ payload = ThemeEditorConfig(palettes=["default", brand], value={"palette": "brand"}).to_dict()
29
+ assert payload["palettes"] == ["default", {"id": "brand", "label": "Brand",
30
+ "light": {"accent": "#00aa77"}, "dark": {"accent": "#00cc99"}}]
31
+ assert payload["value"]["palette"] == "brand"
32
+
33
+
34
+ @pytest.mark.parametrize("palettes, message", [
35
+ (["nope"], "unknown palette"),
36
+ ([], "at least one"),
37
+ ([ThemePalette("ocean")], "built-in"),
38
+ ([ThemePalette("x", light={"shadow": "#000"})], "unknown theme token"),
39
+ ])
40
+ def test_bad_palettes(palettes, message):
41
+ with pytest.raises(ValueError, match=message):
42
+ ThemeEditorConfig(palettes=palettes).to_dict()
43
+
44
+
45
+ def test_bad_mode():
46
+ with pytest.raises(ValueError):
47
+ ThemeEditorConfig(mode="sepia").to_dict()
48
+
49
+
50
+ def test_check_theme_normalises():
51
+ assert check_theme({"palette": "forest", "dark": {"text": "#FFF"}, "radius": 7.6}) == {
52
+ "palette": "forest", "light": {}, "dark": {"text": "#ffffff"}, "radius": 8, "font": "system"}
53
+ assert check_theme(None)["palette"] == "default"
54
+
55
+
56
+ @pytest.mark.parametrize("theme, message", [
57
+ ({"palette": "mystery"}, "unknown palette"),
58
+ ({"light": {"accent": "blue"}}, "not a hex"),
59
+ ({"light": {"glow": "#000"}}, "unknown theme token"),
60
+ ({"radius": 40}, "radius"),
61
+ ({"radius": True}, "radius"),
62
+ ({"colours": {}}, "unknown theme keys"),
63
+ ])
64
+ def test_check_theme_rejects(theme, message):
65
+ with pytest.raises(ValueError, match=message):
66
+ check_theme(theme)
67
+
68
+
69
+ def test_custom_palette_ids_are_accepted_when_named():
70
+ assert check_theme({"palette": "brand"}, ["brand"])["palette"] == "brand"
71
+
72
+
73
+ @pytest.mark.parametrize("raw, hex_", [("#ABC", "#aabbcc"), ("123456", "#123456"), (" #0f0f0f ", "#0f0f0f")])
74
+ def test_normalize_hex(raw, hex_):
75
+ assert normalize_hex(raw) == hex_
76
+
77
+
78
+ class FakeEditor:
79
+ def __init__(self):
80
+ self.calls = []
81
+
82
+ def __getattr__(self, name):
83
+ return lambda *args: self.calls.append((name, args)) or {"palette": "default"}
84
+
85
+
86
+ @pytest.fixture
87
+ def editor(monkeypatch):
88
+ monkeypatch.setattr(te_module, "_to_js", lambda value: json.loads(json.dumps(value)))
89
+ w = object.__new__(ThemeEditor)
90
+ w.config = ThemeEditorConfig(palettes=["default", ThemePalette("brand")])
91
+ w.editor = FakeEditor()
92
+ return w
93
+
94
+
95
+ def test_set_theme_validates_then_calls_js(editor):
96
+ editor.set_theme({"palette": "brand", "light": {"accent": "#ABC"}})
97
+ name, (theme, opts) = editor.editor.calls[-1]
98
+ assert name == "setTheme" and theme["light"] == {"accent": "#aabbcc"} and opts == {"silent": False}
99
+ with pytest.raises(ValueError):
100
+ editor.set_theme({"palette": "unknown"})
101
+
102
+
103
+ def test_set_mode_checks_its_argument(editor):
104
+ editor.set_mode("dark")
105
+ assert editor.editor.calls[-1] == ("setMode", ("dark",))
106
+ with pytest.raises(ValueError):
107
+ editor.set_mode("dusk")
108
+
109
+
110
+ def test_apply_theme_registers_palettes(monkeypatch):
111
+ calls = []
112
+ ns = SimpleNamespace(registerPalette=lambda spec: calls.append(("register", spec)),
113
+ apply=lambda theme: calls.append(("apply", theme)) or theme)
114
+ monkeypatch.setattr(te_module, "_theme_js", lambda: ns)
115
+ monkeypatch.setattr(te_module, "_to_js", lambda value: value)
116
+ result = te_module.apply_theme({"palette": "brand"}, palettes=[ThemePalette("brand", light={"accent": "#000"})])
117
+ assert calls[0][0] == "register" and calls[1][0] == "apply"
118
+ assert result["palette"] == "brand"
119
+ te_module.apply_theme(None)
120
+ assert calls[-1] == ("apply", None)
@@ -0,0 +1,112 @@
1
+ """
2
+ The theme model in themeeditor.js, run under Node (skipped without node):
3
+ it must agree with the Python lists and with wapyt.css, and every built-in
4
+ palette must meet its own contrast targets.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import re
10
+ import shutil
11
+ import subprocess
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+
16
+ from wapyt.themeeditor import FONTS, PALETTES, THEME_TOKENS
17
+
18
+ ASSETS = Path(__file__).resolve().parents[1] / "wapyt" / "assets"
19
+
20
+ HARNESS = r"""
21
+ globalThis.document = { documentElement: { style: { setProperty(){}, removeProperty(){} },
22
+ setAttribute(){}, removeAttribute(){}, getAttribute(){ return null; } } };
23
+ require("vm").runInThisContext(require("fs").readFileSync(process.argv[1], "utf8"));
24
+ const T = globalThis.wapyt.theme;
25
+ const accents = ["#2563eb", "#0e7490", "#facc15", "#ff00ff", "#111111", "#eeeeee", "#16a34a", "#9333ea"];
26
+ const derived = {};
27
+ for (const a of accents) for (const m of ["light", "dark"]) {
28
+ const d = T.deriveAccent(a, m);
29
+ derived[a + "/" + m] = { ...d, ratio: T.contrast(a, d["on-accent"]) };
30
+ }
31
+ console.log(JSON.stringify({
32
+ tokens: T.TOKENS.map((t) => ({ key: t.key, light: t.light, dark: t.dark, on: t.on || null, min: t.min || null })),
33
+ fonts: T.FONTS.map((f) => f.id),
34
+ palettes: T.palettes().map((p) => ({ id: p.id, ...T.resolve({ palette: p.id }) })),
35
+ defaults: T.DEFAULT,
36
+ normalized: T.normalize({ palette: "nope", light: { accent: "#ABC", bogus: "#000", text: "red" }, radius: 99 }),
37
+ ratioWhiteBlack: T.contrast("#ffffff", "#000000"),
38
+ derived,
39
+ }));
40
+ """
41
+
42
+
43
+ @pytest.fixture(scope="module")
44
+ def model():
45
+ node = shutil.which("node")
46
+ if not node:
47
+ pytest.skip("node is not installed")
48
+ out = subprocess.run([node, "-e", HARNESS, str(ASSETS / "themeeditor.js")],
49
+ capture_output=True, text=True, check=True)
50
+ return json.loads(out.stdout)
51
+
52
+
53
+ def _lum(hex_):
54
+ def ch(v):
55
+ c = int(v, 16) / 255
56
+ return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
57
+ r, g, b = (ch(hex_[i:i + 2]) for i in (1, 3, 5))
58
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
59
+
60
+
61
+ def ratio(a, b):
62
+ hi, lo = sorted((_lum(a), _lum(b)), reverse=True)
63
+ return (hi + 0.05) / (lo + 0.05)
64
+
65
+
66
+ def test_lists_match_python(model):
67
+ assert [t["key"] for t in model["tokens"]] == THEME_TOKENS
68
+ assert [p["id"] for p in model["palettes"]] == PALETTES
69
+ assert model["fonts"] == FONTS
70
+
71
+
72
+ def test_defaults_match_the_stylesheet(model):
73
+ css = (ASSETS / "wapyt.css").read_text()
74
+ root = css[css.index(":root {"):css.index("}", css.index(":root {"))]
75
+ declared = dict(re.findall(r"(--wapyt-[a-z-]+):\s*(#[0-9a-fA-F]{6})", root))
76
+ for tok in model["tokens"]:
77
+ for mode in ("light", "dark"):
78
+ assert declared[tok[mode]].lower() == model["defaults"][mode][tok["key"]], (tok["key"], mode)
79
+ assert "--wapyt-radius: 6px" in root
80
+
81
+
82
+ def test_contrast_maths(model):
83
+ assert round(model["ratioWhiteBlack"], 2) == 21.0
84
+
85
+
86
+ @pytest.mark.parametrize("mode", ["light", "dark"])
87
+ def test_built_in_palettes_meet_their_targets(model, mode):
88
+ failures = []
89
+ for p in model["palettes"]:
90
+ v = p[mode]
91
+ for tok in model["tokens"]:
92
+ if not tok["on"] or tok["on"] == "tint":
93
+ continue
94
+ r = ratio(v[tok["key"]], v[tok["on"]])
95
+ if r < tok["min"]:
96
+ failures.append(f"{p['id']}/{mode}: {tok['key']} on {tok['on']} = {r:.2f} < {tok['min']}")
97
+ r = ratio(v["text"], v["surface"])
98
+ if r < 4.5:
99
+ failures.append(f"{p['id']}/{mode}: text on surface = {r:.2f}")
100
+ assert failures == []
101
+
102
+
103
+ def test_normalize_drops_bad_values(model):
104
+ n = model["normalized"]
105
+ assert n["palette"] == "default"
106
+ assert n["light"] == {"accent": "#aabbcc"}
107
+ assert n["radius"] == 24
108
+
109
+
110
+ def test_derived_accent_text_is_readable(model):
111
+ assert all(d["ratio"] >= 4.5 for d in model["derived"].values()), {
112
+ k: round(d["ratio"], 2) for k, d in model["derived"].items() if d["ratio"] < 4.5}
@@ -0,0 +1,220 @@
1
+ """WidgetBase: event binding, payload conversion and proxy release."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import datetime as dt
6
+ from types import SimpleNamespace
7
+
8
+ import pytest
9
+
10
+ import wapyt._base as base
11
+ from wapyt import (
12
+ CardPanel, Chart, Chat, ContextMenu, DataTable, Form, Kanban, Listbox,
13
+ MediaPlayer, MenuBar, ModalWindow, Pagination, Popup, ProgressBar,
14
+ ResourceBoard, Scheduler, Sidebar, TabWidget, Terminal, ThemeEditor, Toolbar, Tree, Window,
15
+ )
16
+ from wapyt._base import WidgetBase, resolve_root
17
+
18
+
19
+ class FakeProxy:
20
+ def __init__(self, fn):
21
+ self.fn = fn
22
+ self.destroyed = False
23
+
24
+ def __call__(self, *args):
25
+ assert not self.destroyed, "called a destroyed proxy"
26
+ return self.fn(*args)
27
+
28
+ def destroy(self):
29
+ self.destroyed = True
30
+
31
+
32
+ class FakeJsWidget:
33
+ """Stands in for a JS widget: on/off/emit/destroy."""
34
+
35
+ def __init__(self):
36
+ self.listeners = {}
37
+ self.destroyed = False
38
+ self.closed = False
39
+
40
+ def on(self, event, handler):
41
+ self.listeners.setdefault(event, []).append(handler)
42
+
43
+ def off(self, event, handler):
44
+ self.listeners[event].remove(handler)
45
+
46
+ def emit(self, event, *args):
47
+ return [h(*args) for h in self.listeners.get(event, [])]
48
+
49
+ def destroy(self):
50
+ self.destroyed = True
51
+
52
+ def close(self):
53
+ self.closed = True
54
+
55
+
56
+ class JsNull: # what to_plain recognises by type name
57
+ pass
58
+
59
+
60
+ class Demo(WidgetBase):
61
+ _js_attr = "thing"
62
+
63
+
64
+ @pytest.fixture(autouse=True)
65
+ def fake_proxies(monkeypatch):
66
+ monkeypatch.setattr(base, "create_proxy", FakeProxy)
67
+
68
+
69
+ def make(cls=Demo):
70
+ widget = object.__new__(cls) # no __init__, like the Form tests
71
+ setattr(widget, cls._js_attr, FakeJsWidget())
72
+ return widget
73
+
74
+
75
+ def test_payload_is_converted():
76
+ w = make()
77
+ seen = []
78
+ w._bind_event("select", seen.append)
79
+ w.thing.emit("select", {"id": JsNull(), "items": [JsNull()]})
80
+ assert seen == [{"id": None, "items": [None]}]
81
+
82
+
83
+ def test_no_argument_event_calls_handler_without_arguments():
84
+ # CardPanel emits "add" with nothing.
85
+ w = make()
86
+ calls = []
87
+ w._bind_event("add", lambda: calls.append("add"))
88
+ w.thing.emit("add")
89
+ assert calls == ["add"]
90
+
91
+
92
+ def test_return_value_goes_back_to_js():
93
+ # Chat marks an event handled when a listener returns True.
94
+ w = make()
95
+ w._bind_event("send", lambda payload: True)
96
+ assert w.thing.emit("send", {}) == [True]
97
+
98
+
99
+ def test_async_handler_is_scheduled():
100
+ w = make()
101
+ done = []
102
+
103
+ async def handler(payload):
104
+ done.append(payload)
105
+
106
+ async def main():
107
+ w._bind_event("change", handler)
108
+ assert w.thing.emit("change", 7) == [None]
109
+ await asyncio.sleep(0)
110
+
111
+ asyncio.run(main())
112
+ assert done == [7]
113
+
114
+
115
+ def test_every_handler_is_kept():
116
+ w = make()
117
+ seen = []
118
+ w._bind_event("select", lambda p: seen.append(("a", p)))
119
+ w._bind_event("select", lambda p: seen.append(("b", p)))
120
+ w.thing.emit("select", 1)
121
+ assert seen == [("a", 1), ("b", 1)]
122
+ assert len(w._proxies["select"]) == 2
123
+
124
+
125
+ def test_dispose_destroys_widget_then_unhooks_and_destroys_proxies():
126
+ w = make()
127
+ w._bind_event("a", lambda p: None)
128
+ w._bind_event("b", lambda p: None)
129
+ proxies = [p for ps in w._proxies.values() for p in ps]
130
+ w._dispose()
131
+ assert w.thing.destroyed
132
+ assert all(p.destroyed for p in proxies)
133
+ assert w.thing.listeners == {"a": [], "b": []}
134
+ assert w._proxies == {}
135
+ # A late event (a socket closing after destroy) reaches nothing.
136
+ assert w.thing.emit("a", 1) == []
137
+
138
+
139
+ def test_release_happens_even_if_js_destroy_fails():
140
+ w = make()
141
+ w._bind_event("a", lambda p: None)
142
+ proxy = w._proxies["a"][0]
143
+
144
+ def boom():
145
+ raise RuntimeError("js failed")
146
+
147
+ w.thing.destroy = boom
148
+ with pytest.raises(RuntimeError):
149
+ w._dispose()
150
+ assert proxy.destroyed and w._proxies == {}
151
+
152
+
153
+ def test_release_without_off_still_destroys_proxies():
154
+ w = object.__new__(Demo)
155
+ w.thing = SimpleNamespace(on=lambda event, handler: None)
156
+ w._bind_event("a", lambda p: None)
157
+ proxy = w._proxies["a"][0]
158
+ w._release_proxies()
159
+ assert proxy.destroyed
160
+
161
+
162
+ def test_resolve_root(monkeypatch):
163
+ cell = SimpleNamespace(getContainer=lambda: "cell-el")
164
+ assert resolve_root(cell, None) == "cell-el"
165
+ assert resolve_root(SimpleNamespace(element="wrapped-el"), None) == "wrapped-el"
166
+ found = {"#a": None, "a": "by-id"}
167
+ document = SimpleNamespace(
168
+ querySelector=lambda sel: found.get(sel),
169
+ getElementById=lambda id_: found.get(id_),
170
+ )
171
+ monkeypatch.setattr(base._runtime, "js", SimpleNamespace(document=document))
172
+ assert resolve_root(None, "a") == "by-id"
173
+ element = object()
174
+ assert resolve_root(None, element) is element
175
+
176
+
177
+ def test_scheduler_payloads_become_dates():
178
+ w = make(Scheduler)
179
+ seen = []
180
+ w._bind_event("move", seen.append)
181
+ w.scheduler.emit("move", {"start": "2026-10-08T09:30", "end": "2026-10-08T10:00"})
182
+ assert seen[0]["start"] == dt.datetime(2026, 10, 8, 9, 30)
183
+
184
+
185
+ WITH_DESTROY = [
186
+ CardPanel, Chart, Chat, ContextMenu, DataTable, Kanban, Listbox, MediaPlayer,
187
+ MenuBar, Pagination, Popup, ProgressBar, Scheduler, Sidebar, Terminal, ThemeEditor, Toolbar, Tree,
188
+ ]
189
+ WITH_CLOSE = [ModalWindow, Window]
190
+ ALL = WITH_DESTROY + WITH_CLOSE + [Form, ResourceBoard, TabWidget]
191
+
192
+
193
+ @pytest.mark.parametrize("cls", ALL, ids=lambda c: c.__name__)
194
+ def test_widget_binds_through_the_base(cls):
195
+ assert issubclass(cls, WidgetBase) and cls._js_attr
196
+ w = make(cls)
197
+ seen = []
198
+ w._bind_event("x", seen.append)
199
+ getattr(w, cls._js_attr).emit("x", {"v": JsNull()})
200
+ assert seen == [{"v": None}]
201
+
202
+
203
+ @pytest.mark.parametrize("cls", WITH_DESTROY, ids=lambda c: c.__name__)
204
+ def test_destroy_releases_handlers(cls):
205
+ w = make(cls)
206
+ w._bind_event("x", lambda p: None)
207
+ proxy = w._proxies["x"][0]
208
+ w.destroy()
209
+ assert getattr(w, cls._js_attr).destroyed
210
+ assert proxy.destroyed and w._proxies == {}
211
+
212
+
213
+ @pytest.mark.parametrize("cls", WITH_CLOSE, ids=lambda c: c.__name__)
214
+ def test_close_releases_handlers(cls):
215
+ w = make(cls)
216
+ w._bind_event("x", lambda p: None)
217
+ proxy = w._proxies["x"][0]
218
+ w.close()
219
+ assert getattr(w, cls._js_attr).closed
220
+ assert proxy.destroyed and w._proxies == {}
@@ -7,7 +7,7 @@ import re as _re
7
7
  __widgetset__ = "wapyt"
8
8
  # A plain string literal: pytincture reads it by parsing this file, and it must
9
9
  # match pyproject.toml and the asset manifest (CI and the release workflow check).
10
- __version__ = "0.2.0.dev2"
10
+ __version__ = "0.3.0"
11
11
  # The numeric release part only, so pre-releases ("0.2.0.dev0", "0.2.0rc1")
12
12
  # do not break `int()`.
13
13
  __version_tuple__ = tuple(int(part) for part in _re.match(r"\d+(?:\.\d+)*", __version__).group(0).split("."))
@@ -41,6 +41,7 @@ from .contextmenu.contextmenu import ContextMenu
41
41
  from .progressbar.progressbar import ProgressBar
42
42
  from .progressbar.progressbar_config import ProgressBarConfig, progress_html
43
43
  from .pagination.pagination import Pagination, page_slice
44
+ from .themeeditor import ThemeEditor, ThemeEditorConfig, ThemePalette, apply_theme, restore_theme, current_theme
44
45
  from .pagination.pagination_config import PaginationConfig
45
46
  from .contextmenu.contextmenu_config import ContextMenuConfig, MenuItem
46
47
  from .menubar.menubar import MenuBar
@@ -128,6 +129,12 @@ __all__ = [
128
129
  "ProgressBarConfig",
129
130
  "progress_html",
130
131
  "Pagination",
132
+ "ThemeEditor",
133
+ "ThemeEditorConfig",
134
+ "ThemePalette",
135
+ "apply_theme",
136
+ "restore_theme",
137
+ "current_theme",
131
138
  "PaginationConfig",
132
139
  "page_slice",
133
140
  "ContextMenuConfig",