mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-23 13:46:11 +00:00
add dark theme support for puter.ui.contextMenu and puter.ui.setMenubar (works only with puter.env === 'web')
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 `<puter-menubar>` and `<puter-context-menu>`, 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:
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
<h2>Context Menu</h2>
|
||||
<button oncontextmenu="testContextMenu(event); return false;">Right-click me</button>
|
||||
<button onclick="testContextMenuClick(event)">Click for Context Menu</button>
|
||||
<button oncontextmenu="testContextMenuDark(event); return false;">Right-click me for dark menu</button>
|
||||
<div class="result" id="menu-result">Right-click or click a button to test...</div>
|
||||
</div>
|
||||
|
||||
@@ -89,6 +90,7 @@
|
||||
<div class="section">
|
||||
<h2>Menubar</h2>
|
||||
<button onclick="testMenubar()">Show Menubar</button>
|
||||
<button onclick="testMenubarDark()">Show Dark Menubar</button>
|
||||
<button onclick="hideMenubar()">Hide Menubar</button>
|
||||
<div class="result" id="menubar-result">Click "Show Menubar" to add an app menubar at top of page...</div>
|
||||
</div>
|
||||
@@ -107,7 +109,7 @@
|
||||
<div class="result" id="font-result">Click to test...</div>
|
||||
</div>
|
||||
|
||||
<script src="/dist/puter.js"></script>
|
||||
<script src="/dist/puter.dev.js"></script>
|
||||
<script>
|
||||
async function testAlert() {
|
||||
const result = await puter.ui.alert('Hello from standalone Puter!');
|
||||
@@ -217,6 +219,31 @@
|
||||
});
|
||||
}
|
||||
|
||||
function testContextMenuDark(e) {
|
||||
puter.ui.contextMenu({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
theme: 'dark',
|
||||
items: [
|
||||
{ label: 'Get Info', icon: ICONS.info, action: () => log('menu', 'Get Info') },
|
||||
'-',
|
||||
{ label: 'Play Next', icon: ICONS.play, action: () => log('menu', 'Play Next') },
|
||||
{ label: 'Add to Queue', icon: ICONS.queue, action: () => log('menu', 'Add to Queue') },
|
||||
'-',
|
||||
{ label: 'Go to Artist', icon: ICONS.user, action: () => log('menu', 'Go to Artist') },
|
||||
{ label: 'Go to Album', icon: ICONS.disc, action: () => log('menu', 'Go to Album') },
|
||||
'-',
|
||||
{ label: 'Add to Playlist', icon: ICONS.plus, items: [
|
||||
{ label: 'Favorites', action: () => log('menu', 'Add to Favorites') },
|
||||
{ label: 'Workout Mix', action: () => log('menu', 'Add to Workout Mix') },
|
||||
{ label: 'Chill Vibes', action: () => log('menu', 'Add to Chill Vibes') },
|
||||
]},
|
||||
{ label: 'Download', icon: ICONS.download, action: () => log('menu', 'Download') },
|
||||
{ label: 'Delete', icon: ICONS.trash, type: 'danger', action: () => log('menu', 'Delete') },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function testContextMenuClick(e) {
|
||||
puter.ui.contextMenu({
|
||||
x: e.clientX,
|
||||
@@ -275,6 +302,45 @@
|
||||
document.getElementById('menubar-result').textContent = 'Menubar shown at top of page';
|
||||
}
|
||||
|
||||
function testMenubarDark() {
|
||||
puter.ui.setMenubar({
|
||||
theme: 'dark',
|
||||
items: [
|
||||
{ label: 'File', items: [
|
||||
{ label: 'New', icon: ICONS.plus, shortcut: '\u2318N', action: () => log('menubar', 'New') },
|
||||
{ label: 'Open\u2026', shortcut: '\u2318O', action: () => log('menubar', 'Open') },
|
||||
'-',
|
||||
{ label: 'Save', shortcut: '\u2318S', action: () => log('menubar', 'Save') },
|
||||
{ label: 'Save As\u2026', shortcut: '\u21E7\u2318S', action: () => log('menubar', 'Save As') },
|
||||
'-',
|
||||
{ label: 'Close Window', shortcut: '\u2318W', action: () => log('menubar', 'Close') },
|
||||
]},
|
||||
{ label: 'Edit', items: [
|
||||
{ label: 'Undo', icon: ICONS.cut, shortcut: '\u2318Z', action: () => log('menubar', 'Undo') },
|
||||
{ label: 'Redo', shortcut: '\u21E7\u2318Z', action: () => log('menubar', 'Redo') },
|
||||
'-',
|
||||
{ label: 'Cut', icon: ICONS.cut, shortcut: '\u2318X', action: () => log('menubar', 'Cut') },
|
||||
{ label: 'Copy', icon: ICONS.copy, shortcut: '\u2318C', action: () => log('menubar', 'Copy') },
|
||||
{ label: 'Paste', icon: ICONS.paste, shortcut: '\u2318V', action: () => log('menubar', 'Paste') },
|
||||
'-',
|
||||
{ label: 'Delete', icon: ICONS.trash, type: 'danger', action: () => log('menubar', 'Delete') },
|
||||
]},
|
||||
{ label: 'View', items: [
|
||||
{ label: 'Show Sidebar', checked: true, action: () => log('menubar', 'Toggle Sidebar') },
|
||||
{ label: 'Show Toolbar', checked: false, action: () => log('menubar', 'Toggle Toolbar') },
|
||||
'-',
|
||||
{ label: 'Zoom In', shortcut: '\u2318+', action: () => log('menubar', 'Zoom In') },
|
||||
{ label: 'Zoom Out', shortcut: '\u2318-', action: () => log('menubar', 'Zoom Out') },
|
||||
]},
|
||||
{ label: 'Help', items: [
|
||||
{ label: 'Documentation', icon: ICONS.info, action: () => log('menubar', 'Docs') },
|
||||
{ label: 'About', action: () => log('menubar', 'About') },
|
||||
]},
|
||||
],
|
||||
});
|
||||
document.getElementById('menubar-result').textContent = 'Menubar shown at top of page';
|
||||
}
|
||||
|
||||
function hideMenubar() {
|
||||
document.querySelectorAll('puter-menubar').forEach(el => el.remove());
|
||||
document.getElementById('menubar-result').textContent = 'Menubar removed';
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
<button id="set-menubar-btn" disabled>Set Menubar</button>
|
||||
<button id="ctx-trigger" disabled>Show Context Menu</button>
|
||||
<button id="set-menubar-dark-btn" disabled>Set Menubar (dark)</button>
|
||||
<button id="ctx-trigger-dark" disabled>Show Context Menu (dark)</button>
|
||||
|
||||
<div id="log" data-testid="log"></div>
|
||||
|
||||
@@ -36,6 +38,8 @@
|
||||
statusEl.textContent = `Ready (env=${puter.env})`;
|
||||
document.getElementById('set-menubar-btn').disabled = false;
|
||||
document.getElementById('ctx-trigger').disabled = false;
|
||||
document.getElementById('set-menubar-dark-btn').disabled = false;
|
||||
document.getElementById('ctx-trigger-dark').disabled = false;
|
||||
}
|
||||
|
||||
function waitForPuter() {
|
||||
@@ -68,6 +72,35 @@
|
||||
});
|
||||
log('ctx:opened');
|
||||
});
|
||||
|
||||
// Themed variants — exercise the `theme` option, which is forwarded as
|
||||
// the `theme` attribute onto the standalone (env=web) web components.
|
||||
document.getElementById('set-menubar-dark-btn').addEventListener('click', () => {
|
||||
puter.ui.setMenubar({
|
||||
theme: 'dark',
|
||||
items: [
|
||||
{ label: 'TestFile', action: () => log('menubar:TestFile') },
|
||||
{ label: 'TestEdit', items: [
|
||||
{ label: 'TestUndo', action: () => log('menubar:TestUndo') },
|
||||
{ label: 'TestRedo', action: () => log('menubar:TestRedo') },
|
||||
]},
|
||||
],
|
||||
});
|
||||
log('menubar:set:dark');
|
||||
});
|
||||
|
||||
document.getElementById('ctx-trigger-dark').addEventListener('click', () => {
|
||||
puter.ui.contextMenu({
|
||||
theme: 'dark',
|
||||
items: [
|
||||
{ label: 'CtxAlpha', action: () => log('ctx:CtxAlpha') },
|
||||
{ label: 'CtxBeta', action: () => log('ctx:CtxBeta') },
|
||||
'-',
|
||||
{ label: 'CtxGamma', action: () => log('ctx:CtxGamma') },
|
||||
],
|
||||
});
|
||||
log('ctx:opened:dark');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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 <puter-menubar>', 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 <puter-context-menu>', 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', /.*/);
|
||||
});
|
||||
});
|
||||
Vendored
+12
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user