From b01cd3bfa34f89eb6e5450ed1eaa5e92eacdec2d Mon Sep 17 00:00:00 2001 From: Miika Kuisma Date: Wed, 24 Jun 2026 20:47:17 +0300 Subject: [PATCH] add dark theme support for puter.ui.contextMenu and puter.ui.setMenubar (works only with puter.env === 'web') --- src/puter-js/src/modules/UI.js | 8 +++ src/puter-js/src/ui/components.md | 9 +++ src/puter-js/test/web-components-test.html | 68 ++++++++++++++++++- .../e2e/fixtures/menubar-contextmenu.html | 33 +++++++++ .../specs/menubar-contextmenu-theme.spec.js | 44 ++++++++++++ src/puter-js/types/modules/ui.d.ts | 12 ++++ 6 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/puter-js/tests/e2e/specs/menubar-contextmenu-theme.spec.js diff --git a/src/puter-js/src/modules/UI.js b/src/puter-js/src/modules/UI.js index 2e7ef1c43..f996319e6 100644 --- a/src/puter-js/src/modules/UI.js +++ b/src/puter-js/src/modules/UI.js @@ -1096,6 +1096,10 @@ class UI extends EventListener { // Replace any existing menubar document.querySelectorAll('puter-menubar').forEach(el => el.remove()); const el = document.createElement('puter-menubar'); + // Forward an explicit theme ('dark' | 'light') to the web component; + // unset → the component follows the system preference. The component + // also forwards this to the dropdowns it spawns. (env=web only.) + if ( spec.theme ) el.setAttribute('theme', spec.theme); el.items = spec.items || []; document.body.appendChild(el); }; @@ -1137,6 +1141,10 @@ class UI extends EventListener { } // Standalone fallback: render web component const el = document.createElement('puter-context-menu'); + // Forward an explicit theme ('dark' | 'light') to the web component; + // unset → the component follows the system preference. The component + // also forwards this to any submenus it spawns. (env=web only.) + if ( spec.theme ) el.setAttribute('theme', spec.theme); el.items = spec.items || []; // Use mouse position or provided position const x = spec.x ?? (globalThis.event?.clientX ?? 0); diff --git a/src/puter-js/src/ui/components.md b/src/puter-js/src/ui/components.md index 555eafa6b..63156d6d8 100644 --- a/src/puter-js/src/ui/components.md +++ b/src/puter-js/src/ui/components.md @@ -360,6 +360,15 @@ To force a specific theme regardless of the OS preference, set the `theme` attri The attribute is live — changing it at runtime re-paints the component immediately. For `` and ``, the `theme` is forwarded to any dropdowns and submenus they spawn, so the whole tree stays in sync. +When you use the imperative API instead of the elements directly, pass a `theme` in the options and it is applied as the `theme` attribute on the rendered component: + +```js +puter.ui.setMenubar({ theme: 'dark', items: [ /* … */ ] }); +puter.ui.contextMenu({ theme: 'dark', items: [ /* … */ ] }); +``` + +This only takes effect when running standalone (`puter.env === 'web'`). Inside the Puter desktop (`puter.env === 'app'`) the spec is handled by the desktop and the `theme` option is ignored. + ## Responsive / mobile All components have mobile breakpoints at `@media (max-width: 480px)`. Notable behaviors: diff --git a/src/puter-js/test/web-components-test.html b/src/puter-js/test/web-components-test.html index 4a14bafe5..47ceb0179 100644 --- a/src/puter-js/test/web-components-test.html +++ b/src/puter-js/test/web-components-test.html @@ -78,6 +78,7 @@

Context Menu

+ @@ -89,6 +90,7 @@

Menubar

+
@@ -107,7 +109,7 @@
Click to test...
- + diff --git a/src/puter-js/tests/e2e/specs/menubar-contextmenu-theme.spec.js b/src/puter-js/tests/e2e/specs/menubar-contextmenu-theme.spec.js new file mode 100644 index 000000000..3affd0083 --- /dev/null +++ b/src/puter-js/tests/e2e/specs/menubar-contextmenu-theme.spec.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test'; +import { FIXTURE_URL } from '../helpers/testApp.js'; + +// The `theme` option on setMenubar()/contextMenu() only applies when puter.js +// runs standalone (puter.env === 'web'). Loading the fixture directly on its +// own origin (rather than as an app inside the Puter desktop) puts the SDK in +// env=web, which renders the web components locally — exactly the path that +// reads spec.theme and forwards it as the `theme` attribute. +test.describe('puter.ui setMenubar/contextMenu theme option (env=web)', () => { + test.beforeEach(async ({ page }) => { + await page.goto(FIXTURE_URL); + await page.locator('body.ready').waitFor({ timeout: 30_000 }); + // Sanity: confirm we are actually in the standalone web environment. + const env = await page.evaluate(() => window.puter?.env); + expect(env).toBe('web'); + }); + + test('setMenubar({ theme: "dark" }) forwards theme to ', async ({ page }) => { + await page.locator('#set-menubar-dark-btn').click(); + + const menubar = page.locator('puter-menubar'); + await expect(menubar).toHaveAttribute('theme', 'dark'); + // The base component resolves theme → toggles .puter-theme-dark on the host. + await expect(menubar).toHaveClass(/puter-theme-dark/); + }); + + test('contextMenu({ theme: "dark" }) forwards theme to ', async ({ page }) => { + await page.locator('#ctx-trigger-dark').click(); + + const menu = page.locator('puter-context-menu').last(); + await expect(menu).toHaveAttribute('theme', 'dark'); + await expect(menu).toHaveClass(/puter-theme-dark/); + }); + + test('omitting theme leaves no forced theme attribute on the menubar', async ({ page }) => { + await page.locator('#set-menubar-btn').click(); + + const menubar = page.locator('puter-menubar'); + await expect(menubar).toBeAttached(); + // No explicit theme → the component follows the system preference rather + // than a forced one, so the attribute must be absent. + await expect(menubar).not.toHaveAttribute('theme', /.*/); + }); +}); diff --git a/src/puter-js/types/modules/ui.d.ts b/src/puter-js/types/modules/ui.d.ts index 8d8d6d220..096beabc2 100644 --- a/src/puter-js/types/modules/ui.d.ts +++ b/src/puter-js/types/modules/ui.d.ts @@ -53,6 +53,12 @@ export type WindowIdentifier = string | WindowHandle; export interface ContextMenuOptions { /** Menu items and separators. Use the string `'-'` to insert a separator. */ items: (ContextMenuItem | '-')[]; + /** + * Forces the rendered menu's color theme. Only applies when running standalone + * (`puter.env === 'web'`); ignored inside the Puter desktop (`puter.env === 'app'`). + * When unset, the menu follows the system color-scheme preference. + */ + theme?: 'dark' | 'light'; } /** Options that configure a window created by `createWindow()`. */ @@ -113,6 +119,12 @@ export interface ThemeData { export interface MenubarOptions { /** Menu items and separators. Use the string `'-'` to insert a separator. */ items: (MenuItem | '-')[]; + /** + * Forces the rendered menubar's color theme. Only applies when running standalone + * (`puter.env === 'web'`); ignored inside the Puter desktop (`puter.env === 'app'`). + * When unset, the menubar follows the system color-scheme preference. + */ + theme?: 'dark' | 'light'; } /** A single item in a menubar menu. The string `'-'` may be used in place of an item to render a separator. */