From e5f742e867c7a16b4d119e22afb09c404074435d Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Fri, 20 Mar 2026 19:35:23 -0700 Subject: [PATCH 01/31] Web Backend CDN Shoelace 2.3.0 > WebAwesome 3.3.1 Use the WebAwesome shoelace.css compatibility theme to preserve existing CSS variable names and minimize the scope of this migration. --- web/src/toga_web/deploy/inserts/index.html~head | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/src/toga_web/deploy/inserts/index.html~head b/web/src/toga_web/deploy/inserts/index.html~head index 9131aa3ac3..74cfcdbef7 100644 --- a/web/src/toga_web/deploy/inserts/index.html~head +++ b/web/src/toga_web/deploy/inserts/index.html~head @@ -1,3 +1,4 @@ - - - + + + + From f3e14a5ba7c2551d9911a922404a988530dcc401 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Fri, 20 Mar 2026 19:42:26 -0700 Subject: [PATCH 02/31] Migrate web widgets from Shoelace to WebAwesome - Rename sl-* tags to wa-* (e.g., sl-button -> wa-button) - Rename sl-range to wa-slider (component was redesigned and renamed) - Unprefix custom events: sl-change -> change, sl-input -> input - Replace divider vertical attribute with orientation="vertical" --- web/src/toga_web/widgets/activityindicator.py | 2 +- web/src/toga_web/widgets/button.py | 2 +- web/src/toga_web/widgets/dateinput.py | 6 +++--- web/src/toga_web/widgets/divider.py | 6 +++--- web/src/toga_web/widgets/progressbar.py | 2 +- web/src/toga_web/widgets/selection.py | 10 +++++----- web/src/toga_web/widgets/slider.py | 6 +++--- web/src/toga_web/widgets/switch.py | 4 ++-- web/src/toga_web/widgets/textinput.py | 6 +++--- web/src/toga_web/widgets/timeinput.py | 6 +++--- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/web/src/toga_web/widgets/activityindicator.py b/web/src/toga_web/widgets/activityindicator.py index 9366342146..5266b78a94 100644 --- a/web/src/toga_web/widgets/activityindicator.py +++ b/web/src/toga_web/widgets/activityindicator.py @@ -3,7 +3,7 @@ class ActivityIndicator(Widget): def create(self): - self.native = self._create_native_widget("sl-spinner") + self.native = self._create_native_widget("wa-spinner") self.stop() # Actions diff --git a/web/src/toga_web/widgets/button.py b/web/src/toga_web/widgets/button.py index 07e003ccc8..59ac26eb01 100644 --- a/web/src/toga_web/widgets/button.py +++ b/web/src/toga_web/widgets/button.py @@ -5,7 +5,7 @@ class Button(Widget): def create(self): - self.native = self._create_native_widget("sl-button") + self.native = self._create_native_widget("wa-button") self.native.addEventListener("click", create_proxy(self.dom_click)) def dom_click(self, event): diff --git a/web/src/toga_web/widgets/dateinput.py b/web/src/toga_web/widgets/dateinput.py index 990c0f95ff..1252ea77f6 100644 --- a/web/src/toga_web/widgets/dateinput.py +++ b/web/src/toga_web/widgets/dateinput.py @@ -15,12 +15,12 @@ def native_date(py_date_obj): class DateInput(Widget): def create(self): - self.native = self._create_native_widget("sl-input") + self.native = self._create_native_widget("wa-input") self.native.type = "date" self.native.value = native_date(datetime.date.today()) - self.native.addEventListener("sl-change", create_proxy(self.dom_sl_change)) + self.native.addEventListener("change", create_proxy(self.dom_change)) - def dom_sl_change(self, event): + def dom_change(self, event): try: input_date = py_date(self.native.value) except Exception: diff --git a/web/src/toga_web/widgets/divider.py b/web/src/toga_web/widgets/divider.py index 9f01865058..25ab0dd18a 100644 --- a/web/src/toga_web/widgets/divider.py +++ b/web/src/toga_web/widgets/divider.py @@ -5,13 +5,13 @@ class Divider(Widget): def create(self): - self.native = self._create_native_widget("sl-divider") + self.native = self._create_native_widget("wa-divider") def get_direction(self): return self.interface.direction def set_direction(self, value): if value is Direction.VERTICAL: - self.native.setAttribute("vertical", "") + self.native.setAttribute("orientation", "vertical") else: - self.native.removeAttribute("vertical") + self.native.removeAttribute("orientation") diff --git a/web/src/toga_web/widgets/progressbar.py b/web/src/toga_web/widgets/progressbar.py index de65bf9be3..d690c84619 100644 --- a/web/src/toga_web/widgets/progressbar.py +++ b/web/src/toga_web/widgets/progressbar.py @@ -3,7 +3,7 @@ class ProgressBar(Widget): def create(self): - self.native = self._create_native_widget("sl-progress-bar") + self.native = self._create_native_widget("wa-progress-bar") self._is_running = False self._value = 0 self._max = 0 diff --git a/web/src/toga_web/widgets/selection.py b/web/src/toga_web/widgets/selection.py index ed62a21d9c..ff49375c62 100644 --- a/web/src/toga_web/widgets/selection.py +++ b/web/src/toga_web/widgets/selection.py @@ -7,10 +7,10 @@ class Selection(Widget): def create(self): - self.native = self._create_native_widget("sl-select") - self.native.addEventListener("sl-change", create_proxy(self.dom_sl_change)) + self.native = self._create_native_widget("wa-select") + self.native.addEventListener("change", create_proxy(self.dom_change)) - def dom_sl_change(self, event): + def dom_change(self, event): self.interface.on_change() # Alias for backwards compatibility: @@ -45,7 +45,7 @@ def insert(self, index, item): def source_insert(self, *, index, item): display_text = self.interface._title_for_item(item) - option = self._create_native_widget("sl-option") + option = self._create_native_widget("wa-option") option.value = str(index) option.textContent = display_text if self.native.value == "": @@ -62,7 +62,7 @@ def get_selected_index(self): def select_item(self, index, item): self.native.value = str(index) - self.native.dispatchEvent(CustomEvent.new("sl-change")) + self.native.dispatchEvent(CustomEvent.new("change")) def rehint(self): pass diff --git a/web/src/toga_web/widgets/slider.py b/web/src/toga_web/widgets/slider.py index 7249261154..b485ab4146 100644 --- a/web/src/toga_web/widgets/slider.py +++ b/web/src/toga_web/widgets/slider.py @@ -5,14 +5,14 @@ class Slider(Widget): def create(self): - self.native = self._create_native_widget("sl-range") - self.native.addEventListener("sl-input", create_proxy(self.dom_sl_input)) + self.native = self._create_native_widget("wa-slider") + self.native.addEventListener("input", create_proxy(self.dom_input)) self.native.addEventListener( "pointerdown", create_proxy(self.dom_onpointerdown) ) self.native.addEventListener("pointerup", create_proxy(self.dom_onpointerup)) - def dom_sl_input(self, event): + def dom_input(self, event): self.interface.value = float(self.native.value) if self.interface.on_change: self.interface.on_change() diff --git a/web/src/toga_web/widgets/switch.py b/web/src/toga_web/widgets/switch.py index 558f837661..6ab2095897 100644 --- a/web/src/toga_web/widgets/switch.py +++ b/web/src/toga_web/widgets/switch.py @@ -5,8 +5,8 @@ class Switch(Widget): def create(self): - self.native = self._create_native_widget("sl-switch") - self.native.addEventListener("sl-change", create_proxy(self.dom_onchange)) + self.native = self._create_native_widget("wa-switch") + self.native.addEventListener("change", create_proxy(self.dom_onchange)) def dom_onchange(self, event): self.interface.on_change() diff --git a/web/src/toga_web/widgets/textinput.py b/web/src/toga_web/widgets/textinput.py index 1760983208..b8a566a740 100644 --- a/web/src/toga_web/widgets/textinput.py +++ b/web/src/toga_web/widgets/textinput.py @@ -6,15 +6,15 @@ class TextInput(Widget): def create(self): self._return_listener = None - self.native = self._create_native_widget("sl-input") + self.native = self._create_native_widget("wa-input") self.native.onkeyup = self.dom_onkeyup self.native.addEventListener("onkeyup", create_proxy(self.dom_onkeyup)) - self.native.addEventListener("sl-change", create_proxy(self.dom_sl_change)) + self.native.addEventListener("change", create_proxy(self.dom_change)) def dom_onkeyup(self, event): self.interface.on_change() - def dom_sl_change(self, event): + def dom_change(self, event): self.interface.on_confirm() def set_readonly(self, value): diff --git a/web/src/toga_web/widgets/timeinput.py b/web/src/toga_web/widgets/timeinput.py index 807876f5ed..c63740380b 100644 --- a/web/src/toga_web/widgets/timeinput.py +++ b/web/src/toga_web/widgets/timeinput.py @@ -27,13 +27,13 @@ def native_time(py_time): class TimeInput(Widget): def create(self): - self.native = self._create_native_widget("sl-input") + self.native = self._create_native_widget("wa-input") self.native.setAttribute("step", "1") # force seconds to show self.native.type = "time" self.native.value = native_time(datetime.datetime.now()) - self.native.addEventListener("sl-change", create_proxy(self.dom_sl_change)) + self.native.addEventListener("change", create_proxy(self.dom_change)) - def dom_sl_change(self, event): + def dom_change(self, event): try: input_time = py_time(self.native.value) except Exception: From 01109ef0257f282d8ce5a6d5d783392c6da088d2 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Fri, 20 Mar 2026 19:47:23 -0700 Subject: [PATCH 03/31] Migrate app dialogs and menus to WebAwesome API - Dialog: replace .show()/.hide() with .open property (https://www.webawesome.com/docs/components/dialog) - Menu: remove sl-menu wrapper; wa-dropdown now takes wa-dropdown-item children directly (https://www.webawesome.com/docs/components/dropdown) - Button variant: primary -> brand (https://www.webawesome.com/docs/components/button) --- web/src/toga_web/app.py | 14 +++++++------- web/src/toga_web/dialogs.py | 8 ++++---- web/src/toga_web/window.py | 11 ++++------- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/web/src/toga_web/app.py b/web/src/toga_web/app.py index 49a1f7e547..46d9654158 100644 --- a/web/src/toga_web/app.py +++ b/web/src/toga_web/app.py @@ -95,10 +95,10 @@ def show_about_dialog(self): copyright = f"\n\nCopyright © {self.interface.author}" close_button = create_element( - "sl-button", slot="footer", variant="primary", content="Ok" + "wa-button", slot="footer", variant="brand", content="Ok" ) about_dialog = create_element( - "sl-dialog", + "wa-dialog", id="toga-about-dialog", label="About", children=[ @@ -111,7 +111,7 @@ def show_about_dialog(self): # Create a button handler to capture the close, # and destroy the dialog def dialog_close(event): - about_dialog.hide() + about_dialog.open = False self.native.removeChild(about_dialog) close_button.onclick = dialog_close @@ -119,15 +119,15 @@ def dialog_close(event): # Add the dialog to the DOM. self.native.appendChild(about_dialog) - # If this is the first time a dialog is being shown, the Shoelace + # If this is the first time a dialog is being shown, the WebAwesome # autoloader needs to construct the Dialog custom element. We can't # display the dialog until that element has been fully loaded and - # constructed. Only show the dialog when the promise of + # constructed. Only show the dialog when the promise of # element construction has been fulfilled. def show_dialog(promise): - about_dialog.show() + about_dialog.open = True - js.customElements.whenDefined("sl-dialog").then(show_dialog) + js.customElements.whenDefined("wa-dialog").then(show_dialog) ###################################################################### # Cursor control diff --git a/web/src/toga_web/dialogs.py b/web/src/toga_web/dialogs.py index 45b8e29dc5..ff8bda48cd 100644 --- a/web/src/toga_web/dialogs.py +++ b/web/src/toga_web/dialogs.py @@ -11,7 +11,7 @@ def show(self, host_window, future): # modal dialogs. toga.App.app._impl.native.appendChild(self.native) - self.native.show() + self.native.open = True else: # Dialog doesn't have an implementation self.future.set_result(None) @@ -21,7 +21,7 @@ class InfoDialog(BaseDialog): def __init__(self, title, message): super().__init__() self.native = create_element( - "sl-dialog", + "wa-dialog", id="toga-info-dialog", label=title, children=[ @@ -32,7 +32,7 @@ def __init__(self, title, message): def create_buttons(self): close_button = create_element( - "sl-button", slot="footer", variant="primary", content="Ok" + "wa-button", slot="footer", variant="brand", content="Ok" ) # Handle the close of the dialog close_button.onclick = self.dialog_close @@ -40,7 +40,7 @@ def create_buttons(self): return [close_button] def dialog_close(self, event): - self.native.hide() + self.native.open = False self.native.parentElement.removeChild(self.native) self.future.set_result(None) diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index 9dbfdc9771..9a277255c7 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -156,7 +156,7 @@ def get_image_data(self): class MainWindow(Window): def _create_submenu(self, group, items): submenu = create_element( - "sl-dropdown", + "wa-dropdown", children=[ create_element( "span", @@ -165,11 +165,8 @@ def _create_submenu(self, group, items): slot="trigger", content=group.text, ), - create_element( - "sl-menu", - children=items, - ), - ], + ] + + items, ) return submenu @@ -187,7 +184,7 @@ def create_menus(self): submenu = self._menu_groups.setdefault(cmd.group, []) menu_item = create_element( - "sl-menu-item", + "wa-dropdown-item", content=cmd.text, disabled=not cmd.enabled, ) From b6f51bc35db1a9af63f5f7cb934491e648dc841b Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Fri, 20 Mar 2026 19:51:25 -0700 Subject: [PATCH 04/31] Update doco references from Shoelace to WebAwesome --- docs/en/how-to/contribute/what/implement-feature.md | 2 +- docs/en/reference/platforms/web.md | 4 ++-- docs/spelling_wordlist | 1 + web/README.md | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/en/how-to/contribute/what/implement-feature.md b/docs/en/how-to/contribute/what/implement-feature.md index 31fd8a7058..f74dab67f3 100644 --- a/docs/en/how-to/contribute/what/implement-feature.md +++ b/docs/en/how-to/contribute/what/implement-feature.md @@ -10,7 +10,7 @@ Don't worry if you don't have an idea for a new feature, there are [plenty of fe ### Implement a platform native widget -If the core library already specifies an interface for a widget, but the widget isn't implemented on your platform of choice, implement that interface. The [API reference table](/reference/api/index.md) table can show you the widgets that are missing on various platforms. You can also look for log messages in a running app (or the direct `factory.not_implemented()` function calls that produce those log messages). At present, Qt, the Web and Textual backends have the most missing widgets. If you have web skills, or would like to learn more about [PyScript](https://pyscript.net) and [Shoelace](https://shoelace.style), the web backend could be a good place to contribute; if you'd like to learn more about terminal applications or the [Textual](https://textual.textualize.io) API, contributing to the Textual backend could be a good place for you to contribute. If you’re interested in desktop GUI development or want to deepen your understanding of the Qt framework, contributing to the [Qt](https://www.qt.io/product/framework) backend is a great option. +If the core library already specifies an interface for a widget, but the widget isn't implemented on your platform of choice, implement that interface. The [API reference table](/reference/api/index.md) table can show you the widgets that are missing on various platforms. You can also look for log messages in a running app (or the direct `factory.not_implemented()` function calls that produce those log messages). At present, Qt, the Web and Textual backends have the most missing widgets. If you have web skills, or would like to learn more about [PyScript](https://pyscript.net) and [WebAwesome](https://www.webawesome.com), the web backend could be a good place to contribute; if you'd like to learn more about terminal applications or the [Textual](https://textual.textualize.io) API, contributing to the Textual backend could be a good place for you to contribute. If you’re interested in desktop GUI development or want to deepen your understanding of the Qt framework, contributing to the [Qt](https://www.qt.io/product/framework) backend is a great option. Alternatively, if there's a widget that doesn't exist, propose an interface design, and implement it for at least one platform. You may find [this presentation by BeeWare emeritus team member Dan Yeaw](https://www.youtube.com/watch?v=sWt_sEZUiY8) helpful. This talk gives an architectural overview of Toga, as well as providing a guide to the process of adding new widgets. diff --git a/docs/en/reference/platforms/web.md b/docs/en/reference/platforms/web.md index 3e7de424a7..982542a29f 100644 --- a/docs/en/reference/platforms/web.md +++ b/docs/en/reference/platforms/web.md @@ -9,7 +9,7 @@ The Web backend is currently proof-of-concept only. Most widgets have not been i ## Prerequisites -`toga-web` will run in any modern browser. It requires [PyScript](https://pyscript.net) 2023.05.01 or newer, and [Shoelace v2.3](https://shoelace.style). +`toga-web` will run in any modern browser. It requires [PyScript](https://pyscript.net) 2023.05.01 or newer, and [WebAwesome v3.3](https://www.webawesome.com). ## Installation @@ -19,6 +19,6 @@ The recommended approach for deploying `toga-web` is to use [Briefcase](https:// ## Implementation details -The `toga-web` backend is implemented using [Shoelace web components](https://shoelace.style). +The `toga-web` backend is implemented using [WebAwesome web components](https://www.webawesome.com). The DOM is accessed using [PyScript](https://pyscript.net). diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index 22987d5b12..64b88930ce 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -230,6 +230,7 @@ viewport watchOS Wayland WebKit +WebAwesome WebView WebViews WGS diff --git a/web/README.md b/web/README.md index 0fed84757f..2468674513 100644 --- a/web/README.md +++ b/web/README.md @@ -4,7 +4,7 @@ [![BSD-3-Clause License](https://img.shields.io/pypi/l/toga-web.svg)](https://github.com/beeware/toga-web/blob/main/LICENSE) [![Project status](https://img.shields.io/pypi/status/toga-web.svg)](https://pypi.python.org/pypi/toga-web) -A backend for the [Toga widget toolkit](https://beeware.org/toga) on web platforms. It uses [Shoelace](https://shoelace.style) to provide web components. +A backend for the [Toga widget toolkit](https://beeware.org/toga) on web platforms. It uses [WebAwesome](https://www.webawesome.com) to provide web components. This package isn't much use by itself; it needs to be combined with [the core Toga library](https://pypi.python.org/pypi/toga-core). From 50f234c6aa504b06358d86de3f1df986d2fd458f Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Fri, 20 Mar 2026 19:56:24 -0700 Subject: [PATCH 05/31] Update examples for WebAwesome --- examples/activityindicator/pyproject.toml | 2 +- examples/beeliza/pyproject.toml | 2 +- examples/box/pyproject.toml | 2 +- examples/button/pyproject.toml | 2 +- examples/canvas/pyproject.toml | 2 +- examples/colors/pyproject.toml | 2 +- examples/command/pyproject.toml | 2 +- examples/date_and_time/pyproject.toml | 2 +- examples/detailedlist/pyproject.toml | 2 +- examples/dialogs/pyproject.toml | 2 +- examples/divider/pyproject.toml | 2 +- examples/examples_overview/pyproject.toml | 2 +- examples/focus/pyproject.toml | 2 +- examples/font/pyproject.toml | 2 +- examples/font_size/pyproject.toml | 2 +- examples/handlers/pyproject.toml | 2 +- examples/imageview/pyproject.toml | 2 +- examples/layout/pyproject.toml | 2 +- examples/mapview/pyproject.toml | 2 +- examples/multilinetextinput/pyproject.toml | 2 +- examples/numberinput/pyproject.toml | 2 +- examples/optioncontainer/pyproject.toml | 2 +- examples/passwordinput/pyproject.toml | 2 +- examples/positron-django/pyproject.toml | 2 +- examples/positron-static/pyproject.toml | 2 +- examples/progressbar/pyproject.toml | 2 +- examples/resize/pyproject.toml | 2 +- examples/scrollcontainer/pyproject.toml | 2 +- examples/selection/pyproject.toml | 2 +- examples/simpleapp/pyproject.toml | 2 +- examples/slider/pyproject.toml | 2 +- examples/splitcontainer/pyproject.toml | 2 +- examples/switch_demo/pyproject.toml | 2 +- examples/table/pyproject.toml | 2 +- examples/table_columns/pyproject.toml | 2 +- examples/table_source/pyproject.toml | 2 +- examples/textinput/pyproject.toml | 2 +- examples/tree/pyproject.toml | 2 +- examples/tree_source/pyproject.toml | 2 +- examples/tutorial0/pyproject.toml | 2 +- examples/tutorial0/sandbox.html | 40 +++++++++------------- examples/tutorial1/pyproject.toml | 2 +- examples/tutorial2/pyproject.toml | 2 +- examples/tutorial3/pyproject.toml | 2 +- examples/tutorial4/pyproject.toml | 2 +- examples/webview/pyproject.toml | 2 +- examples/window/pyproject.toml | 2 +- 47 files changed, 63 insertions(+), 69 deletions(-) diff --git a/examples/activityindicator/pyproject.toml b/examples/activityindicator/pyproject.toml index 20df77c913..c3a3a1afd3 100644 --- a/examples/activityindicator/pyproject.toml +++ b/examples/activityindicator/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/beeliza/pyproject.toml b/examples/beeliza/pyproject.toml index 660bf77bab..a56d67403a 100644 --- a/examples/beeliza/pyproject.toml +++ b/examples/beeliza/pyproject.toml @@ -63,4 +63,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/box/pyproject.toml b/examples/box/pyproject.toml index 33e8444084..3dd41f418d 100644 --- a/examples/box/pyproject.toml +++ b/examples/box/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/button/pyproject.toml b/examples/button/pyproject.toml index f3d84016d5..15b2cc89cb 100644 --- a/examples/button/pyproject.toml +++ b/examples/button/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/canvas/pyproject.toml b/examples/canvas/pyproject.toml index e30db33767..2c011ca011 100644 --- a/examples/canvas/pyproject.toml +++ b/examples/canvas/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/colors/pyproject.toml b/examples/colors/pyproject.toml index 2648afa409..effef0e2cb 100644 --- a/examples/colors/pyproject.toml +++ b/examples/colors/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/command/pyproject.toml b/examples/command/pyproject.toml index c038212a66..6ef8b6870c 100644 --- a/examples/command/pyproject.toml +++ b/examples/command/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/date_and_time/pyproject.toml b/examples/date_and_time/pyproject.toml index 9be0da8e51..72a2df427d 100644 --- a/examples/date_and_time/pyproject.toml +++ b/examples/date_and_time/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/detailedlist/pyproject.toml b/examples/detailedlist/pyproject.toml index ab60bdaed7..e00f88403d 100644 --- a/examples/detailedlist/pyproject.toml +++ b/examples/detailedlist/pyproject.toml @@ -63,4 +63,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/dialogs/pyproject.toml b/examples/dialogs/pyproject.toml index 8e56caefa1..d8eb144f10 100644 --- a/examples/dialogs/pyproject.toml +++ b/examples/dialogs/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/divider/pyproject.toml b/examples/divider/pyproject.toml index cd38047ea0..652ee3a111 100644 --- a/examples/divider/pyproject.toml +++ b/examples/divider/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/examples_overview/pyproject.toml b/examples/examples_overview/pyproject.toml index 71f7eb2c76..12a3bd080c 100644 --- a/examples/examples_overview/pyproject.toml +++ b/examples/examples_overview/pyproject.toml @@ -63,4 +63,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/focus/pyproject.toml b/examples/focus/pyproject.toml index b300e43b02..96cff7cc9a 100644 --- a/examples/focus/pyproject.toml +++ b/examples/focus/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/font/pyproject.toml b/examples/font/pyproject.toml index daefdae20d..ecc4a4a797 100644 --- a/examples/font/pyproject.toml +++ b/examples/font/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/font_size/pyproject.toml b/examples/font_size/pyproject.toml index 8acb5e46b5..0fde87c961 100644 --- a/examples/font_size/pyproject.toml +++ b/examples/font_size/pyproject.toml @@ -61,4 +61,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/handlers/pyproject.toml b/examples/handlers/pyproject.toml index ac8d7af3ef..4f28ae8417 100644 --- a/examples/handlers/pyproject.toml +++ b/examples/handlers/pyproject.toml @@ -63,4 +63,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/imageview/pyproject.toml b/examples/imageview/pyproject.toml index 02fb3761a5..1d19328ed4 100644 --- a/examples/imageview/pyproject.toml +++ b/examples/imageview/pyproject.toml @@ -67,4 +67,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/layout/pyproject.toml b/examples/layout/pyproject.toml index affcd1de32..49aef35007 100644 --- a/examples/layout/pyproject.toml +++ b/examples/layout/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/mapview/pyproject.toml b/examples/mapview/pyproject.toml index b58032e715..6d801046ec 100644 --- a/examples/mapview/pyproject.toml +++ b/examples/mapview/pyproject.toml @@ -63,4 +63,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/multilinetextinput/pyproject.toml b/examples/multilinetextinput/pyproject.toml index 9fd51a19b0..b44ba919be 100644 --- a/examples/multilinetextinput/pyproject.toml +++ b/examples/multilinetextinput/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/numberinput/pyproject.toml b/examples/numberinput/pyproject.toml index 00bfb92880..3127a10c99 100644 --- a/examples/numberinput/pyproject.toml +++ b/examples/numberinput/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/optioncontainer/pyproject.toml b/examples/optioncontainer/pyproject.toml index 6557cee93b..48f19ae88a 100644 --- a/examples/optioncontainer/pyproject.toml +++ b/examples/optioncontainer/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/passwordinput/pyproject.toml b/examples/passwordinput/pyproject.toml index 12694699ba..8957912f3f 100644 --- a/examples/passwordinput/pyproject.toml +++ b/examples/passwordinput/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ '../../web', ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/positron-django/pyproject.toml b/examples/positron-django/pyproject.toml index 885f350f6c..b2ea786c2b 100644 --- a/examples/positron-django/pyproject.toml +++ b/examples/positron-django/pyproject.toml @@ -64,4 +64,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/positron-static/pyproject.toml b/examples/positron-static/pyproject.toml index 30fdfab242..37e7c813b4 100644 --- a/examples/positron-static/pyproject.toml +++ b/examples/positron-static/pyproject.toml @@ -63,4 +63,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/progressbar/pyproject.toml b/examples/progressbar/pyproject.toml index 7b2ac0fd8e..7d0dcaaeea 100644 --- a/examples/progressbar/pyproject.toml +++ b/examples/progressbar/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/resize/pyproject.toml b/examples/resize/pyproject.toml index 79a12637d4..8225f11cb2 100644 --- a/examples/resize/pyproject.toml +++ b/examples/resize/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/scrollcontainer/pyproject.toml b/examples/scrollcontainer/pyproject.toml index bd453e3ece..500126987d 100644 --- a/examples/scrollcontainer/pyproject.toml +++ b/examples/scrollcontainer/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/selection/pyproject.toml b/examples/selection/pyproject.toml index 09000f207f..d48a606688 100644 --- a/examples/selection/pyproject.toml +++ b/examples/selection/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/simpleapp/pyproject.toml b/examples/simpleapp/pyproject.toml index 811ae7066e..121961e430 100644 --- a/examples/simpleapp/pyproject.toml +++ b/examples/simpleapp/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/slider/pyproject.toml b/examples/slider/pyproject.toml index dc2bd6a075..598ea03987 100644 --- a/examples/slider/pyproject.toml +++ b/examples/slider/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/splitcontainer/pyproject.toml b/examples/splitcontainer/pyproject.toml index 6d8593e581..3fb3f38af0 100644 --- a/examples/splitcontainer/pyproject.toml +++ b/examples/splitcontainer/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/switch_demo/pyproject.toml b/examples/switch_demo/pyproject.toml index 5464e582eb..dda2283dba 100644 --- a/examples/switch_demo/pyproject.toml +++ b/examples/switch_demo/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/table/pyproject.toml b/examples/table/pyproject.toml index 9b29b34093..50b1831c55 100644 --- a/examples/table/pyproject.toml +++ b/examples/table/pyproject.toml @@ -61,4 +61,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/table_columns/pyproject.toml b/examples/table_columns/pyproject.toml index 629306d972..cc2aac113d 100644 --- a/examples/table_columns/pyproject.toml +++ b/examples/table_columns/pyproject.toml @@ -65,4 +65,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/table_source/pyproject.toml b/examples/table_source/pyproject.toml index 57d32e6ad9..2c207f2883 100644 --- a/examples/table_source/pyproject.toml +++ b/examples/table_source/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/textinput/pyproject.toml b/examples/textinput/pyproject.toml index 88b18555b5..d115e16ecc 100644 --- a/examples/textinput/pyproject.toml +++ b/examples/textinput/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/tree/pyproject.toml b/examples/tree/pyproject.toml index 08ed3f3285..bbc2c9d7a1 100644 --- a/examples/tree/pyproject.toml +++ b/examples/tree/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/tree_source/pyproject.toml b/examples/tree_source/pyproject.toml index e4a99cb9c5..71d1805741 100644 --- a/examples/tree_source/pyproject.toml +++ b/examples/tree_source/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/tutorial0/pyproject.toml b/examples/tutorial0/pyproject.toml index 19825aacf5..3baf2a41e7 100644 --- a/examples/tutorial0/pyproject.toml +++ b/examples/tutorial0/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/tutorial0/sandbox.html b/examples/tutorial0/sandbox.html index 70f1cce6c4..076b89c0f5 100644 --- a/examples/tutorial0/sandbox.html +++ b/examples/tutorial0/sandbox.html @@ -11,9 +11,9 @@ - - - + + + -
+
First App logo
- Hello world + Hello world
diff --git a/examples/tutorial1/pyproject.toml b/examples/tutorial1/pyproject.toml index 91776ca945..124d1f688d 100644 --- a/examples/tutorial1/pyproject.toml +++ b/examples/tutorial1/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/tutorial2/pyproject.toml b/examples/tutorial2/pyproject.toml index 487bb2188d..b8ed1fbbc6 100644 --- a/examples/tutorial2/pyproject.toml +++ b/examples/tutorial2/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/tutorial3/pyproject.toml b/examples/tutorial3/pyproject.toml index 949d1882f7..7126d3441f 100644 --- a/examples/tutorial3/pyproject.toml +++ b/examples/tutorial3/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/tutorial4/pyproject.toml b/examples/tutorial4/pyproject.toml index a9f738bc0e..9737cee3f9 100644 --- a/examples/tutorial4/pyproject.toml +++ b/examples/tutorial4/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/webview/pyproject.toml b/examples/webview/pyproject.toml index b66dde4faa..e535b05fa7 100644 --- a/examples/webview/pyproject.toml +++ b/examples/webview/pyproject.toml @@ -66,4 +66,4 @@ chaquopy.defaultConfig.staticProxy("toga_android.widgets.internal.webview") requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" diff --git a/examples/window/pyproject.toml b/examples/window/pyproject.toml index bb85778178..0e7fb54400 100644 --- a/examples/window/pyproject.toml +++ b/examples/window/pyproject.toml @@ -62,4 +62,4 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "Shoelace v2.3" +style_framework = "WebAwesome v3.3" From 7c2bbf63a35ad0a6e6904ae0e398d94918072821 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Fri, 20 Mar 2026 19:56:45 -0700 Subject: [PATCH 06/31] Add changelog --- changes/4227.feature.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/4227.feature.md diff --git a/changes/4227.feature.md b/changes/4227.feature.md new file mode 100644 index 0000000000..23506498f5 --- /dev/null +++ b/changes/4227.feature.md @@ -0,0 +1 @@ +The web backend now uses WebAwesome instead of Shoelace to provide web components. From bfa301ad9566d3b85bb093dfb27ae0752ef9a5b2 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Mon, 23 Mar 2026 16:52:37 -0700 Subject: [PATCH 07/31] Remove deprecated style_framework from example apps Briefcase no longer uses the style_framework key; the web backend now loads WebAwesome via insert files instead. Refs #3822. --- examples/activityindicator/pyproject.toml | 1 - examples/beeliza/pyproject.toml | 1 - examples/box/pyproject.toml | 1 - examples/button/pyproject.toml | 1 - examples/canvas/pyproject.toml | 1 - examples/colors/pyproject.toml | 1 - examples/command/pyproject.toml | 1 - examples/date_and_time/pyproject.toml | 1 - examples/detailedlist/pyproject.toml | 1 - examples/dialogs/pyproject.toml | 1 - examples/divider/pyproject.toml | 1 - examples/examples_overview/pyproject.toml | 1 - examples/focus/pyproject.toml | 1 - examples/font/pyproject.toml | 1 - examples/font_size/pyproject.toml | 1 - examples/handlers/pyproject.toml | 1 - examples/imageview/pyproject.toml | 1 - examples/layout/pyproject.toml | 1 - examples/mapview/pyproject.toml | 1 - examples/multilinetextinput/pyproject.toml | 1 - examples/numberinput/pyproject.toml | 1 - examples/optioncontainer/pyproject.toml | 1 - examples/passwordinput/pyproject.toml | 1 - examples/positron-django/pyproject.toml | 1 - examples/positron-static/pyproject.toml | 1 - examples/progressbar/pyproject.toml | 1 - examples/resize/pyproject.toml | 1 - examples/scrollcontainer/pyproject.toml | 1 - examples/selection/pyproject.toml | 1 - examples/simpleapp/pyproject.toml | 1 - examples/slider/pyproject.toml | 1 - examples/splitcontainer/pyproject.toml | 1 - examples/switch_demo/pyproject.toml | 1 - examples/table/pyproject.toml | 1 - examples/table_columns/pyproject.toml | 1 - examples/table_source/pyproject.toml | 1 - examples/textinput/pyproject.toml | 1 - examples/tree/pyproject.toml | 1 - examples/tree_source/pyproject.toml | 1 - examples/tutorial0/pyproject.toml | 1 - examples/tutorial1/pyproject.toml | 1 - examples/tutorial2/pyproject.toml | 1 - examples/tutorial3/pyproject.toml | 1 - examples/tutorial4/pyproject.toml | 1 - examples/webview/pyproject.toml | 1 - examples/window/pyproject.toml | 1 - 46 files changed, 46 deletions(-) diff --git a/examples/activityindicator/pyproject.toml b/examples/activityindicator/pyproject.toml index c3a3a1afd3..f82b8c49d7 100644 --- a/examples/activityindicator/pyproject.toml +++ b/examples/activityindicator/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/beeliza/pyproject.toml b/examples/beeliza/pyproject.toml index a56d67403a..7a3997f9de 100644 --- a/examples/beeliza/pyproject.toml +++ b/examples/beeliza/pyproject.toml @@ -63,4 +63,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/box/pyproject.toml b/examples/box/pyproject.toml index 3dd41f418d..8764ca06a2 100644 --- a/examples/box/pyproject.toml +++ b/examples/box/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/button/pyproject.toml b/examples/button/pyproject.toml index 15b2cc89cb..6364fefc52 100644 --- a/examples/button/pyproject.toml +++ b/examples/button/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/canvas/pyproject.toml b/examples/canvas/pyproject.toml index 2c011ca011..773cf47036 100644 --- a/examples/canvas/pyproject.toml +++ b/examples/canvas/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/colors/pyproject.toml b/examples/colors/pyproject.toml index effef0e2cb..e4b6624ef7 100644 --- a/examples/colors/pyproject.toml +++ b/examples/colors/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/command/pyproject.toml b/examples/command/pyproject.toml index 6ef8b6870c..1b14422af6 100644 --- a/examples/command/pyproject.toml +++ b/examples/command/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/date_and_time/pyproject.toml b/examples/date_and_time/pyproject.toml index 72a2df427d..07f1c354ff 100644 --- a/examples/date_and_time/pyproject.toml +++ b/examples/date_and_time/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/detailedlist/pyproject.toml b/examples/detailedlist/pyproject.toml index e00f88403d..1937edcc07 100644 --- a/examples/detailedlist/pyproject.toml +++ b/examples/detailedlist/pyproject.toml @@ -63,4 +63,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/dialogs/pyproject.toml b/examples/dialogs/pyproject.toml index d8eb144f10..156ab87707 100644 --- a/examples/dialogs/pyproject.toml +++ b/examples/dialogs/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/divider/pyproject.toml b/examples/divider/pyproject.toml index 652ee3a111..10695c8026 100644 --- a/examples/divider/pyproject.toml +++ b/examples/divider/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/examples_overview/pyproject.toml b/examples/examples_overview/pyproject.toml index 12a3bd080c..ef38d7015c 100644 --- a/examples/examples_overview/pyproject.toml +++ b/examples/examples_overview/pyproject.toml @@ -63,4 +63,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/focus/pyproject.toml b/examples/focus/pyproject.toml index 96cff7cc9a..0017c5b125 100644 --- a/examples/focus/pyproject.toml +++ b/examples/focus/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/font/pyproject.toml b/examples/font/pyproject.toml index ecc4a4a797..6f700e0bc5 100644 --- a/examples/font/pyproject.toml +++ b/examples/font/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/font_size/pyproject.toml b/examples/font_size/pyproject.toml index 0fde87c961..1a3e9dd001 100644 --- a/examples/font_size/pyproject.toml +++ b/examples/font_size/pyproject.toml @@ -61,4 +61,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/handlers/pyproject.toml b/examples/handlers/pyproject.toml index 4f28ae8417..df896ecb57 100644 --- a/examples/handlers/pyproject.toml +++ b/examples/handlers/pyproject.toml @@ -63,4 +63,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/imageview/pyproject.toml b/examples/imageview/pyproject.toml index 1d19328ed4..7269c11b3c 100644 --- a/examples/imageview/pyproject.toml +++ b/examples/imageview/pyproject.toml @@ -67,4 +67,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/layout/pyproject.toml b/examples/layout/pyproject.toml index 49aef35007..984ebb9ed2 100644 --- a/examples/layout/pyproject.toml +++ b/examples/layout/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/mapview/pyproject.toml b/examples/mapview/pyproject.toml index 6d801046ec..efa6feb25e 100644 --- a/examples/mapview/pyproject.toml +++ b/examples/mapview/pyproject.toml @@ -63,4 +63,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/multilinetextinput/pyproject.toml b/examples/multilinetextinput/pyproject.toml index b44ba919be..fd243c317c 100644 --- a/examples/multilinetextinput/pyproject.toml +++ b/examples/multilinetextinput/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/numberinput/pyproject.toml b/examples/numberinput/pyproject.toml index 3127a10c99..f576b13b59 100644 --- a/examples/numberinput/pyproject.toml +++ b/examples/numberinput/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/optioncontainer/pyproject.toml b/examples/optioncontainer/pyproject.toml index 48f19ae88a..9efe87d3ce 100644 --- a/examples/optioncontainer/pyproject.toml +++ b/examples/optioncontainer/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/passwordinput/pyproject.toml b/examples/passwordinput/pyproject.toml index 8957912f3f..e9d5590848 100644 --- a/examples/passwordinput/pyproject.toml +++ b/examples/passwordinput/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ '../../web', ] -style_framework = "WebAwesome v3.3" diff --git a/examples/positron-django/pyproject.toml b/examples/positron-django/pyproject.toml index b2ea786c2b..62c25218ff 100644 --- a/examples/positron-django/pyproject.toml +++ b/examples/positron-django/pyproject.toml @@ -64,4 +64,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/positron-static/pyproject.toml b/examples/positron-static/pyproject.toml index 37e7c813b4..646c31ea49 100644 --- a/examples/positron-static/pyproject.toml +++ b/examples/positron-static/pyproject.toml @@ -63,4 +63,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/progressbar/pyproject.toml b/examples/progressbar/pyproject.toml index 7d0dcaaeea..e9617f4438 100644 --- a/examples/progressbar/pyproject.toml +++ b/examples/progressbar/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/resize/pyproject.toml b/examples/resize/pyproject.toml index 8225f11cb2..314118f4b9 100644 --- a/examples/resize/pyproject.toml +++ b/examples/resize/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/scrollcontainer/pyproject.toml b/examples/scrollcontainer/pyproject.toml index 500126987d..f96bdc6eab 100644 --- a/examples/scrollcontainer/pyproject.toml +++ b/examples/scrollcontainer/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/selection/pyproject.toml b/examples/selection/pyproject.toml index d48a606688..66bf346344 100644 --- a/examples/selection/pyproject.toml +++ b/examples/selection/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/simpleapp/pyproject.toml b/examples/simpleapp/pyproject.toml index 121961e430..5d69ef7ae2 100644 --- a/examples/simpleapp/pyproject.toml +++ b/examples/simpleapp/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/slider/pyproject.toml b/examples/slider/pyproject.toml index 598ea03987..840dff072e 100644 --- a/examples/slider/pyproject.toml +++ b/examples/slider/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/splitcontainer/pyproject.toml b/examples/splitcontainer/pyproject.toml index 3fb3f38af0..b100b9d722 100644 --- a/examples/splitcontainer/pyproject.toml +++ b/examples/splitcontainer/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/switch_demo/pyproject.toml b/examples/switch_demo/pyproject.toml index dda2283dba..0a234fcc08 100644 --- a/examples/switch_demo/pyproject.toml +++ b/examples/switch_demo/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/table/pyproject.toml b/examples/table/pyproject.toml index 50b1831c55..340cb25547 100644 --- a/examples/table/pyproject.toml +++ b/examples/table/pyproject.toml @@ -61,4 +61,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/table_columns/pyproject.toml b/examples/table_columns/pyproject.toml index cc2aac113d..71d8537b53 100644 --- a/examples/table_columns/pyproject.toml +++ b/examples/table_columns/pyproject.toml @@ -65,4 +65,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/table_source/pyproject.toml b/examples/table_source/pyproject.toml index 2c207f2883..1838c1ed54 100644 --- a/examples/table_source/pyproject.toml +++ b/examples/table_source/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/textinput/pyproject.toml b/examples/textinput/pyproject.toml index d115e16ecc..8f5d6d5173 100644 --- a/examples/textinput/pyproject.toml +++ b/examples/textinput/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/tree/pyproject.toml b/examples/tree/pyproject.toml index bbc2c9d7a1..383bf5a736 100644 --- a/examples/tree/pyproject.toml +++ b/examples/tree/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/tree_source/pyproject.toml b/examples/tree_source/pyproject.toml index 71d1805741..443347aef4 100644 --- a/examples/tree_source/pyproject.toml +++ b/examples/tree_source/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/tutorial0/pyproject.toml b/examples/tutorial0/pyproject.toml index 3baf2a41e7..d985ad9406 100644 --- a/examples/tutorial0/pyproject.toml +++ b/examples/tutorial0/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/tutorial1/pyproject.toml b/examples/tutorial1/pyproject.toml index 124d1f688d..5ee9e8d49d 100644 --- a/examples/tutorial1/pyproject.toml +++ b/examples/tutorial1/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/tutorial2/pyproject.toml b/examples/tutorial2/pyproject.toml index b8ed1fbbc6..bf7130de38 100644 --- a/examples/tutorial2/pyproject.toml +++ b/examples/tutorial2/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/tutorial3/pyproject.toml b/examples/tutorial3/pyproject.toml index 7126d3441f..0ccb8c4ec6 100644 --- a/examples/tutorial3/pyproject.toml +++ b/examples/tutorial3/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/tutorial4/pyproject.toml b/examples/tutorial4/pyproject.toml index 9737cee3f9..8e29ca58fb 100644 --- a/examples/tutorial4/pyproject.toml +++ b/examples/tutorial4/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/webview/pyproject.toml b/examples/webview/pyproject.toml index e535b05fa7..b8a070bdb3 100644 --- a/examples/webview/pyproject.toml +++ b/examples/webview/pyproject.toml @@ -66,4 +66,3 @@ chaquopy.defaultConfig.staticProxy("toga_android.widgets.internal.webview") requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" diff --git a/examples/window/pyproject.toml b/examples/window/pyproject.toml index 0e7fb54400..dade244077 100644 --- a/examples/window/pyproject.toml +++ b/examples/window/pyproject.toml @@ -62,4 +62,3 @@ build_gradle_dependencies = [ requires = [ "../../web", ] -style_framework = "WebAwesome v3.3" From e2cd97681ab6da404785378be9a71588707cf82e Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Tue, 24 Mar 2026 14:18:57 -0700 Subject: [PATCH 08/31] Use WebAwesome dist-cdn for browser loading; remove stray sandbox.html The dist/ build requires a bundler to resolve bare module specifiers; dist-cdn/ bundles all dependencies for direct browser use. --- examples/tutorial0/sandbox.html | 111 ------------------ .../toga_web/deploy/inserts/index.html~head | 6 +- 2 files changed, 3 insertions(+), 114 deletions(-) delete mode 100644 examples/tutorial0/sandbox.html diff --git a/examples/tutorial0/sandbox.html b/examples/tutorial0/sandbox.html deleted file mode 100644 index 076b89c0f5..0000000000 --- a/examples/tutorial0/sandbox.html +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - First App - - - - - - - - - - - - - -
- First App logo - - -
-
-
- Hello world -
-
- - diff --git a/web/src/toga_web/deploy/inserts/index.html~head b/web/src/toga_web/deploy/inserts/index.html~head index 74cfcdbef7..c35a6d88f8 100644 --- a/web/src/toga_web/deploy/inserts/index.html~head +++ b/web/src/toga_web/deploy/inserts/index.html~head @@ -1,4 +1,4 @@ - - - + + + From 91b612f8ad6730c0c7a6882be35c5f726a17ee46 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Thu, 26 Mar 2026 18:50:04 -0700 Subject: [PATCH 09/31] get slider demo working on web --- web/src/toga_web/static/toga.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/web/src/toga_web/static/toga.css b/web/src/toga_web/static/toga.css index e3aa84d253..35b9e2b259 100644 --- a/web/src/toga_web/static/toga.css +++ b/web/src/toga_web/static/toga.css @@ -106,3 +106,12 @@ main.toga.window > .container { .toga .label { white-space: nowrap; } + +/***** Slider *********************************************************/ +/* wa-slider defaults to display:inline (custom element default), which + ignores width and doesn't stretch in flex containers. sl-range set + display:block internally; wa-slider does not. */ +.toga wa-slider { + display: block; + width: 100%; +} From 1617576fd430a80246ade1500be325f83dd255a2 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Thu, 26 Mar 2026 18:57:46 -0700 Subject: [PATCH 10/31] Guard against WebAwesome async element upgrade in widget property access WebAwesome registers custom elements asynchronously, so native element properties (.value, .checked, .min, .max, etc.) may not exist when Python code runs during widget init. Add _get_native_attr/_set_native_attr helpers on the base Widget class that catch AttributeError and warn instead of crashing. Apply to Switch, Selection, and Slider. --- web/src/toga_web/widgets/base.py | 32 +++++++++++++++++++++++++++ web/src/toga_web/widgets/selection.py | 14 ++++++++---- web/src/toga_web/widgets/slider.py | 26 +++++++++++++--------- web/src/toga_web/widgets/switch.py | 2 +- 4 files changed, 58 insertions(+), 16 deletions(-) diff --git a/web/src/toga_web/widgets/base.py b/web/src/toga_web/widgets/base.py index 8c1b73cae6..02078e8a7b 100644 --- a/web/src/toga_web/widgets/base.py +++ b/web/src/toga_web/widgets/base.py @@ -51,6 +51,38 @@ def _create_native_widget( @abstractmethod def create(self): ... + def _get_native_attr(self, attr, default): + """Get a native element property, with a fallback if the element isn't + upgraded yet. WebAwesome registers custom elements asynchronously, so + properties may not exist when Python code runs during init.""" + try: + return getattr(self.native, attr) + except AttributeError: + import warnings + + tag = getattr(self.native, "tagName", "unknown") + warnings.warn( + f"{tag} element not yet upgraded; defaulting {attr} to {default!r}", + RuntimeWarning, + stacklevel=2, + ) + return default + + def _set_native_attr(self, attr, value): + """Set a native element property, ignoring if the element isn't + upgraded yet.""" + try: + setattr(self.native, attr, value) + except AttributeError: + import warnings + + tag = getattr(self.native, "tagName", "unknown") + warnings.warn( + f"{tag} element not yet upgraded; ignoring set {attr}={value!r}", + RuntimeWarning, + stacklevel=2, + ) + def set_app(self, app): # noqa B027 pass diff --git a/web/src/toga_web/widgets/selection.py b/web/src/toga_web/widgets/selection.py index ff49375c62..46612a02be 100644 --- a/web/src/toga_web/widgets/selection.py +++ b/web/src/toga_web/widgets/selection.py @@ -48,16 +48,22 @@ def source_insert(self, *, index, item): option = self._create_native_widget("wa-option") option.value = str(index) option.textContent = display_text - if self.native.value == "": - self.native.value = option.value + # Band-aid: wa-select may not be upgraded yet (async element registration). + # If .value isn't available, we default to "" and try to auto-select the + # first item — but the assignment may not stick, so the dropdown can appear + # blank until the user interacts with it. + native_value = self._get_native_attr("value", "") + if native_value == "": + self._set_native_attr("value", option.value) if index >= len(self.native.children): self.native.appendChild(option) else: self.native.insertBefore(option, self.native.children[index]) def get_selected_index(self): - if self.native.value: - return int(self.native.value) + value = self._get_native_attr("value", "") + if value: + return int(value) return None def select_item(self, index, item): diff --git a/web/src/toga_web/widgets/slider.py b/web/src/toga_web/widgets/slider.py index b485ab4146..5595d2c624 100644 --- a/web/src/toga_web/widgets/slider.py +++ b/web/src/toga_web/widgets/slider.py @@ -24,31 +24,35 @@ def dom_onpointerup(self, event): self.interface.on_release() def get_value(self): - return float(self.native.value) + return float(self._get_native_attr("value", 0.0)) def set_value(self, value): - self.native.value = value + self._set_native_attr("value", value) def get_min(self): - return float(self.native.min) + return float(self._get_native_attr("min", 0.0)) def set_min(self, value): - self.native.min = value + self._set_native_attr("min", value) def get_max(self): - return float(self.native.max) + return float(self._get_native_attr("max", 1.0)) def set_max(self, value): - self.native.max = value + self._set_native_attr("max", value) def get_tick_count(self): - step = float(self.native.step or 0) - return int((float(self.native.max) - float(self.native.min)) / step) + 1 + try: + step = float(self.native.step or 0) + return int((float(self.native.max) - float(self.native.min)) / step) + 1 + except AttributeError: + return None def set_tick_count(self, tick_count): if tick_count: - self.native.step = (self.interface.max - self.interface.min) / ( - tick_count - 1 + self._set_native_attr( + "step", + (self.interface.max - self.interface.min) / (tick_count - 1), ) else: - self.native.step = 0.0001 + self._set_native_attr("step", 0.0001) diff --git a/web/src/toga_web/widgets/switch.py b/web/src/toga_web/widgets/switch.py index 6ab2095897..b5c2958a5f 100644 --- a/web/src/toga_web/widgets/switch.py +++ b/web/src/toga_web/widgets/switch.py @@ -18,7 +18,7 @@ def set_text(self, text): self.native.innerHTML = text def get_value(self): - return self.native.checked + return self._get_native_attr("checked", False) def set_value(self, value): old_value = self.get_value() From a428b30fb2b50245bc7b8951c266dfad6ea1e404 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Thu, 26 Mar 2026 19:17:08 -0700 Subject: [PATCH 11/31] linting --- core/src/toga/sources/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/toga/sources/base.py b/core/src/toga/sources/base.py index 08796e671a..bd7509e516 100644 --- a/core/src/toga/sources/base.py +++ b/core/src/toga/sources/base.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from typing import Generic, Protocol, TypeVar, runtime_checkable ListenerT = TypeVar("ListenerT") @@ -128,8 +129,6 @@ def notify(self, notification: str, **kwargs: object) -> None: if method is None: method = getattr(listener, notification, None) if method is not None: - import warnings - warnings.warn( f"Notification handler methods on Listeners now start with " f"'source_'. Change the method name to " From 9e2673ea963d3f1e213d8ae54bffdef633b3309c Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Thu, 26 Mar 2026 19:19:32 -0700 Subject: [PATCH 12/31] clean up obsolete comment --- web/src/toga_web/widgets/selection.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web/src/toga_web/widgets/selection.py b/web/src/toga_web/widgets/selection.py index 46612a02be..1c093443a0 100644 --- a/web/src/toga_web/widgets/selection.py +++ b/web/src/toga_web/widgets/selection.py @@ -48,10 +48,6 @@ def source_insert(self, *, index, item): option = self._create_native_widget("wa-option") option.value = str(index) option.textContent = display_text - # Band-aid: wa-select may not be upgraded yet (async element registration). - # If .value isn't available, we default to "" and try to auto-select the - # first item — but the assignment may not stick, so the dropdown can appear - # blank until the user interacts with it. native_value = self._get_native_attr("value", "") if native_value == "": self._set_native_attr("value", option.value) From 94f8ccc0c4c46fc936e2ffb947208695054ed322 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Thu, 26 Mar 2026 19:21:05 -0700 Subject: [PATCH 13/31] linting --- web/src/toga_web/widgets/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/toga_web/widgets/base.py b/web/src/toga_web/widgets/base.py index 02078e8a7b..e906af0f58 100644 --- a/web/src/toga_web/widgets/base.py +++ b/web/src/toga_web/widgets/base.py @@ -1,3 +1,4 @@ +import warnings from abc import ABC, abstractmethod from toga_web.libs import create_element @@ -58,8 +59,6 @@ def _get_native_attr(self, attr, default): try: return getattr(self.native, attr) except AttributeError: - import warnings - tag = getattr(self.native, "tagName", "unknown") warnings.warn( f"{tag} element not yet upgraded; defaulting {attr} to {default!r}", From 8dc2adb3b07c2f90c2fd110101981946c989b882 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Thu, 26 Mar 2026 19:23:08 -0700 Subject: [PATCH 14/31] linting --- web/src/toga_web/widgets/base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/src/toga_web/widgets/base.py b/web/src/toga_web/widgets/base.py index e906af0f58..f0a8feccf7 100644 --- a/web/src/toga_web/widgets/base.py +++ b/web/src/toga_web/widgets/base.py @@ -73,8 +73,6 @@ def _set_native_attr(self, attr, value): try: setattr(self.native, attr, value) except AttributeError: - import warnings - tag = getattr(self.native, "tagName", "unknown") warnings.warn( f"{tag} element not yet upgraded; ignoring set {attr}={value!r}", From 270ac505843412ef8f4f3d83f4719562985db8df Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Thu, 26 Mar 2026 20:37:25 -0700 Subject: [PATCH 15/31] trigger CI From 7e7ccf5443e62ec94793eee75c4a1b63aa23c9b7 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Tue, 14 Apr 2026 20:19:00 -0700 Subject: [PATCH 16/31] Replace helpers with buffered-replay NativeProxy The last approach, _get_native_attr / _set_native_attr, handled WebAwesome's async upgrade by silently dropping pre-upgrade writes, so widgets lost any state configured before upgrade. Replace with a NativeProxy that buffers pre-upgrade sets and replays them from a customElements.whenDefined callback after force-upgrading the element. Widget code reverts to direct self.native.foo access with local AttributeError fallbacks where reads can happen before writes. Callers that pass .native into appendChild/insertBefore now use .unwrap() to reach the underlying JsProxy, since Pyodide does not auto-unwrap Python wrappers. --- web/src/toga_web/widgets/base.py | 98 ++++++++++++++------- web/src/toga_web/widgets/box.py | 2 +- web/src/toga_web/widgets/scrollcontainer.py | 4 +- web/src/toga_web/widgets/selection.py | 16 ++-- web/src/toga_web/widgets/slider.py | 28 +++--- web/src/toga_web/widgets/switch.py | 5 +- web/src/toga_web/window.py | 2 +- 7 files changed, 104 insertions(+), 51 deletions(-) diff --git a/web/src/toga_web/widgets/base.py b/web/src/toga_web/widgets/base.py index f0a8feccf7..426967ae85 100644 --- a/web/src/toga_web/widgets/base.py +++ b/web/src/toga_web/widgets/base.py @@ -1,7 +1,71 @@ -import warnings from abc import ABC, abstractmethod -from toga_web.libs import create_element +import js + +from toga_web.libs import create_element, create_proxy + + +class NativeProxy: + """Wraps a WebAwesome custom element, buffering attribute sets that happen + before the element's tag has been defined and the instance upgraded. + + Toga's widget lifecycle creates elements detached from the DOM and + immediately sets class-defined properties on them (e.g. `wa-switch.checked`). + Those properties only exist after the browser has (a) loaded the component + module and called `customElements.define` and (b) upgraded the specific + element instance. Until then, attribute sets would fail. + + The proxy: + * buffers pre-upgrade sets in an insertion-ordered dict + * returns the buffered value (or passes through) on reads + * once `whenDefined` resolves for the element's tag, force-upgrades the + (possibly still-disconnected) element with `customElements.upgrade` + and replays the buffer onto the now-upgraded element + """ + + def __init__(self, element): + # object.__setattr__ to avoid calling our own __setattr__ override when mutating these bookkeeping members + object.__setattr__(self, "_element", element) + object.__setattr__(self, "_pending", {}) + object.__setattr__(self, "_upgraded", False) + + tag = element.tagName.lower() + if tag.startswith("wa-"): + + def on_defined(promise): + js.customElements.upgrade(element) + object.__setattr__(self, "_upgraded", True) + for attr, value in self._pending.items(): + setattr(element, attr, value) + self._pending.clear() + + js.customElements.whenDefined(tag).then(create_proxy(on_defined)) + else: + object.__setattr__(self, "_upgraded", True) + + def unwrap(self): + """Return the underlying JsProxy element. + + Pyodide unwraps JsProxy arguments automatically when marshaling into + JS, but it does not unwrap arbitrary Python objects — so callers + that pass a NativeProxy as an *argument* to a JS method (e.g. + appendChild, insertBefore) must call unwrap() to hand JS the real Node. + """ + return self._element + + def __getattr__(self, name): + if not self._upgraded and name in self._pending: + return self._pending[name] + + # If not upgraded and name hasn't been set, raise AttributeError; + # The widgets themselves should define their defaults for those cases + return getattr(self._element, name) + + def __setattr__(self, name, value): + if self._upgraded: + setattr(self._element, name, value) + else: + self._pending[name] = value class Widget(ABC): @@ -47,39 +111,11 @@ def _create_native_widget( **properties, ) - return native + return NativeProxy(native) @abstractmethod def create(self): ... - def _get_native_attr(self, attr, default): - """Get a native element property, with a fallback if the element isn't - upgraded yet. WebAwesome registers custom elements asynchronously, so - properties may not exist when Python code runs during init.""" - try: - return getattr(self.native, attr) - except AttributeError: - tag = getattr(self.native, "tagName", "unknown") - warnings.warn( - f"{tag} element not yet upgraded; defaulting {attr} to {default!r}", - RuntimeWarning, - stacklevel=2, - ) - return default - - def _set_native_attr(self, attr, value): - """Set a native element property, ignoring if the element isn't - upgraded yet.""" - try: - setattr(self.native, attr, value) - except AttributeError: - tag = getattr(self.native, "tagName", "unknown") - warnings.warn( - f"{tag} element not yet upgraded; ignoring set {attr}={value!r}", - RuntimeWarning, - stacklevel=2, - ) - def set_app(self, app): # noqa B027 pass diff --git a/web/src/toga_web/widgets/box.py b/web/src/toga_web/widgets/box.py index 04927a6688..7a732105e4 100644 --- a/web/src/toga_web/widgets/box.py +++ b/web/src/toga_web/widgets/box.py @@ -6,4 +6,4 @@ def create(self): self.native = self._create_native_widget("div", classes=["container"]) def add_child(self, child): - self.native.appendChild(child.native) + self.native.appendChild(child.native.unwrap()) diff --git a/web/src/toga_web/widgets/scrollcontainer.py b/web/src/toga_web/widgets/scrollcontainer.py index d5f362274c..42d29c13dd 100644 --- a/web/src/toga_web/widgets/scrollcontainer.py +++ b/web/src/toga_web/widgets/scrollcontainer.py @@ -20,7 +20,7 @@ def create(self): "div", classes=["toga-scroll-content"], ) - self.native.appendChild(self._content_div) + self.native.appendChild(self._content_div.unwrap()) self._update_overflow() def set_content(self, content_impl): @@ -28,7 +28,7 @@ def set_content(self, content_impl): self._content_div.removeChild(self._content_div.firstChild) if content_impl is not None: - self._content_div.appendChild(content_impl.native) + self._content_div.appendChild(content_impl.native.unwrap()) def _update_overflow(self): self.native.style.overflowX = "auto" if self._horizontal_enabled else "hidden" diff --git a/web/src/toga_web/widgets/selection.py b/web/src/toga_web/widgets/selection.py index 1c093443a0..e1592abc40 100644 --- a/web/src/toga_web/widgets/selection.py +++ b/web/src/toga_web/widgets/selection.py @@ -48,16 +48,22 @@ def source_insert(self, *, index, item): option = self._create_native_widget("wa-option") option.value = str(index) option.textContent = display_text - native_value = self._get_native_attr("value", "") + try: + native_value = self.native.value + except AttributeError: + native_value = "" if native_value == "": - self._set_native_attr("value", option.value) + self.native.value = option.value if index >= len(self.native.children): - self.native.appendChild(option) + self.native.appendChild(option.unwrap()) else: - self.native.insertBefore(option, self.native.children[index]) + self.native.insertBefore(option.unwrap(), self.native.children[index]) def get_selected_index(self): - value = self._get_native_attr("value", "") + try: + value = self.native.value + except AttributeError: + value = "" if value: return int(value) return None diff --git a/web/src/toga_web/widgets/slider.py b/web/src/toga_web/widgets/slider.py index 5595d2c624..f2734b2c04 100644 --- a/web/src/toga_web/widgets/slider.py +++ b/web/src/toga_web/widgets/slider.py @@ -24,22 +24,31 @@ def dom_onpointerup(self, event): self.interface.on_release() def get_value(self): - return float(self._get_native_attr("value", 0.0)) + try: + return float(self.native.value) + except AttributeError: + return 0.0 def set_value(self, value): - self._set_native_attr("value", value) + self.native.value = value def get_min(self): - return float(self._get_native_attr("min", 0.0)) + try: + return float(self.native.min) + except AttributeError: + return 0.0 def set_min(self, value): - self._set_native_attr("min", value) + self.native.min = value def get_max(self): - return float(self._get_native_attr("max", 1.0)) + try: + return float(self.native.max) + except AttributeError: + return 1.0 def set_max(self, value): - self._set_native_attr("max", value) + self.native.max = value def get_tick_count(self): try: @@ -50,9 +59,8 @@ def get_tick_count(self): def set_tick_count(self, tick_count): if tick_count: - self._set_native_attr( - "step", - (self.interface.max - self.interface.min) / (tick_count - 1), + self.native.step = (self.interface.max - self.interface.min) / ( + tick_count - 1 ) else: - self._set_native_attr("step", 0.0001) + self.native.step = 0.0001 diff --git a/web/src/toga_web/widgets/switch.py b/web/src/toga_web/widgets/switch.py index b5c2958a5f..ef38019bda 100644 --- a/web/src/toga_web/widgets/switch.py +++ b/web/src/toga_web/widgets/switch.py @@ -18,7 +18,10 @@ def set_text(self, text): self.native.innerHTML = text def get_value(self): - return self._get_native_attr("checked", False) + try: + return self.native.checked + except AttributeError: + return False def set_value(self, value): old_value = self.get_value() diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index 9a277255c7..a4809d6d78 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -97,7 +97,7 @@ def set_content(self, widget): self.native.removeChild(child) # Add all children to the content widget. - self.native.appendChild(widget.native) + self.native.appendChild(widget.native.unwrap()) ###################################################################### # Window size From 4fd03359b5029a3ae66be169784b4e4359321b37 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Tue, 14 Apr 2026 20:26:56 -0700 Subject: [PATCH 17/31] linting --- web/src/toga_web/widgets/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/toga_web/widgets/base.py b/web/src/toga_web/widgets/base.py index 426967ae85..dd13383b16 100644 --- a/web/src/toga_web/widgets/base.py +++ b/web/src/toga_web/widgets/base.py @@ -24,7 +24,8 @@ class NativeProxy: """ def __init__(self, element): - # object.__setattr__ to avoid calling our own __setattr__ override when mutating these bookkeeping members + # when mutating these bookkeeping members, + # call object.__setattr__ to avoid calling our own __setattr__ override object.__setattr__(self, "_element", element) object.__setattr__(self, "_pending", {}) object.__setattr__(self, "_upgraded", False) From 7fba2606d771cfb0aa6bba95fbbe1249c1883327 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Wed, 15 Apr 2026 10:32:59 -0700 Subject: [PATCH 18/31] maintain write order on elements post-upgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For example, you don’t want to set a slider’s value before its min and max --- web/src/toga_web/widgets/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/src/toga_web/widgets/base.py b/web/src/toga_web/widgets/base.py index dd13383b16..6ad9595a67 100644 --- a/web/src/toga_web/widgets/base.py +++ b/web/src/toga_web/widgets/base.py @@ -66,6 +66,9 @@ def __setattr__(self, name, value): if self._upgraded: setattr(self._element, name, value) else: + # Pop-then-reinsert so replay order mirrors write order even + # when the same key is set more than once pre-upgrade. + self._pending.pop(name, None) self._pending[name] = value From 19c4b55507d6bd864f7050d6cff706b41eb4778e Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Wed, 15 Apr 2026 10:59:39 -0700 Subject: [PATCH 19/31] defaults on pre-upgraded dateinput and timeinput --- web/src/toga_web/widgets/dateinput.py | 21 +++++++++++++++------ web/src/toga_web/widgets/timeinput.py | 12 ++++++++++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/web/src/toga_web/widgets/dateinput.py b/web/src/toga_web/widgets/dateinput.py index 1252ea77f6..47c75ec90c 100644 --- a/web/src/toga_web/widgets/dateinput.py +++ b/web/src/toga_web/widgets/dateinput.py @@ -1,5 +1,6 @@ import datetime +from toga.widgets.dateinput import MAX_DATE, MIN_DATE from toga_web.libs import create_proxy from .base import Widget @@ -53,14 +54,22 @@ def set_min_date(self, value): self.native.min = native_date(value) def get_min_date(self): - if self.native.min is None: - return datetime.date(1800, 1, 1) - return datetime.date.fromisoformat(str(self.native.min)) + try: + native_min = self.native.min + except AttributeError: + return MIN_DATE + if native_min is None: + return MIN_DATE + return datetime.date.fromisoformat(str(native_min)) def set_max_date(self, value): self.native.max = native_date(value) def get_max_date(self): - if self.native.max is None: - return datetime.date(8999, 12, 31) - return datetime.date.fromisoformat(str(self.native.max)) + try: + native_max = self.native.max + except AttributeError: + return MAX_DATE + if native_max is None: + return MAX_DATE + return datetime.date.fromisoformat(str(native_max)) diff --git a/web/src/toga_web/widgets/timeinput.py b/web/src/toga_web/widgets/timeinput.py index c63740380b..7ba8e708c3 100644 --- a/web/src/toga_web/widgets/timeinput.py +++ b/web/src/toga_web/widgets/timeinput.py @@ -62,14 +62,22 @@ def set_value(self, value): self.native.value = native_time(value) def get_min_time(self): - min_value = self.native.min if self.native.min else datetime.time(0, 0, 0) + try: + min_value = self.native.min if self.native.min else datetime.time(0, 0, 0) + except AttributeError: + min_value = datetime.time(0, 0, 0) return py_time(min_value) def set_min_time(self, value): self.native.min = native_time(value) def get_max_time(self): - max_value = self.native.max if self.native.max else datetime.time(23, 59, 59) + try: + max_value = ( + self.native.max if self.native.max else datetime.time(23, 59, 59) + ) + except AttributeError: + max_value = datetime.time(23, 59, 59) return py_time(max_value) def set_max_time(self, value): From 915ee382ee78faec48af139d882249868933c71d Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Wed, 15 Apr 2026 17:58:33 -0700 Subject: [PATCH 20/31] Port theming tokens to WebAwesome and outline buttons toga.css referenced --sl-* tokens that no longer exist in WA's shoelace theme, leaving the header unstyled. Replace with --wa-* equivalents. wa-button defaults to appearance="accent" which rendered as a dark filled pill; switch to "outlined" to match the previous Shoelace look and let per-widget background colors show through. --- web/src/toga_web/static/toga.css | 22 +++++++++++----------- web/src/toga_web/widgets/button.py | 1 + 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/web/src/toga_web/static/toga.css b/web/src/toga_web/static/toga.css index 35b9e2b259..78af4d2369 100644 --- a/web/src/toga_web/static/toga.css +++ b/web/src/toga_web/static/toga.css @@ -13,8 +13,8 @@ html, body { } #app-placeholder { - font-family: var(--sl-font-sans); - font-size: var(--sl-font-size-medium); + font-family: var(--wa-font-family-body); + font-size: var(--wa-font-size-m); } /* If a custom element hasn't been defined yet, hide it from rendering */ @@ -27,8 +27,8 @@ html, body { **********************************************************************/ header.toga { - background-color: var(--sl-color-primary-800); - color: var(--sl-color-neutral-50); + background-color: var(--wa-color-brand-fill-loud); + color: var(--wa-color-brand-on-loud); display: flex; flex-direction: row; align-items: center; @@ -50,8 +50,8 @@ header.toga nav { header.toga nav.menubar { flex-grow: 1; - font-family: var(--sl-font-sans); - font-size: var(--sl-font-size-large); + font-family: var(--wa-font-family-body); + font-size: var(--wa-font-size-l); } header.toga nav.menubar:last-of-type { @@ -62,18 +62,18 @@ header.toga nav.menubar:last-of-type { /***** Menubar ********************************************************/ header.toga nav.menubar .menu { - color: var(--sl-color-neutral-200); + color: var(--wa-color-brand-on-loud); padding: 0.5em; } header.toga nav.menubar .menu:hover { - background-color: var(--sl-color-primary-700); - color: var(--sl-color-primary-300); + background-color: var(--wa-color-brand-fill-loud); + color: var(--wa-color-brand-on-loud); } header.toga nav.menubar .app { - color: var(--sl-color-neutral-50); - font-weight: var(--sl-font-weight-bold); + color: var(--wa-color-brand-on-loud); + font-weight: var(--wa-font-weight-bold); } /********************************************************************** diff --git a/web/src/toga_web/widgets/button.py b/web/src/toga_web/widgets/button.py index 59ac26eb01..0876a7f26b 100644 --- a/web/src/toga_web/widgets/button.py +++ b/web/src/toga_web/widgets/button.py @@ -6,6 +6,7 @@ class Button(Widget): def create(self): self.native = self._create_native_widget("wa-button") + self.native.setAttribute("appearance", "outlined") self.native.addEventListener("click", create_proxy(self.dom_click)) def dom_click(self, event): From 0ca1fef3da980a94ca5501dfd6448443d3c9480c Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Wed, 15 Apr 2026 21:02:12 -0700 Subject: [PATCH 21/31] make header bar closer to shoelace color --- web/src/toga_web/static/toga.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/toga_web/static/toga.css b/web/src/toga_web/static/toga.css index 78af4d2369..43cd51a0ba 100644 --- a/web/src/toga_web/static/toga.css +++ b/web/src/toga_web/static/toga.css @@ -27,7 +27,7 @@ html, body { **********************************************************************/ header.toga { - background-color: var(--wa-color-brand-fill-loud); + background-color: var(--wa-color-brand-30); color: var(--wa-color-brand-on-loud); display: flex; flex-direction: row; @@ -67,7 +67,7 @@ header.toga nav.menubar .menu { } header.toga nav.menubar .menu:hover { - background-color: var(--wa-color-brand-fill-loud); + background-color: var(--wa-color-brand-30); color: var(--wa-color-brand-on-loud); } From ece4ca4695603229e004da244b734cf8c9d895fb Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Thu, 16 Apr 2026 08:45:00 -0700 Subject: [PATCH 22/31] auto-unwrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of needing to call .unwrap when we’re calling into a JS method, this adds to our __getattr__ override to, if the method we’re calling is a JS method, automatically unwrap any NativeProxy arguments --- web/src/toga_web/widgets/base.py | 16 +++++++++++++--- web/src/toga_web/widgets/box.py | 2 +- web/src/toga_web/widgets/scrollcontainer.py | 4 ++-- web/src/toga_web/widgets/selection.py | 4 ++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/web/src/toga_web/widgets/base.py b/web/src/toga_web/widgets/base.py index 6ad9595a67..6d993d437a 100644 --- a/web/src/toga_web/widgets/base.py +++ b/web/src/toga_web/widgets/base.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod import js +from pyodide.ffi import JsProxy from toga_web.libs import create_element, create_proxy @@ -58,9 +59,18 @@ def __getattr__(self, name): if not self._upgraded and name in self._pending: return self._pending[name] - # If not upgraded and name hasn't been set, raise AttributeError; - # The widgets themselves should define their defaults for those cases - return getattr(self._element, name) + attr = getattr(self._element, name) + if isinstance(attr, JsProxy) and callable(attr): + # Wrap JS methods so any NativeProxy arguments are automatically + # unwrapped to their underlying JsProxy elements before the call. + def _auto_unwrap(*args): + unwrapped_args = [ + a.unwrap() if isinstance(a, NativeProxy) else a for a in args + ] + return attr(*unwrapped_args) + + return _auto_unwrap + return attr def __setattr__(self, name, value): if self._upgraded: diff --git a/web/src/toga_web/widgets/box.py b/web/src/toga_web/widgets/box.py index 7a732105e4..04927a6688 100644 --- a/web/src/toga_web/widgets/box.py +++ b/web/src/toga_web/widgets/box.py @@ -6,4 +6,4 @@ def create(self): self.native = self._create_native_widget("div", classes=["container"]) def add_child(self, child): - self.native.appendChild(child.native.unwrap()) + self.native.appendChild(child.native) diff --git a/web/src/toga_web/widgets/scrollcontainer.py b/web/src/toga_web/widgets/scrollcontainer.py index 42d29c13dd..d5f362274c 100644 --- a/web/src/toga_web/widgets/scrollcontainer.py +++ b/web/src/toga_web/widgets/scrollcontainer.py @@ -20,7 +20,7 @@ def create(self): "div", classes=["toga-scroll-content"], ) - self.native.appendChild(self._content_div.unwrap()) + self.native.appendChild(self._content_div) self._update_overflow() def set_content(self, content_impl): @@ -28,7 +28,7 @@ def set_content(self, content_impl): self._content_div.removeChild(self._content_div.firstChild) if content_impl is not None: - self._content_div.appendChild(content_impl.native.unwrap()) + self._content_div.appendChild(content_impl.native) def _update_overflow(self): self.native.style.overflowX = "auto" if self._horizontal_enabled else "hidden" diff --git a/web/src/toga_web/widgets/selection.py b/web/src/toga_web/widgets/selection.py index e1592abc40..d61d308469 100644 --- a/web/src/toga_web/widgets/selection.py +++ b/web/src/toga_web/widgets/selection.py @@ -55,9 +55,9 @@ def source_insert(self, *, index, item): if native_value == "": self.native.value = option.value if index >= len(self.native.children): - self.native.appendChild(option.unwrap()) + self.native.appendChild(option) else: - self.native.insertBefore(option.unwrap(), self.native.children[index]) + self.native.insertBefore(option, self.native.children[index]) def get_selected_index(self): try: From aff9fb6d5ec85745d909c150ce3d42d02f625d11 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Thu, 16 Apr 2026 09:22:20 -0700 Subject: [PATCH 23/31] Window needs to unwrap widgets in set_content --- web/src/toga_web/window.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/src/toga_web/window.py b/web/src/toga_web/window.py index a4809d6d78..0a67de71eb 100644 --- a/web/src/toga_web/window.py +++ b/web/src/toga_web/window.py @@ -96,7 +96,11 @@ def set_content(self, widget): for child in self.native.childNodes: self.native.removeChild(child) - # Add all children to the content widget. + # widget.native is a NativeProxy (a Python wrapper, not a JsProxy). + # NativeProxy.__getattr__ auto-unwraps NativeProxy arguments when you + # call a JS method on a NativeProxy — but self.native here is a plain + # JsProxy (Window doesn't use _create_native_widget), so auto-unwrap + # doesn't apply. We must manually unwrap to give JS a real Node. self.native.appendChild(widget.native.unwrap()) ###################################################################### From 4fd442dd510c0c072f19c0f8ab3b2091b13ecf08 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Thu, 16 Apr 2026 13:11:45 -0700 Subject: [PATCH 24/31] comments for clarity --- web/src/toga_web/widgets/base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/src/toga_web/widgets/base.py b/web/src/toga_web/widgets/base.py index 6d993d437a..4c953dd1ea 100644 --- a/web/src/toga_web/widgets/base.py +++ b/web/src/toga_web/widgets/base.py @@ -56,13 +56,15 @@ def unwrap(self): return self._element def __getattr__(self, name): + # If we're still waiting to be upgraded and we've seen a set, return that value if not self._upgraded and name in self._pending: return self._pending[name] attr = getattr(self._element, name) + # if we're asking for a JsProxy callable, assume we're going to call it; + # return wrapper that checks all args and unwraps any NativeProxys + # to their underlying JsProxy elements if isinstance(attr, JsProxy) and callable(attr): - # Wrap JS methods so any NativeProxy arguments are automatically - # unwrapped to their underlying JsProxy elements before the call. def _auto_unwrap(*args): unwrapped_args = [ a.unwrap() if isinstance(a, NativeProxy) else a for a in args @@ -70,6 +72,8 @@ def _auto_unwrap(*args): return attr(*unwrapped_args) return _auto_unwrap + + # otherwise just return what was asked for return attr def __setattr__(self, name, value): From db21b903b430b3c4e8f19cb4149416eb4c6519f1 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 19 Apr 2026 20:17:09 +0800 Subject: [PATCH 25/31] Correct some widget registrations. --- examples/passwordinput/LICENSE | 1 + web/pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 examples/passwordinput/LICENSE diff --git a/examples/passwordinput/LICENSE b/examples/passwordinput/LICENSE new file mode 100644 index 0000000000..f09583bfab --- /dev/null +++ b/examples/passwordinput/LICENSE @@ -0,0 +1 @@ +Released under the same license as Toga. See the root of the Toga repository for details. diff --git a/web/pyproject.toml b/web/pyproject.toml index b88cb9a213..267fb920f5 100644 --- a/web/pyproject.toml +++ b/web/pyproject.toml @@ -80,7 +80,7 @@ SimpleStatusIcon = "toga_web.statusicons:SimpleStatusIcon" StatusIconSet = "toga_web.statusicons:StatusIconSet" # Widgets -# ActivityIndicator = "toga_web.widgets.activityindicator:ActivityIndicator" +ActivityIndicator = "toga_web.widgets.activityindicator:ActivityIndicator" Box = "toga_web.widgets.box:Box" Button = "toga_web.widgets.button:Button" Canvas = "toga_web.widgets.canvas:Canvas" @@ -102,7 +102,7 @@ Slider = "toga_web.widgets.slider:Slider" Switch = "toga_web.widgets.switch:Switch" # Table = "toga_web.widgets.table:Table" TextInput = "toga_web.widgets.textinput:TextInput" -# TimeInput = "toga_web.widgets.timeinput:TimeInput" +TimeInput = "toga_web.widgets.timeinput:TimeInput" # Tree = "toga_web.widgets.tree:Tree" # WebView = "toga_web.widgets.webview:WebView" From aef4509170ad88c6833e6aff03c0e79777adb13b Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Sun, 26 Apr 2026 12:06:44 -0700 Subject: [PATCH 26/31] formatting --- web/src/toga_web/widgets/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/toga_web/widgets/base.py b/web/src/toga_web/widgets/base.py index 4c953dd1ea..f0d27cdcad 100644 --- a/web/src/toga_web/widgets/base.py +++ b/web/src/toga_web/widgets/base.py @@ -65,6 +65,7 @@ def __getattr__(self, name): # return wrapper that checks all args and unwraps any NativeProxys # to their underlying JsProxy elements if isinstance(attr, JsProxy) and callable(attr): + def _auto_unwrap(*args): unwrapped_args = [ a.unwrap() if isinstance(a, NativeProxy) else a for a in args From cab47742e76653e2b1cfe39d67c4ce105dafd3cb Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Sun, 26 Apr 2026 13:28:52 -0700 Subject: [PATCH 27/31] Fix Box.insert_child and remove_child in web backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit insert_child was ignoring the index and always appending; remove_child was only clearing the Python container reference without touching the DOM. In the Box example, removing the yellow button does not reset the background color — the Switch controls button availability, not color state. --- web/src/toga_web/widgets/box.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/src/toga_web/widgets/box.py b/web/src/toga_web/widgets/box.py index 04927a6688..030d3e4fdc 100644 --- a/web/src/toga_web/widgets/box.py +++ b/web/src/toga_web/widgets/box.py @@ -7,3 +7,14 @@ def create(self): def add_child(self, child): self.native.appendChild(child.native) + + def insert_child(self, index, child): + children = self.native.children + if index < children.length: + self.native.insertBefore(child.native, children.item(index)) + else: + self.native.appendChild(child.native) + + def remove_child(self, child): + self.native.removeChild(child.native) + child.container = None From 4e24e4a43d6f6a0448da5d3b4522333228360af1 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Sun, 26 Apr 2026 14:18:28 -0700 Subject: [PATCH 28/31] Activate WA shoelace compatibility theme; adjust header color The shoelace.css compatibility theme was loading but silently doing nothing: its palette and theme rules are scoped to .wa-palette-shoelace and .wa-theme-shoelace respectively, and neither class was present on . Added a synchronous script to index.html~head that sets both classes before CSS is evaluated. With the shoelace palette now active, --wa-color-brand-40 (rgb(0, 93, 147)) is closer to the original Shoelace --sl-color-primary-800 (rgb(7, 89, 133)) than --wa-color-brand-30 was. --- web/src/toga_web/deploy/inserts/index.html~head | 3 ++- web/src/toga_web/static/toga.css | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/web/src/toga_web/deploy/inserts/index.html~head b/web/src/toga_web/deploy/inserts/index.html~head index c35a6d88f8..aeb9605543 100644 --- a/web/src/toga_web/deploy/inserts/index.html~head +++ b/web/src/toga_web/deploy/inserts/index.html~head @@ -1,4 +1,5 @@ - + + diff --git a/web/src/toga_web/static/toga.css b/web/src/toga_web/static/toga.css index 43cd51a0ba..d0b90ab7d5 100644 --- a/web/src/toga_web/static/toga.css +++ b/web/src/toga_web/static/toga.css @@ -27,7 +27,7 @@ html, body { **********************************************************************/ header.toga { - background-color: var(--wa-color-brand-30); + background-color: var(--wa-color-brand-40); color: var(--wa-color-brand-on-loud); display: flex; flex-direction: row; @@ -67,7 +67,7 @@ header.toga nav.menubar .menu { } header.toga nav.menubar .menu:hover { - background-color: var(--wa-color-brand-30); + background-color: var(--wa-color-brand-40); color: var(--wa-color-brand-on-loud); } From 327e369a1746b73fad8fdf6079481147a75481b4 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Sun, 26 Apr 2026 17:18:42 -0700 Subject: [PATCH 29/31] Fix ActivityIndicator visibility: use classList instead of inline style _reapply_style replays style.cssText on element upgrade, which erases any inline visibility set by stop(). Use a CSS class instead so the two mechanisms don't interfere. --- web/src/toga_web/static/toga.css | 5 +++++ web/src/toga_web/widgets/activityindicator.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/web/src/toga_web/static/toga.css b/web/src/toga_web/static/toga.css index d0b90ab7d5..05d5404549 100644 --- a/web/src/toga_web/static/toga.css +++ b/web/src/toga_web/static/toga.css @@ -107,6 +107,11 @@ main.toga.window > .container { white-space: nowrap; } +/***** ActivityIndicator *********************************************/ +wa-spinner.stopped { + visibility: hidden; +} + /***** Slider *********************************************************/ /* wa-slider defaults to display:inline (custom element default), which ignores width and doesn't stretch in flex containers. sl-range set diff --git a/web/src/toga_web/widgets/activityindicator.py b/web/src/toga_web/widgets/activityindicator.py index 5266b78a94..488c3f580e 100644 --- a/web/src/toga_web/widgets/activityindicator.py +++ b/web/src/toga_web/widgets/activityindicator.py @@ -8,11 +8,11 @@ def create(self): # Actions def start(self): - self.native.style.visibility = "visible" + self.native.classList.remove("stopped") self._is_running = True def stop(self): - self.native.style.visibility = "hidden" + self.native.classList.add("stopped") self._is_running = False def is_running(self): From 374cfc8cf29fb587c4155901e55b82189255533d Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Sun, 26 Apr 2026 17:59:59 -0700 Subject: [PATCH 30/31] Fix Slider on_release routing via document-level pointerup listener wa-slider is built without pointer capture (sl-range wrapped a native which the browser captures implicitly). Without capture, pointerup fires on whatever element is under the pointer at release time, so on_release could miss entirely or fire on a different slider. Move the listener to document with a _dragging guard. --- web/src/toga_web/widgets/slider.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/web/src/toga_web/widgets/slider.py b/web/src/toga_web/widgets/slider.py index f2734b2c04..08a805bbcd 100644 --- a/web/src/toga_web/widgets/slider.py +++ b/web/src/toga_web/widgets/slider.py @@ -1,3 +1,5 @@ +import js + from toga_web.libs import create_proxy from .base import Widget @@ -5,12 +7,19 @@ class Slider(Widget): def create(self): + self._dragging = False self.native = self._create_native_widget("wa-slider") self.native.addEventListener("input", create_proxy(self.dom_input)) self.native.addEventListener( "pointerdown", create_proxy(self.dom_onpointerdown) ) - self.native.addEventListener("pointerup", create_proxy(self.dom_onpointerup)) + # wa-slider has no pointer capture (unlike sl-range, which wrapped a native + # that the browser captures implicitly). Without capture, + # releasing outside the element fires pointerup on whatever is under the + # pointer,not on the slider — so on_release would miss and could fire on a + # different widget. Listening on document with a _dragging guard fixes both + # problems. + js.document.addEventListener("pointerup", create_proxy(self.dom_onpointerup)) def dom_input(self, event): self.interface.value = float(self.native.value) @@ -18,10 +27,13 @@ def dom_input(self, event): self.interface.on_change() def dom_onpointerdown(self, event): + self._dragging = True self.interface.on_press() def dom_onpointerup(self, event): - self.interface.on_release() + if self._dragging: + self._dragging = False + self.interface.on_release() def get_value(self): try: From 451ae0628dcba6617d4ac8edda0c669d2d251bd9 Mon Sep 17 00:00:00 2001 From: Scott Halgrim Date: Sun, 26 Apr 2026 19:34:05 -0700 Subject: [PATCH 31/31] Fix text color on wa-switch and wa-button (pre-existing bug) Both components set text color via CSS variables inside their shadow DOM (--wa-form-control-value-color for wa-switch, --wa-color-on-quiet for wa-button), so the host element's inherited 'color' property never reached the label text. Override _reapply_style in each to forward the Pack color as the appropriate CSS variable in the inline style string. Switch: restores feature parity with sl-switch, which let the host color cascade through naturally. Button: fixes a pre-existing bug -- sl-button also blocked inherited color the same way, so button text color was silently ignored on both branches. --- web/src/toga_web/widgets/button.py | 9 +++++++++ web/src/toga_web/widgets/switch.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/web/src/toga_web/widgets/button.py b/web/src/toga_web/widgets/button.py index 0876a7f26b..eebbd1f956 100644 --- a/web/src/toga_web/widgets/button.py +++ b/web/src/toga_web/widgets/button.py @@ -32,5 +32,14 @@ def set_enabled(self, value): def set_background_color(self, value): pass + def _reapply_style(self): + # wa-button (appearance="outlined") sets text color via --wa-color-on-quiet + # inside its shadow DOM, so the host's inherited `color` doesn't reach the + # button label. Forward it as a CSS variable so the shadow DOM picks it up. + css = self.interface.style.__css__() + if color := self.interface.style.color: + css += f" --wa-color-on-quiet: {color};" + self.native.style = css + def rehint(self): pass diff --git a/web/src/toga_web/widgets/switch.py b/web/src/toga_web/widgets/switch.py index ef38019bda..678744bb1d 100644 --- a/web/src/toga_web/widgets/switch.py +++ b/web/src/toga_web/widgets/switch.py @@ -28,3 +28,12 @@ def set_value(self, value): self.native.checked = value if value != old_value: self.interface.on_change() + + def _reapply_style(self): + # wa-switch sets color via --wa-form-control-value-color inside its shadow DOM, + # so the host's inherited `color` property doesn't reach the label text. + # Forward it explicitly as a CSS variable so the shadow DOM picks it up. + css = self.interface.style.__css__() + if color := self.interface.style.color: + css += f" --wa-form-control-value-color: {color};" + self.native.style = css