bootstack 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.
Files changed (471) hide show
  1. bootstack/__init__.py +157 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/_core/__init__.py +21 -0
  4. bootstack/_core/capabilities/__init__.py +45 -0
  5. bootstack/_core/capabilities/after.py +103 -0
  6. bootstack/_core/capabilities/bind.py +192 -0
  7. bootstack/_core/capabilities/bindtags.py +112 -0
  8. bootstack/_core/capabilities/busy.py +72 -0
  9. bootstack/_core/capabilities/clipboard.py +89 -0
  10. bootstack/_core/capabilities/focus.py +118 -0
  11. bootstack/_core/capabilities/grab.py +65 -0
  12. bootstack/_core/capabilities/grid.py +211 -0
  13. bootstack/_core/capabilities/localization.py +231 -0
  14. bootstack/_core/capabilities/pack.py +140 -0
  15. bootstack/_core/capabilities/place.py +113 -0
  16. bootstack/_core/capabilities/selection.py +136 -0
  17. bootstack/_core/capabilities/signals.py +244 -0
  18. bootstack/_core/capabilities/winfo.py +315 -0
  19. bootstack/_core/colorutils.py +234 -0
  20. bootstack/_core/exceptions.py +25 -0
  21. bootstack/_core/images.py +463 -0
  22. bootstack/_core/mixins/__init__.py +1 -0
  23. bootstack/_core/mixins/ttk_state.py +35 -0
  24. bootstack/_core/mixins/widget.py +132 -0
  25. bootstack/_core/paths.py +49 -0
  26. bootstack/_core/publisher.py +149 -0
  27. bootstack/_core/variables.py +62 -0
  28. bootstack/_runtime/__init__.py +3 -0
  29. bootstack/_runtime/app.py +930 -0
  30. bootstack/_runtime/base_window.py +945 -0
  31. bootstack/_runtime/events.py +399 -0
  32. bootstack/_runtime/shortcuts.py +496 -0
  33. bootstack/_runtime/tk_patch.py +43 -0
  34. bootstack/_runtime/toplevel.py +276 -0
  35. bootstack/_runtime/utility.py +457 -0
  36. bootstack/_runtime/visual_focus.py +240 -0
  37. bootstack/_runtime/window_utilities.py +1123 -0
  38. bootstack/assets/__init__.py +21 -0
  39. bootstack/assets/bootstack.ico +0 -0
  40. bootstack/assets/bootstack.png +0 -0
  41. bootstack/assets/elements/__init__.py +0 -0
  42. bootstack/assets/elements/badge-pill.png +0 -0
  43. bootstack/assets/elements/badge-square.png +0 -0
  44. bootstack/assets/elements/button-compact.png +0 -0
  45. bootstack/assets/elements/button-default.png +0 -0
  46. bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
  47. bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
  48. bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
  49. bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
  50. bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
  51. bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
  52. bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
  53. bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
  54. bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
  55. bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
  56. bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
  57. bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
  58. bootstack/assets/elements/card.png +0 -0
  59. bootstack/assets/elements/checkbox-checked.png +0 -0
  60. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  61. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  62. bootstack/assets/elements/field.png +0 -0
  63. bootstack/assets/elements/input-addon-compact.png +0 -0
  64. bootstack/assets/elements/input-addon-default.png +0 -0
  65. bootstack/assets/elements/input-compact.png +0 -0
  66. bootstack/assets/elements/input-default.png +0 -0
  67. bootstack/assets/elements/list-item-separated.png +0 -0
  68. bootstack/assets/elements/list-item.png +0 -0
  69. bootstack/assets/elements/listrow-compact.png +0 -0
  70. bootstack/assets/elements/listrow-default.png +0 -0
  71. bootstack/assets/elements/manifest.toml +361 -0
  72. bootstack/assets/elements/menu-item.png +0 -0
  73. bootstack/assets/elements/navitem-compact.png +0 -0
  74. bootstack/assets/elements/navitem-default.png +0 -0
  75. bootstack/assets/elements/progressbar-h-compact.png +0 -0
  76. bootstack/assets/elements/progressbar-h-default.png +0 -0
  77. bootstack/assets/elements/progressbar-v-compact.png +0 -0
  78. bootstack/assets/elements/progressbar-v-default.png +0 -0
  79. bootstack/assets/elements/radiobutton.png +0 -0
  80. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  81. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  82. bootstack/assets/elements/slider-handle.png +0 -0
  83. bootstack/assets/elements/slider-track-h.png +0 -0
  84. bootstack/assets/elements/slider-track-v.png +0 -0
  85. bootstack/assets/elements/switch-off.png +0 -0
  86. bootstack/assets/elements/switch-on.png +0 -0
  87. bootstack/assets/elements/tab-h.png +0 -0
  88. bootstack/assets/elements/tab-v.png +0 -0
  89. bootstack/assets/icons/bootstrap.ttf +0 -0
  90. bootstack/assets/icons/glyphmap.json +2080 -0
  91. bootstack/assets/icons/icon_metrics.json +12470 -0
  92. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  93. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
  94. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
  95. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
  96. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  97. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
  98. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  99. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
  100. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  101. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
  102. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  103. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
  104. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  105. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
  106. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  107. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
  108. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  109. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
  110. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  111. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
  112. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  113. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
  114. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  115. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
  116. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  117. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
  118. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  119. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
  120. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  121. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
  122. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  123. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
  124. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  125. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
  126. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  127. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
  128. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  129. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
  130. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  131. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
  132. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  133. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
  134. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  135. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
  136. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  137. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
  138. bootstack/cli/__init__.py +133 -0
  139. bootstack/cli/__main__.py +6 -0
  140. bootstack/cli/add.py +395 -0
  141. bootstack/cli/appicon.py +285 -0
  142. bootstack/cli/build.py +115 -0
  143. bootstack/cli/config.py +313 -0
  144. bootstack/cli/demo.py +564 -0
  145. bootstack/cli/dev.py +153 -0
  146. bootstack/cli/doctor.py +195 -0
  147. bootstack/cli/icons.py +98 -0
  148. bootstack/cli/promote.py +120 -0
  149. bootstack/cli/pyinstaller.py +268 -0
  150. bootstack/cli/run.py +95 -0
  151. bootstack/cli/start.py +117 -0
  152. bootstack/cli/templates/__init__.py +931 -0
  153. bootstack/clipboard.py +48 -0
  154. bootstack/constants.py +318 -0
  155. bootstack/data/README.md +615 -0
  156. bootstack/data/__init__.py +78 -0
  157. bootstack/data/_observable.py +276 -0
  158. bootstack/data/base.py +780 -0
  159. bootstack/data/file_source.py +367 -0
  160. bootstack/data/memory_source.py +450 -0
  161. bootstack/data/query.py +367 -0
  162. bootstack/data/readers.py +289 -0
  163. bootstack/data/sqlite_source.py +869 -0
  164. bootstack/data/types.py +354 -0
  165. bootstack/data/writers.py +232 -0
  166. bootstack/dev/__init__.py +36 -0
  167. bootstack/dev/_capture.py +141 -0
  168. bootstack/dev/_env.py +43 -0
  169. bootstack/dev/_registry.py +140 -0
  170. bootstack/dev/_reloader.py +351 -0
  171. bootstack/dev/_reset.py +50 -0
  172. bootstack/dev/_watcher.py +91 -0
  173. bootstack/dialogs/__init__.py +948 -0
  174. bootstack/dialogs/_impl/__init__.py +47 -0
  175. bootstack/dialogs/_impl/colorchooser.py +588 -0
  176. bootstack/dialogs/_impl/datedialog.py +450 -0
  177. bootstack/dialogs/_impl/dialog.py +594 -0
  178. bootstack/dialogs/_impl/filterdialog.py +358 -0
  179. bootstack/dialogs/_impl/fontdialog.py +364 -0
  180. bootstack/dialogs/_impl/formdialog.py +564 -0
  181. bootstack/dialogs/_impl/message.py +486 -0
  182. bootstack/dialogs/_impl/query.py +570 -0
  183. bootstack/errors.py +67 -0
  184. bootstack/events/__init__.py +111 -0
  185. bootstack/events/_event.py +135 -0
  186. bootstack/events/_payloads.py +539 -0
  187. bootstack/events/_subscription.py +38 -0
  188. bootstack/i18n/README.md +77 -0
  189. bootstack/i18n/__init__.py +23 -0
  190. bootstack/i18n/catalog.py +121 -0
  191. bootstack/i18n/intl_format.py +584 -0
  192. bootstack/i18n/msgcat.py +425 -0
  193. bootstack/i18n/specs.py +156 -0
  194. bootstack/images.py +563 -0
  195. bootstack/py.typed +1 -0
  196. bootstack/scheduling/__init__.py +11 -0
  197. bootstack/scheduling/_schedule.py +218 -0
  198. bootstack/shortcuts.py +21 -0
  199. bootstack/signals/README.md +98 -0
  200. bootstack/signals/__init__.py +10 -0
  201. bootstack/signals/integration.py +100 -0
  202. bootstack/signals/signal.py +353 -0
  203. bootstack/signals/types.py +6 -0
  204. bootstack/store.py +286 -0
  205. bootstack/streams/__init__.py +12 -0
  206. bootstack/streams/_stream.py +321 -0
  207. bootstack/style/__init__.py +38 -0
  208. bootstack/style/builders/__init__.py +51 -0
  209. bootstack/style/builders/badge.py +46 -0
  210. bootstack/style/builders/button.py +339 -0
  211. bootstack/style/builders/buttongroup.py +311 -0
  212. bootstack/style/builders/calendar.py +271 -0
  213. bootstack/style/builders/checkbutton.py +110 -0
  214. bootstack/style/builders/combobox.py +113 -0
  215. bootstack/style/builders/contextmenu.py +268 -0
  216. bootstack/style/builders/entry.py +82 -0
  217. bootstack/style/builders/expander.py +148 -0
  218. bootstack/style/builders/field.py +335 -0
  219. bootstack/style/builders/frame.py +50 -0
  220. bootstack/style/builders/label.py +28 -0
  221. bootstack/style/builders/labelframe.py +34 -0
  222. bootstack/style/builders/listview.py +369 -0
  223. bootstack/style/builders/menubar.py +91 -0
  224. bootstack/style/builders/menubutton.py +359 -0
  225. bootstack/style/builders/panedwindow.py +25 -0
  226. bootstack/style/builders/progressbar.py +67 -0
  227. bootstack/style/builders/radiobutton.py +99 -0
  228. bootstack/style/builders/scale.py +64 -0
  229. bootstack/style/builders/scrollbar.py +225 -0
  230. bootstack/style/builders/separator.py +49 -0
  231. bootstack/style/builders/sidenav.py +643 -0
  232. bootstack/style/builders/sizegrip.py +15 -0
  233. bootstack/style/builders/spinbox.py +119 -0
  234. bootstack/style/builders/switch.py +70 -0
  235. bootstack/style/builders/tabitem.py +204 -0
  236. bootstack/style/builders/togglegroup.py +294 -0
  237. bootstack/style/builders/toolbutton.py +275 -0
  238. bootstack/style/builders/tooltip.py +26 -0
  239. bootstack/style/builders/treeview.py +193 -0
  240. bootstack/style/builders/utils.py +455 -0
  241. bootstack/style/builders_tk/__init__.py +16 -0
  242. bootstack/style/builders_tk/defaults.py +229 -0
  243. bootstack/style/element.py +173 -0
  244. bootstack/style/fonts.py +123 -0
  245. bootstack/style/style.py +609 -0
  246. bootstack/style/style_builder_base.py +716 -0
  247. bootstack/style/style_builder_mixed.py +93 -0
  248. bootstack/style/style_builder_tk.py +109 -0
  249. bootstack/style/style_builder_ttk.py +353 -0
  250. bootstack/style/style_resolver.py +447 -0
  251. bootstack/style/theme.py +245 -0
  252. bootstack/style/theme_provider.py +471 -0
  253. bootstack/style/themes/__init__.py +128 -0
  254. bootstack/style/tk_patch.py +5 -0
  255. bootstack/style/token_maps.py +41 -0
  256. bootstack/style/types.py +32 -0
  257. bootstack/style/typography.py +523 -0
  258. bootstack/style/utility.py +746 -0
  259. bootstack/types.py +39 -0
  260. bootstack/validation/__init__.py +6 -0
  261. bootstack/validation/types.py +15 -0
  262. bootstack/validation/validation_result.py +17 -0
  263. bootstack/validation/validation_rules.py +205 -0
  264. bootstack/widgets/__init__.py +74 -0
  265. bootstack/widgets/_core/__init__.py +31 -0
  266. bootstack/widgets/_core/app_config.py +266 -0
  267. bootstack/widgets/_core/base.py +461 -0
  268. bootstack/widgets/_core/container.py +334 -0
  269. bootstack/widgets/_core/context.py +35 -0
  270. bootstack/widgets/_core/events.py +130 -0
  271. bootstack/widgets/_core/field_mixin.py +353 -0
  272. bootstack/widgets/_core/icon_image_props.py +72 -0
  273. bootstack/widgets/_core/image_binding.py +93 -0
  274. bootstack/widgets/_core/navmodel.py +345 -0
  275. bootstack/widgets/_core/options.py +210 -0
  276. bootstack/widgets/_core/selection_group.py +130 -0
  277. bootstack/widgets/_core/window_controls.py +96 -0
  278. bootstack/widgets/_core/window_menu.py +246 -0
  279. bootstack/widgets/_impl/__init__.py +1 -0
  280. bootstack/widgets/_impl/_internal/__init__.py +0 -0
  281. bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
  282. bootstack/widgets/_impl/_parts/__init__.py +11 -0
  283. bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
  284. bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
  285. bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
  286. bootstack/widgets/_impl/composites/__init__.py +33 -0
  287. bootstack/widgets/_impl/composites/_dateutils.py +33 -0
  288. bootstack/widgets/_impl/composites/_image_fit.py +105 -0
  289. bootstack/widgets/_impl/composites/accordion.py +381 -0
  290. bootstack/widgets/_impl/composites/avatar.py +191 -0
  291. bootstack/widgets/_impl/composites/buttongroup.py +371 -0
  292. bootstack/widgets/_impl/composites/calendar.py +972 -0
  293. bootstack/widgets/_impl/composites/carousel.py +567 -0
  294. bootstack/widgets/_impl/composites/chart.py +882 -0
  295. bootstack/widgets/_impl/composites/compositeframe.py +298 -0
  296. bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
  297. bootstack/widgets/_impl/composites/dateentry.py +404 -0
  298. bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
  299. bootstack/widgets/_impl/composites/expander.py +515 -0
  300. bootstack/widgets/_impl/composites/field.py +670 -0
  301. bootstack/widgets/_impl/composites/form.py +1066 -0
  302. bootstack/widgets/_impl/composites/gallery.py +551 -0
  303. bootstack/widgets/_impl/composites/list/__init__.py +15 -0
  304. bootstack/widgets/_impl/composites/list/listitem.py +802 -0
  305. bootstack/widgets/_impl/composites/list/listview.py +1433 -0
  306. bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
  307. bootstack/widgets/_impl/composites/menu/model.py +358 -0
  308. bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
  309. bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
  310. bootstack/widgets/_impl/composites/meter.py +860 -0
  311. bootstack/widgets/_impl/composites/numericentry.py +201 -0
  312. bootstack/widgets/_impl/composites/pagestack.py +395 -0
  313. bootstack/widgets/_impl/composites/passwordentry.py +142 -0
  314. bootstack/widgets/_impl/composites/pathentry.py +168 -0
  315. bootstack/widgets/_impl/composites/picture.py +289 -0
  316. bootstack/widgets/_impl/composites/radiogroup.py +511 -0
  317. bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
  318. bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
  319. bootstack/widgets/_impl/composites/scrollview.py +764 -0
  320. bootstack/widgets/_impl/composites/selectbox.py +1026 -0
  321. bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
  322. bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
  323. bootstack/widgets/_impl/composites/shell/layout.py +335 -0
  324. bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
  325. bootstack/widgets/_impl/composites/shell/providers.py +558 -0
  326. bootstack/widgets/_impl/composites/shell/rail.py +117 -0
  327. bootstack/widgets/_impl/composites/shell/shell.py +581 -0
  328. bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
  329. bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
  330. bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
  331. bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
  332. bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
  333. bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
  334. bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
  335. bootstack/widgets/_impl/composites/slider/slider.py +851 -0
  336. bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
  337. bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
  338. bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
  339. bootstack/widgets/_impl/composites/tableview/types.py +169 -0
  340. bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
  341. bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
  342. bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
  343. bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
  344. bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
  345. bootstack/widgets/_impl/composites/textarea/change.py +46 -0
  346. bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
  347. bootstack/widgets/_impl/composites/textarea/core.py +495 -0
  348. bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
  349. bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
  350. bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
  351. bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
  352. bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
  353. bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
  354. bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
  355. bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
  356. bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
  357. bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
  358. bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
  359. bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
  360. bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
  361. bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
  362. bootstack/widgets/_impl/composites/textentry.py +57 -0
  363. bootstack/widgets/_impl/composites/timeentry.py +176 -0
  364. bootstack/widgets/_impl/composites/toast.py +390 -0
  365. bootstack/widgets/_impl/composites/toast_stack.py +156 -0
  366. bootstack/widgets/_impl/composites/togglegroup.py +418 -0
  367. bootstack/widgets/_impl/composites/toolbar.py +608 -0
  368. bootstack/widgets/_impl/composites/tooltip.py +491 -0
  369. bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
  370. bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
  371. bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
  372. bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
  373. bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
  374. bootstack/widgets/_impl/mixins/__init__.py +24 -0
  375. bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
  376. bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
  377. bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
  378. bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
  379. bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
  380. bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
  381. bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
  382. bootstack/widgets/_impl/primitives/__init__.py +49 -0
  383. bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
  384. bootstack/widgets/_impl/primitives/badge.py +45 -0
  385. bootstack/widgets/_impl/primitives/button.py +76 -0
  386. bootstack/widgets/_impl/primitives/card.py +45 -0
  387. bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
  388. bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
  389. bootstack/widgets/_impl/primitives/combobox.py +156 -0
  390. bootstack/widgets/_impl/primitives/entry.py +87 -0
  391. bootstack/widgets/_impl/primitives/flexframe.py +448 -0
  392. bootstack/widgets/_impl/primitives/frame.py +185 -0
  393. bootstack/widgets/_impl/primitives/gridframe.py +546 -0
  394. bootstack/widgets/_impl/primitives/label.py +84 -0
  395. bootstack/widgets/_impl/primitives/labelframe.py +54 -0
  396. bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
  397. bootstack/widgets/_impl/primitives/packframe.py +227 -0
  398. bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
  399. bootstack/widgets/_impl/primitives/progressbar.py +83 -0
  400. bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
  401. bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
  402. bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
  403. bootstack/widgets/_impl/primitives/separator.py +43 -0
  404. bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
  405. bootstack/widgets/_impl/primitives/spinbox.py +95 -0
  406. bootstack/widgets/_impl/primitives/switch.py +44 -0
  407. bootstack/widgets/_impl/primitives/treeview.py +69 -0
  408. bootstack/widgets/app.py +371 -0
  409. bootstack/widgets/appshell.py +1179 -0
  410. bootstack/widgets/avatar.py +140 -0
  411. bootstack/widgets/boolean_controls.py +455 -0
  412. bootstack/widgets/button.py +224 -0
  413. bootstack/widgets/buttongroup.py +239 -0
  414. bootstack/widgets/calendar.py +195 -0
  415. bootstack/widgets/card.py +159 -0
  416. bootstack/widgets/carousel.py +241 -0
  417. bootstack/widgets/chart.py +302 -0
  418. bootstack/widgets/codeeditor.py +675 -0
  419. bootstack/widgets/contextmenu.py +371 -0
  420. bootstack/widgets/datatable.py +688 -0
  421. bootstack/widgets/datefield.py +395 -0
  422. bootstack/widgets/divider.py +60 -0
  423. bootstack/widgets/expander.py +579 -0
  424. bootstack/widgets/form.py +200 -0
  425. bootstack/widgets/gallery.py +250 -0
  426. bootstack/widgets/gauge.py +168 -0
  427. bootstack/widgets/grid.py +121 -0
  428. bootstack/widgets/groupbox.py +162 -0
  429. bootstack/widgets/label.py +229 -0
  430. bootstack/widgets/listview.py +354 -0
  431. bootstack/widgets/menubutton.py +419 -0
  432. bootstack/widgets/numberfield.py +431 -0
  433. bootstack/widgets/pagestack.py +345 -0
  434. bootstack/widgets/passwordfield.py +383 -0
  435. bootstack/widgets/pathfield.py +454 -0
  436. bootstack/widgets/picture.py +251 -0
  437. bootstack/widgets/progressbar.py +106 -0
  438. bootstack/widgets/radio_variants.py +271 -0
  439. bootstack/widgets/radiogroup.py +228 -0
  440. bootstack/widgets/scrollbar.py +64 -0
  441. bootstack/widgets/scrollview.py +186 -0
  442. bootstack/widgets/select.py +302 -0
  443. bootstack/widgets/selectbutton.py +182 -0
  444. bootstack/widgets/sidebar_toggle.py +130 -0
  445. bootstack/widgets/sizegrip.py +42 -0
  446. bootstack/widgets/slider.py +408 -0
  447. bootstack/widgets/spinbox.py +147 -0
  448. bootstack/widgets/spinnerfield.py +413 -0
  449. bootstack/widgets/splash.py +367 -0
  450. bootstack/widgets/splitview.py +482 -0
  451. bootstack/widgets/stacks.py +249 -0
  452. bootstack/widgets/statusbar.py +189 -0
  453. bootstack/widgets/tabs.py +425 -0
  454. bootstack/widgets/textarea.py +459 -0
  455. bootstack/widgets/textfield.py +410 -0
  456. bootstack/widgets/theme_toggle.py +92 -0
  457. bootstack/widgets/timefield.py +340 -0
  458. bootstack/widgets/toast.py +314 -0
  459. bootstack/widgets/togglegroup.py +231 -0
  460. bootstack/widgets/toolbar.py +347 -0
  461. bootstack/widgets/tooltip.py +94 -0
  462. bootstack/widgets/tree.py +648 -0
  463. bootstack/widgets/types.py +305 -0
  464. bootstack/widgets/window.py +307 -0
  465. bootstack-0.1.0.dist-info/METADATA +301 -0
  466. bootstack-0.1.0.dist-info/RECORD +471 -0
  467. bootstack-0.1.0.dist-info/WHEEL +5 -0
  468. bootstack-0.1.0.dist-info/entry_points.txt +2 -0
  469. bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
  470. bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
  471. bootstack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,558 @@
1
+ """Navigation providers — what fills a workspace's sidebar panel.
2
+
3
+ A provider owns one workspace's sidebar UI *and* its content-rendering strategy,
4
+ behind a small contract: populate the sidebar, report selections via an
5
+ `on_select` callback, and render content for the active key. The shell hosts one
6
+ provider per workspace, routes selections through `NavModel`, and asks the active
7
+ provider to show content. This is the seam that lets static (`add_page`) and the
8
+ data-bound providers (`list_nav`/`tree_nav`, later) be interchangeable.
9
+
10
+ `StaticProvider` is the first concrete provider: authored items, each with its
11
+ own authored page (N items -> N independent bodies), rendered through a
12
+ `PageStack`.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any, Callable, Protocol, runtime_checkable
18
+
19
+ from bootstack.widgets._impl.primitives.frame import Frame
20
+ from bootstack.widgets._impl.composites.pagestack import PageStack
21
+ from bootstack.widgets._impl.composites.shell.content_host import ContentHost
22
+ from bootstack.widgets._impl.composites.shell.nav_panel import NavPanel
23
+
24
+
25
+ @runtime_checkable
26
+ class NavProvider(Protocol):
27
+ """The contract a workspace's sidebar provider fulfills.
28
+
29
+ A provider populates the sidebar and renders content; selection truth lives
30
+ in the shell's `NavModel`, so the provider only *reports* clicks (via the
31
+ `on_select` callback given to `mount`) and *reflects* the active key (via
32
+ `select_visual` / `show`).
33
+ """
34
+
35
+ supports_compact: bool
36
+ """Whether the sidebar may render icon-only (compact).
37
+
38
+ True only for the static provider, whose items carry authored icons. A
39
+ data-bound list (`list_nav`) or hierarchy (`tree_nav`) cannot meaningfully
40
+ compact, so theirs is False — they are hidden or shown, never icon-only.
41
+ Compaction is also gated by the host to the standalone case (no rail).
42
+ """
43
+
44
+ def mount(
45
+ self,
46
+ sidebar: Frame,
47
+ content: Frame,
48
+ *,
49
+ on_select: Callable[[str], None],
50
+ on_refresh: Callable[[], None] | None = None,
51
+ ) -> None:
52
+ """Build the sidebar UI in `sidebar` and prepare the `content` region.
53
+
54
+ `on_refresh` (optional) is invoked after the provider rebuilds its items
55
+ from a changed source, so the host can reconcile the active selection.
56
+ """
57
+ ...
58
+
59
+ def set_compact(self, compact: bool) -> None:
60
+ """Render the sidebar icon-only (`compact`) or with labels.
61
+
62
+ A no-op for providers where `supports_compact` is False.
63
+ """
64
+ ...
65
+
66
+ def show(self, key: str, data: dict | None = None) -> None:
67
+ """Render content for the selected `key`."""
68
+ ...
69
+
70
+ def select_visual(self, key: str | None) -> None:
71
+ """Reflect the active `key` in the sidebar's selection visual."""
72
+ ...
73
+
74
+ def keys(self) -> tuple[str, ...]:
75
+ """All selectable keys this provider exposes."""
76
+ ...
77
+
78
+
79
+ class StaticProvider:
80
+ """Static provider — authored items, each with its own authored page.
81
+
82
+ Args:
83
+ accent: Accent token for the active nav item.
84
+ """
85
+
86
+ supports_compact = True
87
+
88
+ def __init__(self, *, accent: str | None = None, variant: str = "nav-quiet",
89
+ surface: str = "card", selection: str = "ghost") -> None:
90
+ self._accent = accent
91
+ self._variant = variant
92
+ self._surface = surface
93
+ self._selection = selection
94
+ self._nav: NavPanel | None = None
95
+ self._pages: PageStack | None = None
96
+ self._keys: list[str] = []
97
+
98
+ def mount(
99
+ self,
100
+ sidebar: Frame,
101
+ content: Frame,
102
+ *,
103
+ on_select: Callable[[str], None],
104
+ on_refresh: Callable[[], None] | None = None,
105
+ ) -> None:
106
+ # Static authoring never refreshes from a source; on_refresh is ignored.
107
+ self._nav = NavPanel(
108
+ sidebar, on_select=on_select, accent=self._accent,
109
+ variant=self._variant, surface=self._surface, selection=self._selection,
110
+ )
111
+ self._nav.pack(fill="both", expand=True)
112
+ self._pages = PageStack(content)
113
+ self._pages.pack(fill="both", expand=True)
114
+
115
+ # ----- Authoring -----
116
+
117
+ def add_page(self, key: str, *, text: str = "", icon=None, footer: bool = False) -> Any:
118
+ """Add a nav item and its page; return the page frame."""
119
+ page = self._pages.add(key)
120
+ if footer:
121
+ self._nav.add_footer_item(key, text=text, icon=icon)
122
+ else:
123
+ self._nav.add_item(key, text=text, icon=icon)
124
+ self._keys.append(key)
125
+ return page
126
+
127
+ def add_header(self, text: str) -> Any:
128
+ """Add a plain section-label header."""
129
+ return self._nav.add_header(text)
130
+
131
+ def add_divider(self) -> Any:
132
+ """Add a divider."""
133
+ return self._nav.add_divider()
134
+
135
+ # ----- Provider contract -----
136
+
137
+ def set_compact(self, compact: bool) -> None:
138
+ self._nav.set_compact(compact)
139
+
140
+ def show(self, key: str, data: dict | None = None) -> None:
141
+ self._pages.navigate(key, data=data)
142
+
143
+ def select_visual(self, key: str | None) -> None:
144
+ self._nav.select(key)
145
+
146
+ def keys(self) -> tuple[str, ...]:
147
+ return tuple(self._keys)
148
+
149
+ # ----- Accessors -----
150
+
151
+ @property
152
+ def nav(self) -> NavPanel:
153
+ """The sidebar navigation panel."""
154
+ return self._nav
155
+
156
+ @property
157
+ def pages(self) -> PageStack:
158
+ """The content page deck."""
159
+ return self._pages
160
+
161
+
162
+ class ListNavProvider:
163
+ """Flat, data-bound provider — a `DataSource` drives a sidebar `ListView`.
164
+
165
+ Each record becomes a single-select row (keyed by record id); selecting one
166
+ re-renders a single `@detail` body parameterized by that record — the public
167
+ record dict (the universal `.selection` shape). Rows render the record's
168
+ `text`/`icon`; the recycling `ListView` follows the source live and inherits
169
+ the sidebar surface (no surface plumbing needed).
170
+
171
+ Args:
172
+ source: A `DataSourceProtocol` supplying records (with `text`/`icon`).
173
+ accent: Accent token (the selection control follows it; the row
174
+ selection itself is a neutral wash by default).
175
+ separator: Draw separator lines between rows. Default False (flush).
176
+ chevron: Show a right-pointing chevron on each row. Off by default — a
177
+ disclosure affordance worth adding only when the row visibly drills
178
+ into something.
179
+ """
180
+
181
+ # A label-less data list is a poor nav, so list_nav is hidden-or-shown,
182
+ # never icon-only (spec Revision 2 / R1).
183
+ supports_compact = False
184
+
185
+ def __init__(
186
+ self,
187
+ source: Any,
188
+ *,
189
+ accent: str | None = None,
190
+ separator: bool = False,
191
+ density: str = "default",
192
+ placeholder: str = "Select an item to view",
193
+ chevron: bool = False,
194
+ ) -> None:
195
+ self._source = source
196
+ self._accent = accent
197
+ self._separator = separator
198
+ self._density = density
199
+ self._placeholder = placeholder
200
+ self._chevron = chevron
201
+ self._listview: Any = None
202
+ self._host: ContentHost | None = None
203
+ self._sidebar_host: ContentHost | None = None
204
+ self._detail: Callable[[dict], Any] | None = None
205
+ self._selected: str | None = None
206
+ self._cache: dict[str, dict] = {}
207
+ self._on_select: Callable[[str], None] | None = None
208
+ self._on_refresh: Callable[[], None] | None = None
209
+ self._sub: Any = None
210
+ self._src_sub: Any = None
211
+
212
+ def mount(
213
+ self,
214
+ sidebar: Frame,
215
+ content: Frame,
216
+ *,
217
+ on_select: Callable[[str], None],
218
+ on_refresh: Callable[[], None] | None = None,
219
+ ) -> None:
220
+ from bootstack.widgets.listview import ListView
221
+
222
+ self._on_select = on_select
223
+ self._on_refresh = on_refresh
224
+ self._host = ContentHost(content)
225
+ self._sidebar_host = ContentHost(sidebar)
226
+ self._listview = ListView(
227
+ parent=self._sidebar_host,
228
+ data_source=self._source,
229
+ selection_mode="single",
230
+ show_separators=self._separator,
231
+ show_scrollbar=False, # mousewheel scrolls; no always-on bar in a nav
232
+ show_chevron=self._chevron, # opt-in disclosure affordance
233
+ density=self._density,
234
+ accent=self._accent,
235
+ # When a nav accent is set, the row selection tints to it (else neutral).
236
+ accent_selection=bool(self._accent),
237
+ fill="both",
238
+ expand=True,
239
+ )
240
+ self._cache = self._read()
241
+ self._sub = self._listview.on_select(self._on_listview_select)
242
+ # The ListView refreshes its own rows from the source; we observe too, to
243
+ # re-render the open detail when the selected record changes in place and
244
+ # to let the host reconcile selection on add/remove.
245
+ self._src_sub = self._source.on_change(self._on_source_change)
246
+ # An empty source has no first record to auto-select, so the content area
247
+ # would be blank — show a quiet placeholder until rows arrive.
248
+ if not self._cache:
249
+ self._render_placeholder()
250
+
251
+ def _render_placeholder(self) -> None:
252
+ """Render a centered, muted empty-state in the content region."""
253
+ from bootstack.widgets._impl.primitives.label import Label
254
+
255
+ self._host.clear()
256
+ Label(self._host._internal, text=self._placeholder, accent="muted").pack(expand=True)
257
+
258
+ def set_detail(self, fn: Callable[[dict], Any]) -> None:
259
+ """Register the parameterized detail body builder."""
260
+ self._detail = fn
261
+
262
+ def _read(self) -> dict[str, dict]:
263
+ """Read all current public records keyed by id, in source order."""
264
+ src = self._source
265
+ out: dict[str, dict] = {}
266
+ for raw in src.page_slice(0, src.count):
267
+ rec = src._public_record(raw)
268
+ out[str(rec.get("id"))] = rec
269
+ return out
270
+
271
+ def _on_listview_select(self, _event: Any = None) -> None:
272
+ sel = self._listview.selection
273
+ if not sel:
274
+ return
275
+ self._selected = str(sel.get("id"))
276
+ self._on_select(self._selected)
277
+
278
+ def _on_source_change(self, _event: Any = None) -> None:
279
+ """React to a source change (the ListView refreshes its own rows).
280
+
281
+ Re-render the open detail if the selected record changed in place, and
282
+ let the host reconcile the active selection (e.g. the selected record was
283
+ removed).
284
+ """
285
+ new = self._read()
286
+ key = self._selected
287
+ changed = (
288
+ key is not None and key in new and key in self._cache
289
+ and new[key] != self._cache[key]
290
+ )
291
+ emptied = bool(self._cache) and not new
292
+ self._cache = new
293
+ if changed:
294
+ self.show(key)
295
+ elif emptied:
296
+ # The list drained to nothing — replace the stale detail with the
297
+ # placeholder (the host won't reconcile to a new selection).
298
+ self._selected = None
299
+ self._render_placeholder()
300
+ if self._on_refresh is not None:
301
+ self._on_refresh()
302
+
303
+ def close(self) -> None:
304
+ """Cancel the selection + source subscriptions."""
305
+ for sub in (self._sub, self._src_sub):
306
+ if sub is not None:
307
+ try:
308
+ sub.cancel()
309
+ except Exception:
310
+ pass
311
+ self._sub = self._src_sub = None
312
+
313
+ # ----- Provider contract -----
314
+
315
+ def set_compact(self, compact: bool) -> None:
316
+ # list_nav never compacts (supports_compact is False); see R1.
317
+ pass
318
+
319
+ def show(self, key: str, data: dict | None = None) -> None:
320
+ record = self._cache.get(key)
321
+ if self._detail is not None and record is not None:
322
+ self._host.clear()
323
+ with self._host:
324
+ self._detail(record)
325
+ else:
326
+ self._render_placeholder()
327
+
328
+ def select_visual(self, key: str | None) -> None:
329
+ self._selected = key
330
+ if key is None:
331
+ self._listview.clear_selection()
332
+ return
333
+ record = self._cache.get(key)
334
+ if record is not None:
335
+ self._listview.select_items([record.get("id")])
336
+
337
+ def keys(self) -> tuple[str, ...]:
338
+ return tuple(self._read().keys())
339
+
340
+ # ----- Accessors -----
341
+
342
+ @property
343
+ def nav(self) -> Any:
344
+ """The sidebar list widget (the `ListView`)."""
345
+ return self._listview
346
+
347
+ @property
348
+ def listview(self) -> Any:
349
+ """The sidebar `ListView`."""
350
+ return self._listview
351
+
352
+
353
+ class TreeNavProvider:
354
+ """Hierarchical, data-bound provider — a `Tree` drives the sidebar.
355
+
356
+ Nodes may be declared inline (`nodes=`) or projected from a flat
357
+ adjacency-list `source` (`source=`/`parent_field=`). Selecting a node
358
+ re-renders one `@detail` body with the node normalized to a record dict
359
+ (`text` from the node label, `icon`, the node id, plus the node's data bag) —
360
+ the same universal `.selection` shape `list_nav` uses (spec decision #10).
361
+
362
+ Hierarchy is incompatible with the icon-rail (a branch cannot render as one
363
+ glyph), so this provider does not support compaction.
364
+ """
365
+
366
+ supports_compact = False
367
+
368
+ def __init__(
369
+ self,
370
+ *,
371
+ nodes: list | None = None,
372
+ source: Any = None,
373
+ parent_field: str = "parent_id",
374
+ label_field: str = "name",
375
+ icon_field: str = "icon",
376
+ accent: str | None = None,
377
+ density: str = "default",
378
+ placeholder: str = "Select an item to view",
379
+ ) -> None:
380
+ self._nodes = nodes
381
+ self._source = source
382
+ self._parent_field = parent_field
383
+ self._label_field = label_field
384
+ self._icon_field = icon_field
385
+ self._accent = accent
386
+ self._density = density
387
+ self._placeholder = placeholder
388
+ self._tree: Any = None
389
+ self._host: ContentHost | None = None
390
+ self._sidebar_host: ContentHost | None = None
391
+ self._detail: Callable[[dict], Any] | None = None
392
+ self._on_select: Callable[[str], None] | None = None
393
+ self._nodes_by_key: dict[str, Any] = {}
394
+ self._sub: Any = None
395
+
396
+ def mount(
397
+ self,
398
+ sidebar: Frame,
399
+ content: Frame,
400
+ *,
401
+ on_select: Callable[[str], None],
402
+ on_refresh: Callable[[], None] | None = None,
403
+ ) -> None:
404
+ from bootstack.widgets.tree import Tree
405
+
406
+ self._on_select = on_select
407
+ self._host = ContentHost(content)
408
+ self._sidebar_host = ContentHost(sidebar)
409
+
410
+ tree_kwargs: dict[str, Any] = {
411
+ "parent": self._sidebar_host,
412
+ "selection_mode": "single",
413
+ "show_scrollbar": False,
414
+ "density": self._density,
415
+ "accent": self._accent,
416
+ # When a nav accent is set, the row selection tints to it (else neutral).
417
+ "accent_selection": bool(self._accent),
418
+ "fill": "both",
419
+ "expand": True,
420
+ }
421
+ if self._source is not None:
422
+ tree_kwargs.update(
423
+ data_source=self._source,
424
+ parent_field=self._parent_field,
425
+ label_field=self._label_field,
426
+ icon_field=self._icon_field,
427
+ )
428
+ elif self._nodes is not None:
429
+ tree_kwargs["nodes"] = self._nodes
430
+ self._tree = Tree(**tree_kwargs)
431
+ self._sub = self._tree.on_select(self._on_tree_select)
432
+ # A tree opens with nothing selected, so the content area would be blank.
433
+ # Show a quiet placeholder until the first node is picked.
434
+ self._render_placeholder()
435
+
436
+ def _render_placeholder(self) -> None:
437
+ """Render a centered, muted empty-state in the content region."""
438
+ from bootstack.widgets._impl.primitives.label import Label
439
+
440
+ self._host.clear()
441
+ Label(self._host._internal, text=self._placeholder, accent="muted").pack(expand=True)
442
+
443
+ def set_detail(self, fn: Callable[[dict], Any]) -> None:
444
+ """Register the parameterized detail body builder."""
445
+ self._detail = fn
446
+
447
+ # ----- Selection plumbing (node identity -> string key) -----
448
+
449
+ def _key_for(self, node: Any) -> str:
450
+ key = str(id(node))
451
+ self._nodes_by_key[key] = node
452
+ return key
453
+
454
+ def _record(self, node: Any, key: str) -> dict:
455
+ rec = dict(getattr(node, "data", {}) or {})
456
+ rec["text"] = node.label
457
+ rec["icon"] = node.icon
458
+ rec["id"] = key
459
+ return rec
460
+
461
+ def _on_tree_select(self, _event: Any) -> None:
462
+ node = self._tree.selection
463
+ if node is None:
464
+ return
465
+ self._on_select(self._key_for(node))
466
+
467
+ # ----- Provider contract -----
468
+
469
+ def set_compact(self, compact: bool) -> None:
470
+ # A hierarchy cannot render as icons; tree_nav never compacts (see R1).
471
+ pass
472
+
473
+ def show(self, key: str, data: dict | None = None) -> None:
474
+ node = self._nodes_by_key.get(key)
475
+ if self._detail is not None and node is not None:
476
+ self._host.clear()
477
+ with self._host:
478
+ self._detail(self._record(node, key))
479
+ else:
480
+ self._render_placeholder()
481
+
482
+ def select_visual(self, key: str | None) -> None:
483
+ node = self._nodes_by_key.get(key) if key is not None else None
484
+ if node is not None:
485
+ self._tree.select(node)
486
+
487
+ def keys(self) -> tuple[str, ...]:
488
+ # Trees register keys lazily as nodes are selected (nodes may be created
489
+ # on expand). Empty until the first selection — so the shell never
490
+ # auto-selects a tree (a tree opens with nothing selected).
491
+ return tuple(self._nodes_by_key)
492
+
493
+ def close(self) -> None:
494
+ """Cancel the tree selection subscription."""
495
+ if self._sub is not None:
496
+ try:
497
+ self._sub.cancel()
498
+ except Exception:
499
+ pass
500
+ self._sub = None
501
+
502
+ # ----- Accessors -----
503
+
504
+ @property
505
+ def tree(self) -> Any:
506
+ """The sidebar `Tree` widget."""
507
+ return self._tree
508
+
509
+
510
+ class CustomProvider:
511
+ """Escape-hatch provider — a blank sidebar container the user fills.
512
+
513
+ There is no cascade and no per-page memory: the workspace hands back the
514
+ sidebar container (via `panel`) and its content region (via `Workspace.content`)
515
+ and the user drives content-switching themselves. A custom panel cannot
516
+ collapse to icons (its content is arbitrary), so `supports_compact` is False —
517
+ it only hides.
518
+ """
519
+
520
+ supports_compact = False
521
+
522
+ def __init__(self) -> None:
523
+ self._container: Frame | None = None
524
+ self._content: Frame | None = None
525
+
526
+ def mount(
527
+ self,
528
+ sidebar: Frame,
529
+ content: Frame,
530
+ *,
531
+ on_select: Callable[[str], None],
532
+ on_refresh: Callable[[], None] | None = None,
533
+ ) -> None:
534
+ # No selection model: on_select/on_refresh are unused.
535
+ self._container = Frame(sidebar)
536
+ self._container.pack(fill="both", expand=True)
537
+ self._content = content
538
+
539
+ # ----- Provider contract (mostly inert) -----
540
+
541
+ def set_compact(self, compact: bool) -> None:
542
+ pass
543
+
544
+ def show(self, key: str, data: dict | None = None) -> None:
545
+ pass
546
+
547
+ def select_visual(self, key: str | None) -> None:
548
+ pass
549
+
550
+ def keys(self) -> tuple[str, ...]:
551
+ return ()
552
+
553
+ # ----- Accessors -----
554
+
555
+ @property
556
+ def container(self) -> Frame:
557
+ """The blank sidebar container to fill."""
558
+ return self._container
@@ -0,0 +1,117 @@
1
+ """The workspace rail — the tier-1 icon switcher.
2
+
3
+ A vertical strip of icon-only items, one per workspace, with a pinned footer
4
+ (for global workspaces like Settings). The rail is a single-select **radio
5
+ group**: every item is a `RadioToggle` sharing one `Signal`, so single-select is
6
+ the radio machinery itself — no hand-rolled selection state.
7
+
8
+ The items use the `rail` Toolbutton style variant (the VS Code activity-bar
9
+ treatment: muted glyph -> full-strength glyph + accent indicator bar on the
10
+ chrome surface). Selection truth still lives in the shell's `NavModel`; the rail
11
+ reports clicks and reflects the active workspace by setting its shared signal.
12
+
13
+ The VS Code gesture (click the active icon -> hide the sidebar) needs *every*
14
+ click — including a re-click on the already-selected item, which a radio does not
15
+ report as a change — so each item also has a direct click binding that routes to
16
+ the shell.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Callable
22
+
23
+ from bootstack.signals import Signal
24
+ from bootstack.widgets._impl.primitives.frame import Frame
25
+ from bootstack.widgets._impl.primitives.radiotoggle import RadioToggle
26
+
27
+
28
+ class Rail(Frame):
29
+ """A single-select icon rail of workspaces (a radio-toggle group).
30
+
31
+ Args:
32
+ master: Parent widget (the rail region).
33
+ on_select: Called with a workspace key when its icon is clicked.
34
+ accent: Accent token for the active item.
35
+ """
36
+
37
+ # Rail glyphs read as standalone, categorical marks — distinctly larger than
38
+ # the inline sidebar icons. Decision #13's rail_icon_size.
39
+ DEFAULT_ICON_SIZE = 28
40
+
41
+ def __init__(
42
+ self,
43
+ master,
44
+ *,
45
+ on_select: Callable[[str], None],
46
+ accent: str | None = None,
47
+ icon_size: int | None = None,
48
+ labels: bool = False,
49
+ surface: str = "chrome",
50
+ ) -> None:
51
+ super().__init__(master, surface=surface)
52
+ self._on_select = on_select
53
+ self._accent = accent
54
+ self._surface = surface
55
+ self._labels = labels
56
+ self._icon_size = icon_size if icon_size is not None else self.DEFAULT_ICON_SIZE
57
+ # Shared selection signal: the value is the active workspace key, so the
58
+ # matching RadioToggle renders selected (radio single-select). A Signal is
59
+ # typed by its initial value, so use an empty-string sentinel for "none"
60
+ # (no workspace key is empty) rather than None.
61
+ self._signal: Signal = Signal("")
62
+ self._items: dict[str, RadioToggle] = {}
63
+
64
+ self._footer = Frame(self, surface=surface)
65
+ self._footer.pack(side="bottom", fill="x")
66
+ self._main = Frame(self, surface=surface)
67
+ self._main.pack(side="top", fill="both", expand=True)
68
+
69
+ def add_workspace(self, key: str, *, icon=None, text: str = "", footer: bool = False) -> RadioToggle:
70
+ """Add a workspace icon to the rail (or its footer)."""
71
+ if key in self._items:
72
+ raise ValueError(f"duplicate rail key: {key!r}")
73
+ parent = self._footer if footer else self._main
74
+ # Labeled rail: a small caption under the icon (compound='top'); otherwise
75
+ # icon-only (relies on tooltips for discoverability — a future add).
76
+ toggle_kwargs: dict = dict(
77
+ value=key,
78
+ signal=self._signal,
79
+ icon=self._icon_spec(icon),
80
+ accent=self._accent,
81
+ variant="rail",
82
+ surface=self._surface,
83
+ )
84
+ if self._labels:
85
+ toggle_kwargs.update(text=text, compound="top", style_options={"labeled": True})
86
+ else:
87
+ toggle_kwargs.update(icon_only=True)
88
+ item = RadioToggle(parent, **toggle_kwargs)
89
+ item.pack(fill="x")
90
+ # Catch ALL clicks (including a re-click on the active item) so the shell
91
+ # can run the VS Code gesture; the radio itself only reports changes.
92
+ item.bind("<Button-1>", lambda _e, k=key: self._on_select(k), add="+")
93
+ self._items[key] = item
94
+ return item
95
+
96
+ def _icon_spec(self, icon):
97
+ """Normalize an icon name/spec to carry the rail icon size."""
98
+ if icon is None:
99
+ return None
100
+ if isinstance(icon, dict):
101
+ spec = dict(icon)
102
+ spec.setdefault("size", self._icon_size)
103
+ return spec
104
+ return {"name": icon, "size": self._icon_size}
105
+
106
+ def select(self, key: str | None) -> None:
107
+ """Set the visual selection to `key` (or clear with `None`)."""
108
+ self._signal.set(key if key is not None else "")
109
+
110
+ @property
111
+ def selected(self) -> str | None:
112
+ """The selected workspace key, or `None`."""
113
+ return self._signal() or None
114
+
115
+ def keys(self) -> tuple[str, ...]:
116
+ """All workspace keys in insertion order."""
117
+ return tuple(self._items)