+
-
+
${repeat(
this._cultures,
@@ -75,12 +104,26 @@ export class UmbPreviewCultureElement extends UmbLitElement {
--uui-button-font-weight: 400;
--uui-button-padding-left-factor: 3;
--uui-button-padding-right-factor: 3;
+ --uui-menu-item-flat-structure: 1;
+ }
+
+ :host([hidden]) {
+ display: none;
+ }
+
+ #expand-symbol {
+ transform: rotate(-90deg);
+ margin-left: var(--uui-size-space-3, 9px);
+
+ &[open] {
+ transform: rotate(0deg);
+ }
}
uui-button > div {
display: flex;
align-items: center;
- gap: 5px;
+ gap: var(--uui-size-2, 6px);
}
umb-popover-layout {
diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-device.element.ts b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-device.element.ts
similarity index 57%
rename from src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-device.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-device.element.ts
index 92cb0412b1..bb0a24baf1 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-device.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-device.element.ts
@@ -1,65 +1,66 @@
-import { UMB_PREVIEW_CONTEXT } from '../preview.context.js';
-import { css, customElement, html, property, repeat } from '@umbraco-cms/backoffice/external/lit';
+import { UMB_PREVIEW_CONTEXT } from '../context/preview.context-token.js';
+import type { UmbPopoverToggleEvent } from './types.js';
+import { css, customElement, html, ifDefined, property, query, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import type { UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui';
export interface UmbPreviewDevice {
alias: string;
label: string;
- css: string;
icon: string;
+ iconClass?: string;
dimensions: { height: string; width: string };
}
@customElement('umb-preview-device')
export class UmbPreviewDeviceElement extends UmbLitElement {
+ @query('#devices-popover')
+ private _popoverElement?: UUIPopoverContainerElement;
+
+ // TODO: [LK] Eventually, convert these devices to be an extension point.
#devices: Array = [
{
alias: 'fullsize',
label: 'Fit browser',
- css: 'fullsize',
icon: 'icon-application-window-alt',
dimensions: { height: '100%', width: '100%' },
},
{
alias: 'desktop',
label: 'Desktop',
- css: 'desktop shadow',
icon: 'icon-display',
dimensions: { height: '1080px', width: '1920px' },
},
{
alias: 'laptop',
label: 'Laptop',
- css: 'laptop shadow',
icon: 'icon-laptop',
dimensions: { height: '768px', width: '1366px' },
},
{
alias: 'ipad-portrait',
label: 'Tablet portrait',
- css: 'ipad-portrait shadow',
icon: 'icon-ipad',
dimensions: { height: '929px', width: '769px' },
},
{
alias: 'ipad-landscape',
label: 'Tablet landscape',
- css: 'ipad-landscape shadow flip',
icon: 'icon-ipad',
+ iconClass: 'flip',
dimensions: { height: '675px', width: '1024px' },
},
{
alias: 'smartphone-portrait',
label: 'Smartphone portrait',
- css: 'smartphone-portrait shadow',
icon: 'icon-iphone',
dimensions: { height: '640px', width: '360px' },
},
{
alias: 'smartphone-landscape',
label: 'Smartphone landscape',
- css: 'smartphone-landscape shadow flip',
icon: 'icon-iphone',
+ iconClass: 'flip',
dimensions: { height: '360px', width: '640px' },
},
];
@@ -67,44 +68,68 @@ export class UmbPreviewDeviceElement extends UmbLitElement {
@property({ attribute: false, type: Object })
device = this.#devices[0];
- override connectedCallback() {
- super.connectedCallback();
- this.#changeDevice(this.device);
+ @state()
+ private _popoverOpen = false;
+
+ constructor() {
+ super();
+ this.addEventListener('blur', this.#onBlur, true); // Use capture phase to catch blur events
}
- async #changeDevice(device: UmbPreviewDevice) {
+ override connectedCallback() {
+ super.connectedCallback();
+ this.hidden = true;
+ this.#loadDevices();
+ }
+
+ async #loadDevices() {
+ // TODO: [LK] Eventually, load devices from extension points.
+ this.hidden = false;
+ }
+
+ async #onClick(device: UmbPreviewDevice) {
if (device === this.device) return;
this.device = device;
const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT);
- previewContext?.updateIFrame({
- className: device.css,
+ await previewContext?.updateIFrame({
+ wrapperClass: device.alias,
height: device.dimensions.height,
width: device.dimensions.width,
});
+
+ // Don't close popover for device selector - users often want to quickly test multiple devices
}
+ #onPopoverToggle(event: UmbPopoverToggleEvent) {
+ this._popoverOpen = event.newState === 'open';
+ }
+
+ #onBlur = () => {
+ if (this._popoverOpen) {
+ this._popoverElement?.hidePopover();
+ }
+ };
+
override render() {
return html`
-
+
${this.device.label}
+
-
+
${repeat(
this.#devices,
(item) => item.alias,
(item) => html`
- this.#changeDevice(item)}>
-
+ this.#onClick(item)}>
+
`,
)}
@@ -121,12 +146,30 @@ export class UmbPreviewDeviceElement extends UmbLitElement {
--uui-button-font-weight: 400;
--uui-button-padding-left-factor: 3;
--uui-button-padding-right-factor: 3;
+ --uui-menu-item-flat-structure: 1;
+ }
+
+ :host([hidden]) {
+ display: none;
+ }
+
+ #expand-symbol {
+ transform: rotate(-90deg);
+ margin-left: var(--uui-size-space-3, 9px);
+
+ &[open] {
+ transform: rotate(0deg);
+ }
}
uui-button > div {
display: flex;
align-items: center;
- gap: 5px;
+ gap: var(--uui-size-2, 6px);
+ }
+
+ uui-icon.flip {
+ transform: rotate(90deg);
}
umb-popover-layout {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts
new file mode 100644
index 0000000000..8b0bf0452a
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts
@@ -0,0 +1,170 @@
+import { UmbPreviewRepository } from '../repository/preview.repository.js';
+import { UMB_PREVIEW_CONTEXT } from '../context/preview.context-token.js';
+import { css, customElement, html, nothing, repeat, state } from '@umbraco-cms/backoffice/external/lit';
+import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
+import { umbPeekError } from '@umbraco-cms/backoffice/notification';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import type { UmbPopoverToggleEvent } from './types.js';
+
+type UmbPreviewEnvironmentItem = {
+ alias: string;
+ label: string;
+ icon: string;
+ urlProviderAlias?: string;
+};
+
+@customElement('umb-preview-environments')
+export class UmbPreviewEnvironmentsElement extends UmbLitElement {
+ #fallbackIcon = 'icon-multiple-windows';
+
+ @state()
+ private _culture?: string;
+
+ @state()
+ private _items: Array = [];
+
+ @state()
+ private _popoverOpen = false;
+
+ @state()
+ private _segment?: string;
+
+ @state()
+ private _unique?: string;
+
+ #previewRepository = new UmbPreviewRepository(this);
+
+ constructor() {
+ super();
+
+ this.consumeContext(UMB_PREVIEW_CONTEXT, (context) => {
+ this.observe(context?.unique, (unique) => (this._unique = unique), '_observeUnique');
+ this.observe(context?.culture, (culture) => (this._culture = culture), '_observeCulture');
+ this.observe(context?.segment, (segment) => (this._segment = segment), '_observeSegment');
+ });
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ this.hidden = true;
+ this.#getPreviewOptions();
+ }
+
+ async #getPreviewOptions() {
+ this.observe(
+ umbExtensionsRegistry.byTypeAndFilter('workspaceActionMenuItem', (ext) => ext.kind === 'previewOption'),
+ (manifests) => {
+ this._items = manifests.map((manifest) => ({
+ alias: manifest.alias,
+ label: (manifest.meta as any).label || manifest.name,
+ icon: (manifest.meta as any).icon || this.#fallbackIcon,
+ urlProviderAlias: (manifest.meta as any).urlProviderAlias,
+ }));
+
+ this.hidden = !this._items.length;
+ },
+ );
+ }
+
+ async #onClick(item: UmbPreviewEnvironmentItem) {
+ if (!this._unique) return;
+ if (!item.urlProviderAlias) return;
+
+ const previewUrlData = await this.#previewRepository.getPreviewUrl(
+ this._unique,
+ item.urlProviderAlias,
+ this._culture,
+ this._segment,
+ );
+
+ if (previewUrlData.url) {
+ const previewWindow = window.open(previewUrlData.url, `umbpreview-${this._unique}`);
+ previewWindow?.focus();
+ return;
+ }
+
+ if (previewUrlData.message) {
+ umbPeekError(this, {
+ color: 'danger',
+ headline: this.localize.term('general_preview'),
+ message: previewUrlData.message,
+ });
+ }
+ }
+
+ #onPopoverToggle(event: UmbPopoverToggleEvent) {
+ this._popoverOpen = event.newState === 'open';
+ }
+
+ override render() {
+ if (!this._items.length) return nothing;
+ return html`
+
+
+
+ Preview environments
+
+
+
+
+
+ ${repeat(
+ this._items,
+ (item) => item.alias,
+ (item) => html`
+ this.#onClick(item)}>
+
+
+ `,
+ )}
+
+
+ `;
+ }
+
+ static override styles = [
+ css`
+ :host {
+ display: flex;
+ border-left: 1px solid var(--uui-color-header-contrast);
+ --uui-button-font-weight: 400;
+ --uui-button-padding-left-factor: 3;
+ --uui-button-padding-right-factor: 3;
+ --uui-menu-item-flat-structure: 1;
+ }
+
+ :host([hidden]) {
+ display: none;
+ }
+
+ #expand-symbol {
+ transform: rotate(-90deg);
+ margin-left: var(--uui-size-space-3, 9px);
+
+ &[open] {
+ transform: rotate(0deg);
+ }
+ }
+
+ uui-button > div {
+ display: flex;
+ align-items: center;
+ gap: var(--uui-size-2, 6px);
+ }
+
+ umb-popover-layout {
+ --uui-color-surface: var(--uui-color-header-surface);
+ --uui-color-border: var(--uui-color-header-surface);
+ color: var(--uui-color-header-contrast);
+ }
+ `,
+ ];
+}
+
+export { UmbPreviewEnvironmentsElement as element };
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'umb-preview-environments': UmbPreviewEnvironmentsElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-exit.element.ts b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-exit.element.ts
similarity index 91%
rename from src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-exit.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-exit.element.ts
index c1a45b753d..80ccec9c7b 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-exit.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-exit.element.ts
@@ -1,4 +1,4 @@
-import { UMB_PREVIEW_CONTEXT } from '../preview.context.js';
+import { UMB_PREVIEW_CONTEXT } from '../context/preview.context-token.js';
import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -33,7 +33,7 @@ export class UmbPreviewExitElement extends UmbLitElement {
uui-button > div {
display: flex;
align-items: center;
- gap: 5px;
+ gap: var(--uui-size-2, 6px);
}
`,
];
diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-open-website.element.ts b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-open-website.element.ts
similarity index 91%
rename from src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-open-website.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-open-website.element.ts
index 4a77454df8..7021ad9426 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-open-website.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-open-website.element.ts
@@ -1,4 +1,4 @@
-import { UMB_PREVIEW_CONTEXT } from '../preview.context.js';
+import { UMB_PREVIEW_CONTEXT } from '../context/preview.context-token.js';
import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -33,7 +33,7 @@ export class UmbPreviewOpenWebsiteElement extends UmbLitElement {
uui-button > div {
display: flex;
align-items: center;
- gap: 5px;
+ gap: var(--uui-size-2, 6px);
}
`,
];
diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-segment.element.ts b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-segment.element.ts
similarity index 62%
rename from src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-segment.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-segment.element.ts
index dc49628d49..c3817d1d95 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-segment.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-segment.element.ts
@@ -1,28 +1,35 @@
-import { UMB_PREVIEW_CONTEXT } from '../preview.context.js';
-import {
- css,
- customElement,
- html,
- nothing,
- repeat,
- state,
- type PropertyValues,
-} from '@umbraco-cms/backoffice/external/lit';
+import { UMB_PREVIEW_CONTEXT } from '../context/preview.context-token.js';
+import type { UmbPopoverToggleEvent } from './types.js';
+import { css, customElement, html, nothing, query, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
-import { UmbSegmentCollectionRepository, type UmbSegmentCollectionItemModel } from '@umbraco-cms/backoffice/segment';
+import { UmbSegmentCollectionRepository } from '@umbraco-cms/backoffice/segment';
+import type { UmbSegmentCollectionItemModel } from '@umbraco-cms/backoffice/segment';
+import type { UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui';
@customElement('umb-preview-segment')
export class UmbPreviewSegmentElement extends UmbLitElement {
#segmentRepository = new UmbSegmentCollectionRepository(this);
+ @query('#segments-popover')
+ private _popoverElement?: UUIPopoverContainerElement;
+
@state()
private _segment?: UmbSegmentCollectionItemModel;
@state()
private _segments: Array = [];
- protected override firstUpdated(_changedProperties: PropertyValues): void {
- super.firstUpdated(_changedProperties);
+ @state()
+ private _popoverOpen = false;
+
+ constructor() {
+ super();
+ this.addEventListener('blur', this.#onBlur, true); // Use capture phase to catch blur events
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+ this.hidden = true;
this.#loadSegments();
}
@@ -36,6 +43,8 @@ export class UmbPreviewSegmentElement extends UmbLitElement {
if (segment && segment !== this._segment?.unique) {
this._segment = this._segments.find((c) => c.unique === segment);
}
+
+ this.hidden = !this._segments.length;
}
async #onClick(segment?: UmbSegmentCollectionItemModel) {
@@ -44,8 +53,20 @@ export class UmbPreviewSegmentElement extends UmbLitElement {
const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT);
previewContext?.updateIFrame({ segment: segment?.unique });
+
+ this._popoverElement?.hidePopover();
}
+ #onPopoverToggle(event: UmbPopoverToggleEvent) {
+ this._popoverOpen = event.newState === 'open';
+ }
+
+ #onBlur = () => {
+ if (this._popoverOpen) {
+ this._popoverElement?.hidePopover();
+ }
+ };
+
override render() {
if (!this._segments.length) return nothing;
return html`
@@ -54,8 +75,9 @@ export class UmbPreviewSegmentElement extends UmbLitElement {
${this._segment?.name ?? 'Segments'}