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,447 @@
1
+ """Style resolution and widget integration for bootstack.
2
+
3
+ Resolves accent/variant tokens into TTK style names and wires those styles
4
+ onto widgets by overriding their constructors.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Optional
10
+
11
+ from bootstack._runtime.app import get_app_settings, has_current_app
12
+ from bootstack.style.token_maps import (COLOR_TOKENS, CONTAINER_CLASSES, ORIENT_CLASSES, WIDGET_CLASS_MAP)
13
+
14
+
15
+
16
+ def to_pascal_case(s: str) -> str:
17
+ """Convert dash-separated string to PascalCase.
18
+
19
+ Examples:
20
+ 'context-check' -> 'ContextCheck'
21
+ 'outline' -> 'Outline'
22
+ 'solid' -> 'Solid'
23
+ """
24
+ return ''.join(part.capitalize() for part in s.split('-'))
25
+
26
+
27
+ def from_pascal_case(s: str) -> str:
28
+ """Convert PascalCase string to dash-separated lowercase.
29
+
30
+ Examples:
31
+ 'ContextCheck' -> 'context-check'
32
+ 'Outline' -> 'outline'
33
+ 'Solid' -> 'solid'
34
+ """
35
+ import re
36
+ # Insert dash before uppercase letters (except at start), then lowercase
37
+ return re.sub(r'(?<!^)(?=[A-Z])', '-', s).lower()
38
+
39
+
40
+ def generate_ttk_style_name(
41
+ accent: Optional[str],
42
+ variant: Optional[str],
43
+ widget_class: str,
44
+ custom_prefix: Optional[str] = None,
45
+ orient: Optional[str] = None,
46
+ ) -> str:
47
+ """Generate TTK style name from parsed components.
48
+
49
+ Returns style name in format: [custom_prefix].[accent].[Variant].[Orient].[Widget]
50
+ """
51
+ parts = []
52
+
53
+ if custom_prefix:
54
+ parts.append(custom_prefix)
55
+ if accent:
56
+ parts.append(accent)
57
+ if variant:
58
+ parts.append(to_pascal_case(variant))
59
+ if orient:
60
+ parts.append(normalize_orientation(orient))
61
+ parts.append(widget_class)
62
+ return '.'.join(parts)
63
+
64
+
65
+ def normalize_orientation(orient: str):
66
+ """Normalize TTK style orientation."""
67
+ if orient.lower().startswith('v'):
68
+ return 'Vertical'
69
+ else:
70
+ return 'Horizontal'
71
+
72
+
73
+
74
+ def extract_orient_from_style(ttk_style: str):
75
+ """Extract orientation from TTK style name."""
76
+ if 'horizontal' in ttk_style.lower():
77
+ return 'Horizontal'
78
+ elif 'vertical' in ttk_style.lower():
79
+ return 'Vertical'
80
+ else:
81
+ return None
82
+
83
+
84
+ def extract_accent_from_style(ttk_style: str, default: str = 'primary') -> str:
85
+ """Extract accent token from TTK style name, including modifiers like [subtle]."""
86
+ parts = ttk_style.split('.')
87
+
88
+ # Skip custom prefix if present (e.g., bs[hash] or custom_xyz)
89
+ if parts and (parts[0].startswith('bs[') or parts[0].startswith('custom_')):
90
+ parts = parts[1:]
91
+
92
+ for part in parts:
93
+ part_lower = part.lower()
94
+ # Check exact match first
95
+ if part_lower in COLOR_TOKENS:
96
+ return part_lower
97
+ # Check if base accent (before modifier) is an accent token
98
+ # e.g., 'primary[subtle]' -> 'primary'
99
+ if '[' in part_lower:
100
+ base_accent = part_lower.split('[')[0]
101
+ if base_accent in COLOR_TOKENS:
102
+ return part_lower # Return full token with modifier
103
+
104
+ return default
105
+
106
+
107
+ def extract_variant_from_style(ttk_style: str, widget_class: str = None) -> Optional[str]:
108
+ """Extract variant name from TTK style name.
109
+
110
+ Args:
111
+ ttk_style: The TTK style name to parse.
112
+ widget_class: Optional widget class from winfo_class() to exclude from parsing.
113
+
114
+ Returns:
115
+ Variant name in dash-separated lowercase format (e.g., 'context-check')
116
+ """
117
+ parts = ttk_style.split('.')
118
+
119
+ # Skip custom prefix if present (e.g., bs[hash] or custom_xyz)
120
+ if parts and (parts[0].startswith('bs[') or parts[0].startswith('custom_')):
121
+ parts = parts[1:]
122
+
123
+ # Build set of class-related parts to skip (e.g., 'Expander', 'TLabel')
124
+ class_parts = set()
125
+ if widget_class:
126
+ class_parts.update(widget_class.split('.'))
127
+
128
+ for part in parts:
129
+ part_lower = part.lower()
130
+ # Skip color tokens (including those with modifiers like 'primary[subtle]')
131
+ if part_lower in COLOR_TOKENS:
132
+ continue
133
+ if '[' in part_lower:
134
+ base_color = part_lower.split('[')[0]
135
+ if base_color in COLOR_TOKENS:
136
+ continue
137
+ # Skip known class parts from widget_class
138
+ if part in class_parts:
139
+ continue
140
+ # Skip standard ttk class names (TLabel, TButton, etc.)
141
+ if part.startswith('T'):
142
+ continue
143
+ # Skip orientation parts
144
+ if part in ('Horizontal', 'Vertical'):
145
+ continue
146
+ # Found a variant - convert from PascalCase to dash-separated
147
+ return from_pascal_case(part)
148
+
149
+ return None
150
+
151
+
152
+ def extract_widget_class_from_style(ttk_style: str) -> Optional[str]:
153
+ """Extract widget class from TTK style name."""
154
+ parts = ttk_style.split('.')
155
+
156
+ for part in parts:
157
+ if part.startswith('T'):
158
+ return part
159
+
160
+ return None
161
+
162
+
163
+ class StyleResolver:
164
+ """Widget integration layer for the style system.
165
+
166
+ Wires bootstack into tkinter/ttk by overriding widget constructors to
167
+ resolve `accent`/`variant` tokens into TTK style names.
168
+ """
169
+
170
+ @staticmethod
171
+ def create_ttk_style(
172
+ widget_class: str,
173
+ style_options: Optional[dict] = None,
174
+ *,
175
+ accent: Optional[str] = None,
176
+ variant: Optional[str] = None,
177
+ ) -> str:
178
+ """Create or get TTK style name for a widget.
179
+
180
+ Args:
181
+ widget_class: TTK widget class (e.g., "TButton")
182
+ style_options: Custom style options dict
183
+ accent: Accent token (e.g., "success", "primary[subtle]")
184
+ variant: Variant name (e.g., "outline", "solid")
185
+
186
+ Returns:
187
+ Generated TTK style name
188
+ """
189
+ from bootstack.style.style_builder_ttk import StyleBuilderTtk
190
+
191
+ # If no accent and no variant, return base widget class
192
+ if not accent and not variant:
193
+ return widget_class
194
+
195
+ # Initialize style_options to empty dict if None
196
+ if style_options is None:
197
+ style_options = {}
198
+
199
+ surface = style_options.get("surface")
200
+
201
+ builder_variant = variant if variant is not None else \
202
+ StyleBuilderTtk.get_default_variant(widget_class)
203
+
204
+ custom_prefix = None
205
+
206
+ if style_options.keys() or surface != 'background':
207
+ import hashlib
208
+ import json
209
+ options_str = json.dumps(style_options, sort_keys=True)
210
+ options_hash = hashlib.md5(options_str.encode()).hexdigest()[:8]
211
+ custom_prefix = f"bs[{options_hash}]"
212
+
213
+ ttk_style = generate_ttk_style_name(
214
+ accent=accent,
215
+ variant=variant,
216
+ widget_class=widget_class,
217
+ custom_prefix=custom_prefix,
218
+ orient=style_options.get('orient'),
219
+ )
220
+
221
+ from bootstack.style.style import get_style
222
+ style = get_style()
223
+
224
+ style.create_style(
225
+ widget_class=widget_class,
226
+ variant=builder_variant,
227
+ ttk_style=ttk_style,
228
+ accent=accent,
229
+ options=style_options
230
+ )
231
+
232
+ return ttk_style
233
+
234
+ @staticmethod
235
+ def override_ttk_widget_constructor(func):
236
+ """Override ttk widget __init__ to accept accent and variant parameters."""
237
+
238
+ def __init__wrapper(self, *args, **kwargs):
239
+
240
+ # Extract accent/variant parameters
241
+ accent = kwargs.pop("accent", None)
242
+ if accent == 'default':
243
+ accent = None
244
+ variant = kwargs.pop("variant", None)
245
+
246
+ had_style_kwarg = 'style' in kwargs
247
+
248
+ style_options = kwargs.pop("style_options", {})
249
+ inherit_surface = kwargs.pop('inherit_surface', None)
250
+ surface_token = kwargs.pop('surface', None)
251
+ input_bg_token = kwargs.pop('input_background', None)
252
+
253
+ # Extract ttk_class for style lookup (doesn't affect widget's actual class_)
254
+ # This allows custom style builders without affecting bindtags
255
+ ttk_class = kwargs.pop('ttk_class', None)
256
+
257
+ func(self, *args, **kwargs) # the actual widget constructor
258
+
259
+ # Use ttk_class for style lookup if provided, otherwise use widget's actual class
260
+ widget_class = self.winfo_class()
261
+ style_class = ttk_class or widget_class
262
+
263
+ # ===== Surface color inheritance =====
264
+
265
+ # Surface inheritance is always on in this framework (the former
266
+ # `inherit_surface_color` app toggle was removed); an explicit
267
+ # per-call `inherit_surface` kwarg can still override locally.
268
+ if inherit_surface is None:
269
+ inherit_surface = True
270
+
271
+ if hasattr(self, 'master') and self.master is not None:
272
+ parent_surface_token = getattr(self.master, '_surface', 'content')
273
+ else:
274
+ parent_surface_token = 'content'
275
+
276
+ if surface_token:
277
+ effective_surface_token = surface_token
278
+ elif inherit_surface:
279
+ effective_surface_token = parent_surface_token
280
+ else:
281
+ effective_surface_token = 'content'
282
+
283
+ # container widgets can take their surface color from the accent param
284
+ # Use style_class so custom ttk_class like 'Field' can opt out of this behavior
285
+ if accent and style_class in CONTAINER_CLASSES and surface_token is None:
286
+ effective_surface_token = accent
287
+
288
+ # cache the surface color for child components
289
+ setattr(self, '_surface', effective_surface_token)
290
+ if effective_surface_token != 'content' and effective_surface_token is not None:
291
+ style_options.setdefault('surface', effective_surface_token)
292
+
293
+ # ===== Input background inheritance =====
294
+
295
+ if hasattr(self, 'master') and self.master is not None:
296
+ parent_input_bg = getattr(self.master, '_input_background', None)
297
+ else:
298
+ parent_input_bg = None
299
+
300
+ if input_bg_token:
301
+ effective_input_bg = input_bg_token
302
+ elif inherit_surface and parent_input_bg:
303
+ effective_input_bg = parent_input_bg
304
+ else:
305
+ effective_input_bg = None
306
+
307
+ setattr(self, '_input_background', effective_input_bg)
308
+ if effective_input_bg:
309
+ style_options.setdefault('input_background', effective_input_bg)
310
+
311
+ # ==== Orientation =====
312
+
313
+ # handle widgets with orientation
314
+ if widget_class in ORIENT_CLASSES:
315
+ orient = str(self.cget('orient'))
316
+ style_options.setdefault('orient', orient)
317
+
318
+ # ==== Create actual ttk style & assign to widget =====
319
+
320
+ if (accent or variant) and style_class:
321
+
322
+ ttk_style = StyleResolver.create_ttk_style(
323
+ widget_class=style_class,
324
+ style_options=style_options,
325
+ accent=accent,
326
+ variant=variant,
327
+ )
328
+ self.configure(style=ttk_style)
329
+
330
+ elif style_class and not had_style_kwarg:
331
+ from bootstack.style.style_builder_ttk import StyleBuilderTtk
332
+ from bootstack.style.style import get_style
333
+
334
+ default_variant = StyleBuilderTtk.get_default_variant(style_class)
335
+
336
+ if StyleBuilderTtk.has_builder(style_class, default_variant):
337
+
338
+ # Build options first so we can decide if a custom bs[...] prefix is needed
339
+ custom_prefix = None
340
+ if style_options.keys():
341
+ import hashlib
342
+ import json
343
+ options_str = json.dumps(style_options, sort_keys=True)
344
+ options_hash = hashlib.md5(options_str.encode()).hexdigest()[:8]
345
+ custom_prefix = f"bs[{options_hash}]"
346
+
347
+ ttk_style = generate_ttk_style_name(
348
+ accent=None,
349
+ variant=default_variant,
350
+ widget_class=style_class,
351
+ custom_prefix=custom_prefix,
352
+ )
353
+
354
+ style_instance = get_style()
355
+ if style_instance is not None:
356
+ style_instance.create_style(
357
+ widget_class=style_class,
358
+ variant=default_variant,
359
+ ttk_style=ttk_style,
360
+ options=style_options,
361
+ )
362
+ self.configure(style=ttk_style)
363
+ else:
364
+ self.configure(style=style_class)
365
+
366
+ # Store accent, variant, ttk_class, and style_options for later retrieval
367
+ setattr(self, '_accent', accent)
368
+ setattr(self, '_variant', variant)
369
+ setattr(self, '_ttk_class', ttk_class)
370
+ setattr(self, '_style_options', style_options)
371
+
372
+ return __init__wrapper
373
+
374
+ @staticmethod
375
+ def override_tk_widget_constructor(func):
376
+ """Override Tk widget __init__ to apply theme background when autostyle=True."""
377
+
378
+ def __init__wrapper(self, *args, **kwargs):
379
+
380
+ # capture bootstrap arguments
381
+ auto_style = kwargs.pop("autostyle", True)
382
+
383
+ # Class-level opt-out: widget manages its own styling
384
+ if auto_style and not getattr(type(self), '_bs_autostyle', True):
385
+ auto_style = False
386
+
387
+ # Parent-level opt-out: parent manages styling for its children
388
+ if auto_style:
389
+ master = args[0] if args else kwargs.get('master')
390
+ if master is not None and not getattr(master, '_bs_autostyle', True):
391
+ auto_style = False
392
+
393
+ inherit_surface = kwargs.pop('inherit_surface', None)
394
+ surface_token = kwargs.pop('surface', None)
395
+
396
+ # Outside an active App there is no theme/settings context — build
397
+ # the widget as plain Tk (no autostyle) instead of raising.
398
+ if not has_current_app():
399
+ func(self, *args, **kwargs)
400
+ setattr(self, '_surface', surface_token or 'content')
401
+ return
402
+
403
+ if inherit_surface is None:
404
+ inherit_surface = True
405
+
406
+ func(self, *args, **kwargs) # the actual constructor
407
+
408
+ # ===== Surface color inheritance =====
409
+
410
+ if hasattr(self, 'master') and self.master is not None:
411
+ parent_surface_token = getattr(self.master, '_surface', 'content')
412
+ else:
413
+ parent_surface_token = 'content'
414
+
415
+ # An explicit `surface=` always wins; otherwise inherit from the
416
+ # parent (the default) or fall back to 'content'. (This mirrors the
417
+ # ttk autostyle path — the Tk path previously let inheritance clobber
418
+ # an explicit surface, freezing e.g. a meter canvas to a resolved hex.)
419
+ if surface_token is None:
420
+ surface_token = parent_surface_token if inherit_surface else 'content'
421
+
422
+ setattr(self, '_surface', surface_token)
423
+
424
+ if not auto_style:
425
+ return
426
+
427
+ # ==== Apply the initial theme background =====
428
+ # `_surface` (the token, set above) is what the unified theme walk
429
+ # later re-resolves on a theme change; `_bs_theme_version` lets the
430
+ # walk tell whether this widget already matches the current theme.
431
+
432
+ from bootstack.style.style import get_style
433
+ style = get_style()
434
+ surface = getattr(self, '_surface', 'content')
435
+ style._get_tk_builder().call_builder(self, surface=surface)
436
+ self._bs_theme_version = style._theme_version
437
+
438
+ return __init__wrapper
439
+
440
+
441
+ __all__ = [
442
+ 'generate_ttk_style_name',
443
+ 'extract_accent_from_style',
444
+ 'extract_variant_from_style',
445
+ 'extract_widget_class_from_style',
446
+ 'StyleResolver',
447
+ ]
@@ -0,0 +1,245 @@
1
+ """Public `Theme` object for declaring and installing color themes.
2
+
3
+ A theme is a *family*: you declare the semantic accent colors once (as the
4
+ midpoint `[500]` of each ramp) plus a light and/or dark background block, and the
5
+ family generates both mode variants. Each accent expands into a 50–950 tint/shade
6
+ spectrum; the framework selects the right step per mode (a darker solid on light,
7
+ a brighter one on dark) so you never hand-write per-mode shades.
8
+
9
+ ```python
10
+ from bootstack.style import Theme
11
+
12
+ Theme(
13
+ name="bootstrap",
14
+ primary="#0d6efd", success="#198754", danger="#dc3545",
15
+ info="#0dcaf0", warning="#ffc107", # the [500] anchors
16
+ light=dict(background="#ffffff", foreground="#212529"),
17
+ dark=dict(background="#212529", foreground="#f8f9fa"),
18
+ ).install() # registers bootstrap-light + -dark
19
+
20
+ bs.set_theme("bootstrap-dark") # activate a generated variant
21
+ ```
22
+
23
+ Installing registers the generated variants (`<name>-light` / `<name>-dark`);
24
+ activation is a separate `set_theme` call (or `install(activate=True)`). Themes
25
+ can be declared and installed at module level before an app exists — color
26
+ resolution is deferred until activation.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ from dataclasses import dataclass, field, fields, replace
31
+ from typing import Literal
32
+
33
+ from bootstack._core.exceptions import ThemeError
34
+ from bootstack.style.theme_provider import register_theme
35
+
36
+ ThemeMode = Literal["light", "dark"]
37
+
38
+ # Registry of declared theme FAMILIES by name (e.g. "bootstrap" -> Theme), so a
39
+ # theme can be derived from an existing one by name. Built-ins register
40
+ # themselves via install() at startup; see _resolve_base().
41
+ _FAMILIES: dict[str, "Theme"] = {}
42
+
43
+ # Accent roles that take a `[500]` anchor and generate their own ramp.
44
+ _ACCENT_ROLES = ("primary", "success", "info", "warning", "danger")
45
+
46
+ # Which ramp step a SOLID (button fill, etc.) uses per mode. On light a darker
47
+ # step reads against white; on dark a brighter step reads against the dark
48
+ # background. `warning`/`info` stay at the bright `[500]` on light — they can't
49
+ # carry white text at any shade (they use dark on-color), so darkening them only
50
+ # mutes them.
51
+ _SOLID_STOP = {
52
+ "light": {"primary": 600, "success": 600, "danger": 600, "info": 500, "warning": 500},
53
+ "dark": {"primary": 400, "success": 400, "danger": 400, "info": 400, "warning": 400},
54
+ }
55
+ # Neutral (gray) step used for the `secondary` role per mode.
56
+ _SECONDARY_STOP = {"light": 700, "dark": 400}
57
+
58
+
59
+ def _default_display_name(name: str) -> str:
60
+ return name.replace("-", " ").replace("_", " ").title()
61
+
62
+
63
+ @dataclass
64
+ class Theme:
65
+ """A color theme family declared in code.
66
+
67
+ Declare the semantic accent colors as `[500]` anchors plus a `light` and/or
68
+ `dark` block (`background`/`foreground`). Each accent generates a 50–950
69
+ ramp; the framework picks the per-mode step for solids, washes, borders, and
70
+ emphasis text. `install()` registers the generated `<name>-light` and
71
+ `<name>-dark` variants.
72
+
73
+ Args:
74
+ name: Family name; generated variants are `<name>-light` / `<name>-dark`.
75
+ primary: Primary accent color (the `[500]` ramp midpoint), as hex.
76
+ success: Success accent color (hex).
77
+ info: Info accent color (hex).
78
+ warning: Warning accent color (hex).
79
+ danger: Danger accent color (hex).
80
+ secondary: Optional colored secondary accent (hex). When omitted,
81
+ `secondary` derives from the neutral ramp.
82
+ neutral: Gray base for the neutral ramp (borders, muted text, secondary).
83
+ light: `{'background': ..., 'foreground': ...}` for the light variant, or
84
+ None to skip it.
85
+ dark: `{'background': ..., 'foreground': ...}` for the dark variant.
86
+ surfaces: Optional surface overrides — either a flat
87
+ `{'chrome': '#...'}` (both modes) or per-mode
88
+ `{'light': {...}, 'dark': {...}}`. Pins surfaces the auto-derivation
89
+ can't produce well (e.g. chrome on a very-dark background).
90
+ display_name: Human label base; the mode is appended (` Light`/` Dark`).
91
+ white: Reference white. Defaults to `'#ffffff'`.
92
+ black: Reference black. Defaults to `'#000000'`.
93
+ """
94
+
95
+ name: str
96
+
97
+ primary: str | None = None
98
+ success: str | None = None
99
+ info: str | None = None
100
+ warning: str | None = None
101
+ danger: str | None = None
102
+ secondary: str | None = None
103
+
104
+ neutral: str = "#adb5bd"
105
+
106
+ light: dict[str, str] | None = None
107
+ dark: dict[str, str] | None = None
108
+
109
+ surfaces: dict | None = None
110
+ display_name: str | None = None
111
+ white: str = "#ffffff"
112
+ black: str = "#000000"
113
+
114
+ def _schema(self, mode: ThemeMode) -> dict | None:
115
+ block = self.light if mode == "light" else self.dark
116
+ if block is None:
117
+ return None
118
+ if "background" not in block or "foreground" not in block:
119
+ raise ThemeError(
120
+ f"Theme '{self.name}' {mode} block must define both "
121
+ f"'background' and 'foreground'."
122
+ )
123
+
124
+ solid = _SOLID_STOP[mode]
125
+ shades: dict[str, str] = {"gray": self.neutral}
126
+ semantic: dict[str, str] = {}
127
+
128
+ for role in _ACCENT_ROLES:
129
+ anchor = getattr(self, role)
130
+ if anchor:
131
+ shades[role] = anchor
132
+ semantic[role] = f"{role}[{solid[role]}]"
133
+
134
+ if self.secondary:
135
+ shades["secondary"] = self.secondary
136
+ semantic["secondary"] = f"secondary[{solid['primary']}]"
137
+ else:
138
+ semantic["secondary"] = f"gray[{_SECONDARY_STOP[mode]}]"
139
+
140
+ base_label = self.display_name or _default_display_name(self.name)
141
+ schema: dict = {
142
+ "name": f"{self.name}-{mode}",
143
+ "display_name": f"{base_label} {mode.title()}",
144
+ "mode": mode,
145
+ "foreground": block["foreground"],
146
+ "background": block["background"],
147
+ "white": self.white,
148
+ "black": self.black,
149
+ "shades": shades,
150
+ "semantic": semantic,
151
+ }
152
+ if self.surfaces:
153
+ override = self.surfaces.get(mode, None)
154
+ if override is None and not ({"light", "dark"} & set(self.surfaces)):
155
+ override = self.surfaces # flat dict → applies to both modes
156
+ if override:
157
+ schema["surfaces"] = dict(override)
158
+ return schema
159
+
160
+ @classmethod
161
+ def from_existing(cls, base: "str | Theme", *, name: str, **overrides) -> "Theme":
162
+ """Derive a new theme from an existing one, overriding only some tokens.
163
+
164
+ Every token you don't override is inherited from `base`, so you can brand
165
+ a built-in family by changing just its `primary` (and any others you want):
166
+
167
+ Theme.from_existing("bootstrap", name="acme", primary="#ff5722").install()
168
+ bs.set_theme("acme-light")
169
+
170
+ Args:
171
+ base: The theme to build on — a built-in or already-installed family
172
+ name (e.g. `'bootstrap'`), or a `Theme` instance. Required.
173
+ name: Family name for the derived theme; its `install()` registers
174
+ `<name>-light` / `<name>-dark`. Required.
175
+ **overrides: Any `Theme` token to replace (`primary`, `neutral`,
176
+ `light`, `dark`, `surfaces`, `display_name`, ...).
177
+
178
+ Returns:
179
+ A new `Theme`; call `install()` on it to register its variants.
180
+
181
+ Raises:
182
+ ThemeError: If `base` names a theme family that is not registered, or
183
+ an override names a token that is not a `Theme` field.
184
+ """
185
+ source = base if isinstance(base, Theme) else _resolve_base(base)
186
+ unknown = set(overrides) - {f.name for f in fields(cls)}
187
+ if unknown:
188
+ raise ThemeError(
189
+ f"Unknown theme token(s): {', '.join(sorted(unknown))}."
190
+ )
191
+ return replace(source, name=name, **overrides)
192
+
193
+ def variants(self) -> list[dict]:
194
+ """Return the generated per-mode schema dicts (light first, then dark)."""
195
+ return [s for s in (self._schema("light"), self._schema("dark")) if s]
196
+
197
+ def install(self, *, activate: bool | str = False) -> Theme:
198
+ """Register the generated variants so they can be activated by name.
199
+
200
+ Args:
201
+ activate: True activates the light variant; pass a variant name
202
+ (e.g. `'bootstrap-dark'`) to activate that one.
203
+
204
+ Returns:
205
+ This theme, to allow `theme = Theme(...).install()`.
206
+ """
207
+ schemas = self.variants()
208
+ if not schemas:
209
+ raise ThemeError(
210
+ f"Theme '{self.name}' defines neither a 'light' nor a 'dark' block."
211
+ )
212
+ for schema in schemas:
213
+ register_theme(schema["name"], schema)
214
+ _FAMILIES[self.name] = self
215
+ if activate:
216
+ from bootstack.style.style import set_theme
217
+ target = activate if isinstance(activate, str) else schemas[0]["name"]
218
+ set_theme(target)
219
+ return self
220
+
221
+ def __repr__(self) -> str:
222
+ modes = "+".join(m for m, b in (("light", self.light), ("dark", self.dark)) if b)
223
+ return f"<Theme {self.name} ({modes or 'empty'})>"
224
+
225
+
226
+ def _resolve_base(name: str) -> Theme:
227
+ """Resolve a theme-family name to its `Theme`, seeding built-ins lazily."""
228
+ if name not in _FAMILIES:
229
+ # Seed the built-in families on demand (lazy import avoids a cycle).
230
+ try:
231
+ from bootstack.style.themes import BUILTIN_THEMES
232
+ for theme in BUILTIN_THEMES:
233
+ _FAMILIES.setdefault(theme.name, theme)
234
+ except Exception:
235
+ pass
236
+ source = _FAMILIES.get(name)
237
+ if source is None and (name.endswith("-light") or name.endswith("-dark")):
238
+ source = _FAMILIES.get(name.rsplit("-", 1)[0])
239
+ if source is None:
240
+ known = ", ".join(sorted(_FAMILIES)) or "none"
241
+ raise ThemeError(
242
+ f"Unknown base theme '{name}'. Pass a registered family name "
243
+ f"(known: {known}) or a Theme instance."
244
+ )
245
+ return source