mirror of
https://github.com/withastro/astro.git
synced 2024-12-16 21:46:22 -05:00
feat(toolbar): allow the user to change the placement (#10591)
* feat(toolbar): add `placement` to settings * feat(toolbar): update `settings.placement` with `<select>` * feat(toolbar): adjust position based on `settings.placement` * test(toolbar): add a test case for `settings.placement` * refactor(toolbar): extract select element from settings app * feat(toolbar): allow select element to have colors * test(toolbar): fix failed test case * refactor(toolbar): add `placement` property to window element * refactor(toolbar): notify apps when placement changes * test(toolbar): fix failed test case * refactor(toolbar): extract `synchronizePlacementOnUpdate` function * chore: add changeset * chore: update changeset Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> --------- Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
3807e3e868
commit
39988ef8e2
13 changed files with 313 additions and 13 deletions
5
.changeset/dry-eels-yell.md
Normal file
5
.changeset/dry-eels-yell.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"astro": minor
|
||||
---
|
||||
|
||||
Adds a new dev toolbar settings option to change the horizontal placement of the dev toolbar on your screen: bottom left, bottom center, or bottom right.
|
|
@ -349,4 +349,37 @@ test.describe('Dev Toolbar', () => {
|
|||
await expect(appButton).not.toHaveClass('active');
|
||||
}
|
||||
});
|
||||
|
||||
test('can adjust the placement', async ({ page, astro }) => {
|
||||
await page.goto(astro.resolveUrl('/audit-no-warning'));
|
||||
|
||||
const toolbar = page.locator('astro-dev-toolbar');
|
||||
const settingsAppButton = toolbar.locator('button[data-app-id="astro:settings"]');
|
||||
await settingsAppButton.click();
|
||||
|
||||
const settingsAppCanvas = toolbar.locator(
|
||||
'astro-dev-toolbar-app-canvas[data-app-id="astro:settings"]'
|
||||
);
|
||||
const settingsWindow = settingsAppCanvas.locator('astro-dev-toolbar-window');
|
||||
await expect(settingsWindow).toBeVisible();
|
||||
|
||||
for (const placement of ['bottom-left', 'bottom-center', 'bottom-right']) {
|
||||
const select = toolbar.getByRole('combobox');
|
||||
await expect(select).toBeVisible();
|
||||
await select.selectOption(placement);
|
||||
|
||||
const toolbarRoot = toolbar.locator('#dev-toolbar-root');
|
||||
await expect(toolbarRoot).toHaveAttribute('data-placement', placement);
|
||||
|
||||
for (const appId of ['astro:home', 'astro:xray', 'astro:settings']) {
|
||||
const appButton = toolbar.locator(`button[data-app-id="${appId}"]`);
|
||||
await appButton.click();
|
||||
|
||||
const appCanvas = toolbar.locator(`astro-dev-toolbar-app-canvas[data-app-id="${appId}"]`);
|
||||
const appWindow = appCanvas.locator('astro-dev-toolbar-window');
|
||||
await expect(appWindow).toBeVisible();
|
||||
await expect(appWindow).toHaveJSProperty('placement', placement);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@ import type {
|
|||
DevToolbarCard,
|
||||
DevToolbarHighlight,
|
||||
DevToolbarIcon,
|
||||
DevToolbarSelect,
|
||||
DevToolbarToggle,
|
||||
DevToolbarTooltip,
|
||||
DevToolbarWindow,
|
||||
|
@ -3003,6 +3004,7 @@ declare global {
|
|||
'astro-dev-toolbar-button': DevToolbarButton;
|
||||
'astro-dev-toolbar-icon': DevToolbarIcon;
|
||||
'astro-dev-toolbar-card': DevToolbarCard;
|
||||
'astro-dev-toolbar-select': DevToolbarSelect;
|
||||
|
||||
// Deprecated names
|
||||
// TODO: Remove in Astro 5.0
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import type { DevToolbarApp, DevToolbarMetadata } from '../../../../@types/astro.js';
|
||||
import { type Icon, isDefinedIcon } from '../ui-library/icons.js';
|
||||
import { colorForIntegration, iconForIntegration } from './utils/icons.js';
|
||||
import { closeOnOutsideClick, createWindowElement } from './utils/window.js';
|
||||
import {
|
||||
closeOnOutsideClick,
|
||||
createWindowElement,
|
||||
synchronizePlacementOnUpdate,
|
||||
} from './utils/window.js';
|
||||
|
||||
const astroLogo =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 99 26" width="100"><path fill="#fff" d="M6.70402 22.1453c-1.17459-1.0737-1.51748-3.3297-1.02811-4.9641.84853 1.0304 2.02424 1.3569 3.24204 1.5411 1.88005.2844 3.72635.178 5.47285-.6813.1998-.0984.3844-.2292.6027-.3617.1639.4755.2065.9554.1493 1.4439-.1392 1.1898-.7313 2.1088-1.673 2.8054-.3765.2787-.775.5278-1.1639.7905-1.1948.8075-1.518 1.7544-1.0691 3.1318.0107.0336.0202.0671.0444.149-.6101-.273-1.0557-.6705-1.39518-1.1931-.3586-.5517-.52921-1.1619-.53819-1.8221-.00449-.3213-.00449-.6455-.0477-.9623-.10551-.7722-.46804-1.118-1.15102-1.1379-.70094-.0205-1.2554.4129-1.40244 1.0953-.01122.0523-.02749.1041-.04377.1649l.00112.0006Z"/><path fill="url(#paint0_linear_386_2739)" d="M6.70402 22.1453c-1.17459-1.0737-1.51748-3.3297-1.02811-4.9641.84853 1.0304 2.02424 1.3569 3.24204 1.5411 1.88005.2844 3.72635.178 5.47285-.6813.1998-.0984.3844-.2292.6027-.3617.1639.4755.2065.9554.1493 1.4439-.1392 1.1898-.7313 2.1088-1.673 2.8054-.3765.2787-.775.5278-1.1639.7905-1.1948.8075-1.518 1.7544-1.0691 3.1318.0107.0336.0202.0671.0444.149-.6101-.273-1.0557-.6705-1.39518-1.1931-.3586-.5517-.52921-1.1619-.53819-1.8221-.00449-.3213-.00449-.6455-.0477-.9623-.10551-.7722-.46804-1.118-1.15102-1.1379-.70094-.0205-1.2554.4129-1.40244 1.0953-.01122.0523-.02749.1041-.04377.1649l.00112.0006Z"/><path fill="#fff" d="M0 16.909s3.47815-1.6944 6.96603-1.6944l2.62973-8.13858c.09846-.39359.38592-.66106.71044-.66106.3246 0 .612.26747.7105.66106l2.6297 8.13858c4.1309 0 6.966 1.6944 6.966 1.6944S14.7045.814589 14.693.782298C14.5234.306461 14.2371 0 13.8512 0H6.76183c-.38593 0-.66063.306461-.84174.782298C5.90733.81398 0 16.909 0 16.909ZM36.671 11.7318c0 1.4262-1.7739 2.2779-4.2302 2.2779-1.5985 0-2.1638-.3962-2.1638-1.2281 0-.8715.7018-1.2875 2.3003-1.2875 1.4426 0 2.6707.0198 4.0937.1981v.0396Zm.0195-1.7629c-.8772-.19808-2.2028-.31693-3.7818-.31693-4.6006 0-6.7644 1.08943-6.7644 3.62483 0 2.6344 1.4815 3.6446 4.9125 3.6446 2.9046 0 4.8735-.7328 5.5947-2.5354h.117c-.0195.4358-.039.8716-.039 1.2083 0 .931.156 1.0102.9162 1.0102h3.5869c-.1949-.5546-.3119-2.1194-.3119-3.4663 0-1.446.0585-2.5355.0585-4.00123 0-2.99098-1.7934-4.89253-7.4077-4.89253-2.4173 0-5.1074.41596-7.1543 1.03.1949.81213.4679 2.45617.6043 3.5258 1.774-.83193 4.2887-1.18847 6.2381-1.18847 2.6902 0 3.4309.61404 3.4309 1.86193v.4952ZM46.5325 12.5637c-.4874.0594-1.1502.0594-1.8325.0594-.7213 0-1.3841-.0198-1.8324-.0792 0 .1585-.0195.3367-.0195.4952 0 2.476 1.618 3.922 7.3102 3.922 5.3609 0 7.0958-1.4262 7.0958-3.9418 0-2.3769-1.1501-3.5456-6.238-3.8031-3.9573-.17827-4.3082-.61404-4.3082-1.10924 0-.57442.5068-.87154 3.158-.87154 2.7487 0 3.4894.37635 3.4894 1.16866v.17827c.3899-.01981 1.0917-.03961 1.813-.03961.6823 0 1.423.0198 1.8519.05942 0-.17827.0195-.33674.0195-.47539 0-2.91175-2.4172-3.86252-7.0958-3.86252-5.2634 0-7.0373 1.2875-7.0373 3.8031 0 2.25805 1.423 3.66445 6.472 3.88235 3.7233.1188 4.1327.5348 4.1327 1.1092 0 .6141-.6043.8914-3.2165.8914-3.0021 0-3.7623-.416-3.7623-1.2677v-.1189ZM63.6883 2.125c-1.423 1.32712-3.9768 2.65425-5.3998 3.01079.0195.73289.0195 2.07982.0195 2.81271l1.3061.01981c-.0195 1.40635-.039 3.10979-.039 4.23889 0 2.6344 1.3841 4.6152 5.6922 4.6152 1.813 0 3.0216-.1981 4.5226-.515-.1559-.9706-.3314-2.4562-.3898-3.5852-.8968.2971-2.0274.4556-3.275.4556-1.735 0-2.4368-.4754-2.4368-1.8422 0-1.1884 0-2.29767.0195-3.32768 2.2223.01981 4.4446.05943 5.7507.09904-.0195-1.03.0195-2.51559.078-3.50598-1.8909.03961-4.0157.05942-5.7702.05942.0195-.87154.039-1.70347.0585-2.5354h-.1365ZM75.3313 7.35427c.0195-1.03001.039-1.90156.0585-2.75329h-3.9183c.0585 1.70347.0585 3.44656.0585 6.00172 0 2.5553-.0195 4.3182-.0585 6.0018h4.4836c-.078-1.1885-.0975-3.189-.0975-4.8925 0-2.69388 1.0917-3.46638 3.5674-3.46638 1.1502 0 1.9689.13865 2.6902.39615.0195-1.01019.2144-2.97117.3314-3.84271-.7408-.21789-1.5595-.35655-2.5537-.35655-2.1249-.0198-3.6844.85174-4.4056 2.93156l-.156-.0198ZM94.8501 10.5235c0 2.1591-1.5595 3.1693-4.0157 3.1693-2.4368 0-3.9963-.9508-3.9963-3.1693 0-2.21846 1.579-3.05039 3.9963-3.05039 2.4367 0 4.0157.89135 4.0157 3.05039Zm4.0743-.099c0-4.29832-3.353-6.21968-8.09-6.21968-4.7566 0-7.9926 1.92136-7.9926 6.21968 0 4.2785 3.0216 6.5762 7.9731 6.5762 4.9904 0 8.1095-2.2977 8.1095-6.5762Z"/><defs><linearGradient id="paint0_linear_386_2739" x1="5.46011" x2="16.8017" y1="25.9999" y2="20.6412" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>';
|
||||
|
@ -45,6 +49,7 @@ export default {
|
|||
});
|
||||
|
||||
closeOnOutsideClick(eventTarget);
|
||||
synchronizePlacementOnUpdate(eventTarget, canvas);
|
||||
|
||||
function fetchIntegrationData() {
|
||||
fetch('https://astro.build/api/v1/dev-overlay/', {
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import type { DevToolbarApp } from '../../../../@types/astro.js';
|
||||
import { type Settings, settings } from '../settings.js';
|
||||
import { closeOnOutsideClick, createWindowElement } from './utils/window.js';
|
||||
import { isValidPlacement, placements } from '../ui-library/window.js';
|
||||
import {
|
||||
closeOnOutsideClick,
|
||||
createWindowElement,
|
||||
synchronizePlacementOnUpdate,
|
||||
} from './utils/window.js';
|
||||
|
||||
interface SettingRow {
|
||||
name: string;
|
||||
|
@ -43,6 +48,22 @@ const settingsRows = [
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Placement',
|
||||
description: 'Adjust the placement of the dev toolbar.',
|
||||
input: 'select',
|
||||
settingKey: 'placement',
|
||||
changeEvent: (evt: Event) => {
|
||||
if (evt.currentTarget instanceof HTMLSelectElement) {
|
||||
const placement = evt.currentTarget.value;
|
||||
if (isValidPlacement(placement)) {
|
||||
document.querySelector('astro-dev-toolbar')?.setToolbarPlacement(placement);
|
||||
settings.updateSetting('placement', placement);
|
||||
settings.logger.verboseLog(`Placement set to ${placement}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
] satisfies SettingRow[];
|
||||
|
||||
export default {
|
||||
|
@ -55,6 +76,7 @@ export default {
|
|||
document.addEventListener('astro:after-swap', createSettingsWindow);
|
||||
|
||||
closeOnOutsideClick(eventTarget);
|
||||
synchronizePlacementOnUpdate(eventTarget, canvas);
|
||||
|
||||
function createSettingsWindow() {
|
||||
const windowElement = createWindowElement(
|
||||
|
@ -161,10 +183,26 @@ export default {
|
|||
case 'checkbox': {
|
||||
const astroToggle = document.createElement('astro-dev-toolbar-toggle');
|
||||
astroToggle.input.addEventListener('change', setting.changeEvent);
|
||||
astroToggle.input.checked = settings.config[setting.settingKey];
|
||||
astroToggle.input.checked = settings.config[setting.settingKey] as boolean;
|
||||
label.append(astroToggle);
|
||||
break;
|
||||
}
|
||||
case 'select': {
|
||||
const astroSelect = document.createElement('astro-dev-toolbar-select');
|
||||
placements.forEach((placement) => {
|
||||
const option = document.createElement('option');
|
||||
option.setAttribute('value', placement);
|
||||
if (placement === settings.config[setting.settingKey]) {
|
||||
option.selected = true;
|
||||
}
|
||||
option.textContent =
|
||||
`${placement.slice(0, 1).toUpperCase()}${placement.slice(1)}`.replace('-', ' ');
|
||||
astroSelect.append(option);
|
||||
});
|
||||
astroSelect.element.addEventListener('change', setting.changeEvent);
|
||||
label.append(astroSelect);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
export function createWindowElement(content: string) {
|
||||
import { settings } from '../../settings.js';
|
||||
import type { Placement } from '../../ui-library/window.js';
|
||||
|
||||
export function createWindowElement(content: string, placement = settings.config.placement) {
|
||||
const windowElement = document.createElement('astro-dev-toolbar-window');
|
||||
windowElement.innerHTML = content;
|
||||
windowElement.placement = placement;
|
||||
return windowElement;
|
||||
}
|
||||
|
||||
|
@ -30,3 +34,17 @@ export function closeOnOutsideClick(
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function synchronizePlacementOnUpdate(eventTarget: EventTarget, canvas: ShadowRoot) {
|
||||
eventTarget.addEventListener('placement-updated', (evt) => {
|
||||
if (!(evt instanceof CustomEvent)) {
|
||||
return;
|
||||
}
|
||||
const windowElement = canvas.querySelector('astro-dev-toolbar-window');
|
||||
if (!windowElement) {
|
||||
return;
|
||||
}
|
||||
const event: CustomEvent<{ placement: Placement }> = evt;
|
||||
windowElement.placement = event.detail.placement;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,7 +7,11 @@ import {
|
|||
getElementsPositionInDocument,
|
||||
positionHighlight,
|
||||
} from './utils/highlight.js';
|
||||
import { closeOnOutsideClick, createWindowElement } from './utils/window.js';
|
||||
import {
|
||||
closeOnOutsideClick,
|
||||
createWindowElement,
|
||||
synchronizePlacementOnUpdate,
|
||||
} from './utils/window.js';
|
||||
|
||||
const icon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#fff" d="M7.9 1.5v-.4a1.1 1.1 0 0 1 2.2 0v.4a1.1 1.1 0 1 1-2.2 0Zm-6.4 8.6a1.1 1.1 0 1 0 0-2.2h-.4a1.1 1.1 0 0 0 0 2.2h.4ZM12 3.7a1.1 1.1 0 0 0 1.4-.7l.4-1.1a1.1 1.1 0 0 0-2.1-.8l-.4 1.2a1.1 1.1 0 0 0 .7 1.4Zm-9.7 7.6-1.2.4a1.1 1.1 0 1 0 .8 2.1l1-.4a1.1 1.1 0 1 0-.6-2ZM20.8 17a1.9 1.9 0 0 1 0 2.6l-1.2 1.2a1.9 1.9 0 0 1-2.6 0l-4.3-4.2-1.6 3.6a1.9 1.9 0 0 1-1.7 1.2A1.9 1.9 0 0 1 7.5 20L2.7 5a1.9 1.9 0 0 1 2.4-2.4l15 5a1.9 1.9 0 0 1 .2 3.4l-3.7 1.6 4.2 4.3ZM19 18.3 14.6 14a1.9 1.9 0 0 1 .6-3l3.2-1.5L5.1 5.1l4.3 13.3 1.5-3.2a1.9 1.9 0 0 1 3-.6l4.4 4.4.7-.7Z"/></svg>';
|
||||
|
@ -25,6 +29,7 @@ export default {
|
|||
document.addEventListener('astro:page-load', refreshIslandsOverlayPositions);
|
||||
|
||||
closeOnOutsideClick(eventTarget);
|
||||
synchronizePlacementOnUpdate(eventTarget, canvas);
|
||||
|
||||
function addIslandsOverlay() {
|
||||
islandsOverlays.forEach(({ highlightElement }) => {
|
||||
|
|
|
@ -23,6 +23,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
DevToolbarButton,
|
||||
DevToolbarBadge,
|
||||
DevToolbarIcon,
|
||||
DevToolbarSelect,
|
||||
},
|
||||
] = await Promise.all([
|
||||
loadDevToolbarApps() as DevToolbarAppDefinition[],
|
||||
|
@ -45,6 +46,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
customElements.define('astro-dev-toolbar-button', DevToolbarButton);
|
||||
customElements.define('astro-dev-toolbar-badge', DevToolbarBadge);
|
||||
customElements.define('astro-dev-toolbar-icon', DevToolbarIcon);
|
||||
customElements.define('astro-dev-toolbar-select', DevToolbarSelect);
|
||||
|
||||
// Add deprecated names
|
||||
// TODO: Remove in Astro 5.0
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import type { Placement } from './ui-library/window.js';
|
||||
|
||||
export interface Settings {
|
||||
disableAppNotification: boolean;
|
||||
verbose: boolean;
|
||||
placement: Placement;
|
||||
}
|
||||
|
||||
export const defaultSettings = {
|
||||
disableAppNotification: false,
|
||||
verbose: false,
|
||||
placement: 'bottom-center',
|
||||
} satisfies Settings;
|
||||
|
||||
export const settings = getSettings();
|
||||
|
@ -25,7 +29,7 @@ function getSettings() {
|
|||
_settings = { ..._settings, ...JSON.parse(toolbarSettings) };
|
||||
}
|
||||
|
||||
function updateSetting(key: keyof Settings, value: Settings[typeof key]) {
|
||||
function updateSetting<Key extends keyof Settings>(key: Key, value: Settings[Key]) {
|
||||
_settings[key] = value;
|
||||
localStorage.setItem('astro:dev-toolbar:settings', JSON.stringify(_settings));
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import type { DevToolbarApp as DevToolbarAppDefinition } from '../../../@types/astro.js';
|
||||
import { settings } from './settings.js';
|
||||
import { type Icon, getIconElement, isDefinedIcon } from './ui-library/icons.js';
|
||||
import { type Placement } from './ui-library/window.js';
|
||||
|
||||
export type DevToolbarApp = DevToolbarAppDefinition & {
|
||||
builtIn: boolean;
|
||||
|
@ -57,8 +58,6 @@ export class AstroDevToolbar extends HTMLElement {
|
|||
#dev-toolbar-root {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0%);
|
||||
z-index: 2000000010;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -75,6 +74,17 @@ export class AstroDevToolbar extends HTMLElement {
|
|||
opacity: 0.2;
|
||||
}
|
||||
|
||||
#dev-toolbar-root[data-placement="bottom-left"] {
|
||||
left: 16px;
|
||||
}
|
||||
#dev-toolbar-root[data-placement="bottom-center"] {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
#dev-toolbar-root[data-placement="bottom-right"] {
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
#dev-bar-hitbox-above,
|
||||
#dev-bar-hitbox-below {
|
||||
width: 100%;
|
||||
|
@ -246,9 +256,7 @@ export class AstroDevToolbar extends HTMLElement {
|
|||
width: 1px;
|
||||
}
|
||||
</style>
|
||||
<div id="dev-toolbar-root" data-hidden ${
|
||||
settings.config.disableAppNotification ? 'data-no-notification' : ''
|
||||
}>
|
||||
<div id="dev-toolbar-root" data-hidden ${settings.config.disableAppNotification ? 'data-no-notification' : ''} data-placement="${settings.config.placement}">
|
||||
<div id="dev-bar-hitbox-above"></div>
|
||||
<div id="dev-bar">
|
||||
<div id="bar-container">
|
||||
|
@ -559,6 +567,19 @@ export class AstroDevToolbar extends HTMLElement {
|
|||
?.querySelector('#dropdown')
|
||||
?.toggleAttribute('data-no-notification', !newStatus);
|
||||
}
|
||||
|
||||
setToolbarPlacement(newPlacement: Placement) {
|
||||
this.devToolbarContainer?.setAttribute('data-placement', newPlacement);
|
||||
this.apps.forEach((app) => {
|
||||
app.eventTarget.dispatchEvent(
|
||||
new CustomEvent('placement-updated', {
|
||||
detail: {
|
||||
placement: newPlacement,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DevToolbarCanvas extends HTMLElement {
|
||||
|
|
|
@ -3,6 +3,7 @@ export { DevToolbarButton } from './button.js';
|
|||
export { DevToolbarCard } from './card.js';
|
||||
export { DevToolbarHighlight } from './highlight.js';
|
||||
export { DevToolbarIcon } from './icon.js';
|
||||
export { DevToolbarSelect } from './select.js';
|
||||
export { DevToolbarToggle } from './toggle.js';
|
||||
export { DevToolbarTooltip } from './tooltip.js';
|
||||
export { DevToolbarWindow } from './window.js';
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
import { settings } from '../settings.js';
|
||||
|
||||
const styles = ['purple', 'gray', 'red', 'green', 'yellow', 'blue'] as const;
|
||||
|
||||
type SelectStyle = (typeof styles)[number];
|
||||
|
||||
export class DevToolbarSelect extends HTMLElement {
|
||||
shadowRoot: ShadowRoot;
|
||||
element: HTMLSelectElement;
|
||||
_selectStyle: SelectStyle = 'gray';
|
||||
|
||||
get selectStyle() {
|
||||
return this._selectStyle;
|
||||
}
|
||||
set selectStyle(value) {
|
||||
if (!styles.includes(value)) {
|
||||
settings.logger.error(`Invalid style: ${value}, expected one of ${styles.join(', ')}.`);
|
||||
return;
|
||||
}
|
||||
this._selectStyle = value;
|
||||
this.updateStyle();
|
||||
}
|
||||
|
||||
static observedAttributes = ['select-style'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.shadowRoot = this.attachShadow({ mode: 'open' });
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
--purple-text: rgba(224, 204, 250, 1);
|
||||
--purple-border: rgba(113, 24, 226, 1);
|
||||
|
||||
--gray-text: rgba(191, 193, 201, 1);
|
||||
--gray-border:rgba(191, 193, 201, 1);
|
||||
|
||||
--red-text: rgba(249, 196, 215, 1);
|
||||
--red-border: rgba(179, 62, 102, 1);
|
||||
|
||||
--green-text: rgba(213, 249, 196, 1);
|
||||
--green-border: rgba(61, 125, 31, 1);
|
||||
|
||||
--yellow-text: rgba(249, 233, 196, 1);
|
||||
--yellow-border: rgba(181, 138, 45, 1);
|
||||
|
||||
--blue-text: rgba(189, 195, 255, 1);
|
||||
--blue-border: rgba(54, 69, 217, 1);
|
||||
|
||||
--text-color: var(--gray-text);
|
||||
--border-color: var(--gray-border);
|
||||
}
|
||||
select {
|
||||
appearance: none;
|
||||
text-align-last: center;
|
||||
display: inline-block;
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
padding: 4px 24px 4px 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--text-color);
|
||||
background-color: transparent;
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, var(--text-color) 50%),
|
||||
linear-gradient(135deg, var(--text-color) 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 12px) calc(1em - 2px),
|
||||
calc(100% - 8px) calc(1em - 2px);
|
||||
background-size: 4px 4px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
</style>
|
||||
<style id="selected-style"></style>
|
||||
<slot></slot>
|
||||
`;
|
||||
this.element = document.createElement('select');
|
||||
this.shadowRoot.addEventListener('slotchange', (event) => {
|
||||
if (event.target instanceof HTMLSlotElement) {
|
||||
// Manually add slotted elements to <select> because it only accepts <option> as children and escapes other elements including <slot>
|
||||
this.element.append(...event.target.assignedNodes());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.shadowRoot.append(this.element);
|
||||
this.updateStyle();
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
if (this.hasAttribute('select-style')) {
|
||||
this.selectStyle = this.getAttribute('select-style') as SelectStyle;
|
||||
}
|
||||
}
|
||||
|
||||
updateStyle() {
|
||||
const style = this.shadowRoot.querySelector<HTMLStyleElement>('#selected-style');
|
||||
if (style) {
|
||||
style.innerHTML = `
|
||||
:host {
|
||||
--text-color: var(--${this.selectStyle}-text);
|
||||
--border-color: var(--${this.selectStyle}-border);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,32 @@
|
|||
import { defaultSettings, settings } from '../settings.js';
|
||||
|
||||
export const placements = ['bottom-left', 'bottom-center', 'bottom-right'] as const;
|
||||
|
||||
export type Placement = (typeof placements)[number];
|
||||
|
||||
export function isValidPlacement(value: string): value is Placement {
|
||||
return placements.map(String).includes(value);
|
||||
}
|
||||
|
||||
export class DevToolbarWindow extends HTMLElement {
|
||||
shadowRoot: ShadowRoot;
|
||||
_placement: Placement = defaultSettings.placement;
|
||||
|
||||
get placement() {
|
||||
return this._placement;
|
||||
}
|
||||
set placement(value) {
|
||||
if (!isValidPlacement(value)) {
|
||||
settings.logger.error(
|
||||
`Invalid placement: ${value}, expected one of ${placements.join(', ')}, got ${value}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._placement = value;
|
||||
this.updateStyle();
|
||||
}
|
||||
|
||||
static observedAttributes = ['placement'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -24,8 +51,6 @@ export class DevToolbarWindow extends HTMLElement {
|
|||
position: fixed;
|
||||
z-index: 999999999;
|
||||
bottom: 72px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
box-shadow: 0px 0px 0px 0px rgba(19, 21, 26, 0.30), 0px 1px 2px 0px rgba(19, 21, 26, 0.29), 0px 4px 4px 0px rgba(19, 21, 26, 0.26), 0px 10px 6px 0px rgba(19, 21, 26, 0.15), 0px 17px 7px 0px rgba(19, 21, 26, 0.04), 0px 26px 7px 0px rgba(19, 21, 26, 0.01);
|
||||
}
|
||||
|
||||
|
@ -75,8 +100,41 @@ export class DevToolbarWindow extends HTMLElement {
|
|||
line-height: 1.5em;
|
||||
}
|
||||
</style>
|
||||
<style id="selected-style"></style>
|
||||
|
||||
<slot />
|
||||
`;
|
||||
|
||||
this.updateStyle();
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
if (this.hasAttribute('placement'))
|
||||
this.placement = this.getAttribute('placement') as Placement;
|
||||
}
|
||||
|
||||
updateStyle() {
|
||||
const style = this.shadowRoot.querySelector<HTMLStyleElement>('#selected-style');
|
||||
if (style) {
|
||||
const styleMap: Record<Placement, string> = {
|
||||
'bottom-left': `
|
||||
:host {
|
||||
left: 16px;
|
||||
}
|
||||
`,
|
||||
'bottom-center': `
|
||||
:host {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
`,
|
||||
'bottom-right': `
|
||||
:host {
|
||||
right: 16px;
|
||||
}
|
||||
`,
|
||||
};
|
||||
style.innerHTML = styleMap[this.placement];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue