From 0d87d7cd0a7e9b771a52ae20604880f41139782a Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 27 Aug 2026 14:15:07 +0530 Subject: [PATCH 01/20] feat: layers panel button styles and strings --- .../Phoenix-live-preview/live-preview.css | 17 +++++++++++++++++ src/nls/root/strings.js | 2 ++ 2 files changed, 19 insertions(+) diff --git a/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css b/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css index cfc18c4e57..0b441dd511 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css +++ b/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css @@ -428,6 +428,23 @@ color: #FBB03B; } +#livePreviewLayersBtn { + border: 1px solid transparent; + border-radius: 3px; +} + +#live-preview-plugin-toolbar #livePreviewLayersBtn:hover, +#live-preview-plugin-toolbar #livePreviewLayersBtn:focus, +#live-preview-plugin-toolbar #livePreviewLayersBtn:active { + border-color: var(--lp-toolbar-border) !important; + background: transparent !important; + box-shadow: none !important; +} + +#livePreviewLayersBtn.selected { + color: #FBB03B; +} + .lp-mode-dropdown-chevron, .lp-device-size-dropdown-chevron { display: flex; diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index d3798b1d8d..3723399d95 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -775,6 +775,7 @@ define({ "LIVE_DEV_IMAGE_FOLDER_DIALOG_REMEMBER": "Don't ask again for this project", "AVAILABLE_IN_PRO_TITLE": "Available in Phoenix Pro", "DEVICE_SIZE_LIMIT_MESSAGE": "To switch between mobile, tablet, and desktop views, upgrade to Phoenix Pro.", + "LAYERS_PANEL_LIMIT_MESSAGE": "To see your page structure and select or edit any element in it, upgrade to Phoenix Pro.", "DEVICE_SIZE_NOT_ENOUGH_SPACE": "This screen doesn’t fit in the current window size — try expanding the window", "DEVICE_SIZE_CYCLE_NO_FIT": "Screen too small to switch to another device size — increase the window size", "DEVICE_SIZE_CYCLE_TOOLTIP": "Switch between mobile, tablet, and desktop", @@ -809,6 +810,7 @@ define({ "LIVE_PREVIEW_SHOW_STYLES_BAR": "Show Styles Bar", "LIVE_PREVIEW_MODE_PREFERENCE": "'{0}' shows only the webpage, '{1}' connects the webpage to your code - click on elements to jump to their code and vice versa, '{2}' provides highlighting along with advanced element manipulation", "LIVE_PREVIEW_CONFIGURE_MODES": "Configure Live Preview Modes", + "LIVE_PREVIEW_LAYERS_PANEL": "Toggle Layers Panel", "LIVE_DEV_DETACHED_REPLACED_WITH_DEVTOOLS": "Live Preview was canceled because the browser's developer tools were opened", "LIVE_DEV_DETACHED_TARGET_CLOSED": "Live Preview was canceled because the page was closed in the browser", From 3d5ec04545c4b8c7de432cd81adea29a47d72727 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 28 Aug 2026 18:05:41 +0530 Subject: [PATCH 02/20] feat: add strings for element section in layers panel --- src/nls/root/strings.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 3723399d95..9417b576a4 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -811,6 +811,22 @@ define({ "LIVE_PREVIEW_MODE_PREFERENCE": "'{0}' shows only the webpage, '{1}' connects the webpage to your code - click on elements to jump to their code and vice versa, '{2}' provides highlighting along with advanced element manipulation", "LIVE_PREVIEW_CONFIGURE_MODES": "Configure Live Preview Modes", "LIVE_PREVIEW_LAYERS_PANEL": "Toggle Layers Panel", + "LIVE_PREVIEW_LAYERS_PANEL_TITLE": "Layers Panel", + "LIVE_PREVIEW_LAYERS_DOCK_LEFT": "Dock to Left", + "LIVE_PREVIEW_LAYERS_DOCK_RIGHT": "Dock to Right", + "LIVE_PREVIEW_LAYERS_ELEMENTS": "Elements", + "LIVE_PREVIEW_LAYERS_DETAILS": "Details", + "LIVE_PREVIEW_LAYERS_SEARCH": "Search elements", + "LIVE_PREVIEW_LAYERS_REFRESH": "Refresh Elements", + "LIVE_PREVIEW_LAYERS_NO_ELEMENTS": "No elements to show", + "LIVE_PREVIEW_LAYERS_NO_MATCHES": "No matching elements", + "LIVE_PREVIEW_LAYERS_TRUNCATED": "Showing the first {0} elements", + "LIVE_PREVIEW_LAYERS_COLLAPSE_ALL": "Collapse All", + "LIVE_PREVIEW_LAYERS_EXPAND_LEVEL": "Expand to Level", + "LIVE_PREVIEW_LAYERS_LEVEL": "Level {0}", + "LIVE_PREVIEW_LAYERS_LEVEL_ALL": "All Levels", + "LIVE_PREVIEW_LAYERS_MORE_OPTIONS": "More Options", + "LIVE_PREVIEW_LAYERS_SHOW_TEXT_NODES": "Show Text Nodes", "LIVE_DEV_DETACHED_REPLACED_WITH_DEVTOOLS": "Live Preview was canceled because the browser's developer tools were opened", "LIVE_DEV_DETACHED_TARGET_CLOSED": "Live Preview was canceled because the page was closed in the browser", From edd17af9bd20a6b3f43277768524648b23ee32c4 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 28 Aug 2026 19:38:28 +0530 Subject: [PATCH 03/20] feat: add strings for details section in layers panel --- src/nls/root/strings.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 9417b576a4..7b2b9155b7 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -817,7 +817,6 @@ define({ "LIVE_PREVIEW_LAYERS_ELEMENTS": "Elements", "LIVE_PREVIEW_LAYERS_DETAILS": "Details", "LIVE_PREVIEW_LAYERS_SEARCH": "Search elements", - "LIVE_PREVIEW_LAYERS_REFRESH": "Refresh Elements", "LIVE_PREVIEW_LAYERS_NO_ELEMENTS": "No elements to show", "LIVE_PREVIEW_LAYERS_NO_MATCHES": "No matching elements", "LIVE_PREVIEW_LAYERS_TRUNCATED": "Showing the first {0} elements", @@ -825,8 +824,16 @@ define({ "LIVE_PREVIEW_LAYERS_EXPAND_LEVEL": "Expand to Level", "LIVE_PREVIEW_LAYERS_LEVEL": "Level {0}", "LIVE_PREVIEW_LAYERS_LEVEL_ALL": "All Levels", - "LIVE_PREVIEW_LAYERS_MORE_OPTIONS": "More Options", "LIVE_PREVIEW_LAYERS_SHOW_TEXT_NODES": "Show Text Nodes", + "LIVE_PREVIEW_LAYERS_HIDE_TEXT_NODES": "Hide Text Nodes", + "LIVE_PREVIEW_LAYERS_TAB_PROPERTIES": "Properties", + "LIVE_PREVIEW_LAYERS_TAB_STYLES": "Styles", + "LIVE_PREVIEW_LAYERS_NO_SELECTION": "Select an element to see its details", + "LIVE_PREVIEW_LAYERS_NOT_IN_SOURCE": "This element is not in the source file", + "LIVE_PREVIEW_LAYERS_NO_STYLES": "No styles applied", + "LIVE_PREVIEW_LAYERS_OPEN_SOURCE": "Open in editor", + "LIVE_PREVIEW_LAYERS_RULE_INACTIVE": "Not active in the current state", + "LIVE_PREVIEW_LAYERS_EDIT_STYLE": "Edit in Styles Bar", "LIVE_DEV_DETACHED_REPLACED_WITH_DEVTOOLS": "Live Preview was canceled because the browser's developer tools were opened", "LIVE_DEV_DETACHED_TARGET_CLOSED": "Live Preview was canceled because the page was closed in the browser", From bc340b1806ea77c9028ff7d08fe99ed86209c869 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 29 Aug 2026 23:29:38 +0530 Subject: [PATCH 04/20] feat: move layers panel into a sidebar tab --- .../Phoenix-live-preview/live-preview.css | 17 - .../Phoenix-live-preview/main.js | 35 +- src/nls/root/strings.js | 10 +- src/styles/Extn-LayersPanel.less | 671 ++++++++++++++++++ src/styles/brackets.less | 1 + 5 files changed, 711 insertions(+), 23 deletions(-) create mode 100644 src/styles/Extn-LayersPanel.less diff --git a/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css b/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css index 0b441dd511..cfc18c4e57 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css +++ b/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css @@ -428,23 +428,6 @@ color: #FBB03B; } -#livePreviewLayersBtn { - border: 1px solid transparent; - border-radius: 3px; -} - -#live-preview-plugin-toolbar #livePreviewLayersBtn:hover, -#live-preview-plugin-toolbar #livePreviewLayersBtn:focus, -#live-preview-plugin-toolbar #livePreviewLayersBtn:active { - border-color: var(--lp-toolbar-border) !important; - background: transparent !important; - box-shadow: none !important; -} - -#livePreviewLayersBtn.selected { - color: #FBB03B; -} - .lp-mode-dropdown-chevron, .lp-device-size-dropdown-chevron { display: flex; diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js index 1f8dc1e25b..fa73d43a52 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/main.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js @@ -45,6 +45,7 @@ define(function (require, exports, module) { FileViewController = require("project/FileViewController"), DocumentManager = require("document/DocumentManager"), ExtensionInterface = require("utils/ExtensionInterface"), + EventDispatcher = require("utils/EventDispatcher"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), Menus = require("command/Menus"), @@ -135,6 +136,9 @@ define(function (require, exports, module) { }); const LIVE_PREVIEW_PANEL_ID = "live-preview-panel"; + + /** Fired with the full path whenever the preview switches to a different file. */ + const EVENT_PREVIEWED_FILE_CHANGE = "previewedFileChange"; const LIVE_PREVIEW_IFRAME_ID = "panel-live-preview-frame"; const MDVIEWR_IFRAME_ID = "panel-md-preview-frame"; const _sandboxAttr = Phoenix.isTestWindow ? "" : @@ -614,6 +618,19 @@ define(function (require, exports, module) { _isMdviewrActive = false, $mdviewrIframe = null; // persistent md iframe, survives HTML preview switches + /** + * Records which file the preview is showing and tells listeners when that changes. + * @param {string} fullPath + * @private + */ + function _setPreviewedFile(fullPath) { + if (currentPreviewFile === fullPath) { + return; + } + currentPreviewFile = fullPath; + exports.trigger(EVENT_PREVIEWED_FILE_CHANGE, fullPath); + } + function _blankIframe() { // we have to remove the dom node altog as at time chrome fails to clear workers if we just change // src. so we delete the node itself to eb thorough. @@ -956,7 +973,7 @@ define(function (require, exports, module) { const mdFileURL = encodeURI(previewDetails.URL); const baseURL = mdFileURL.substring(0, mdFileURL.lastIndexOf("/") + 1); - currentPreviewFile = previewDetails.fullPath; + _setPreviewedFile(previewDetails.fullPath); if (!urlPinned) { currentLivePreviewURL = mdFileURL; } @@ -1075,7 +1092,7 @@ define(function (require, exports, module) { // preview breaks sporadically. to alleviate this, we create a new iframe every time. if(!urlPinned) { currentLivePreviewURL = newSrc; - currentPreviewFile = previewDetails.fullPath; + _setPreviewedFile(previewDetails.fullPath); } if(isReload && previewDetails.isHTMLFile){ LiveDevelopment.openLivePreview(); @@ -1741,10 +1758,24 @@ define(function (require, exports, module) { _loadPreview(true); } + /** + * Full path of the file currently loaded in the preview, which is not always the + * file in the editor: the url can be pinned, and a file with no preview of its own + * leaves the last previewed one up. + * @return {string} empty string when nothing has been previewed yet + */ + function getPreviewedFilePath() { + return currentPreviewFile; + } + + EventDispatcher.makeEventDispatcher(exports); + // private API to be used inside phoenix codebase only exports.LIVE_PREVIEW_PANEL_ID = LIVE_PREVIEW_PANEL_ID; + exports.EVENT_PREVIEWED_FILE_CHANGE = EVENT_PREVIEWED_FILE_CHANGE; exports.showInterstitial = showInterstitial; exports.hideInterstitial = hideInterstitial; + exports.getPreviewedFilePath = getPreviewedFilePath; }); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 7b2b9155b7..71b48c1898 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -810,10 +810,12 @@ define({ "LIVE_PREVIEW_SHOW_STYLES_BAR": "Show Styles Bar", "LIVE_PREVIEW_MODE_PREFERENCE": "'{0}' shows only the webpage, '{1}' connects the webpage to your code - click on elements to jump to their code and vice versa, '{2}' provides highlighting along with advanced element manipulation", "LIVE_PREVIEW_CONFIGURE_MODES": "Configure Live Preview Modes", - "LIVE_PREVIEW_LAYERS_PANEL": "Toggle Layers Panel", - "LIVE_PREVIEW_LAYERS_PANEL_TITLE": "Layers Panel", - "LIVE_PREVIEW_LAYERS_DOCK_LEFT": "Dock to Left", - "LIVE_PREVIEW_LAYERS_DOCK_RIGHT": "Dock to Right", + "LIVE_PREVIEW_LAYERS_TAB": "Layers", + "LIVE_PREVIEW_LAYERS_NEEDS_PREVIEW": "The Layers panel needs a live preview.", + "LIVE_PREVIEW_LAYERS_OPEN_PREVIEW": "Open Live Preview", + "LIVE_PREVIEW_LAYERS_NEEDS_HTML": "Open an HTML file to see its layers.", + "LIVE_PREVIEW_LAYERS_NEEDS_DESIGN_MODE": "The Layers panel needs design mode.", + "LIVE_PREVIEW_LAYERS_ENABLE_DESIGN_MODE": "Turn on Design Mode", "LIVE_PREVIEW_LAYERS_ELEMENTS": "Elements", "LIVE_PREVIEW_LAYERS_DETAILS": "Details", "LIVE_PREVIEW_LAYERS_SEARCH": "Search elements", diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less new file mode 100644 index 0000000000..8a8be940c3 --- /dev/null +++ b/src/styles/Extn-LayersPanel.less @@ -0,0 +1,671 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/* Layers sidebar tab — the live preview element tree and element details. */ + +@layers-accent: #6b9eff; +@layers-accent-soft: rgba(107, 158, 255, 0.16); +@layers-accent-strong: rgba(107, 158, 255, 0.26); +@layers-hover-bg: rgba(255, 255, 255, 0.06); +@layers-border: rgba(255, 255, 255, 0.09); +@layers-input-bg: rgba(255, 255, 255, 0.04); +@layers-input-bg-focus: rgba(255, 255, 255, 0.06); +@layers-row-height: 22px; +@layers-indent: 12px; +@layers-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; +@layers-prop-name: #9cdcfe; +@layers-prop-value: #ce9178; + +.layers-tab-container { + display: flex; + flex-direction: column; + -webkit-box-flex: 1; + flex: 1; + min-height: 0; + overflow: hidden; + position: relative; + background-color: @bc-ai-sidebar-bg; + color: @project-panel-text-1; + font-size: @sidebar-small-font-size; + + .layers-hidden { + display: none !important; + } + + .layers-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + -webkit-box-flex: 1; + flex: 1; + min-height: 0; + padding: 2rem 1.5rem; + text-align: center; + } + + .layers-message-icon { + margin-bottom: 0.9rem; + font-size: 2.1rem; + color: @project-panel-text-2; + opacity: 0.55; + } + + .layers-message-title { + margin-bottom: 0.4rem; + font-size: @sidebar-content-font-size; + font-weight: 600; + color: @project-panel-text-1; + } + + .layers-message-text { + max-width: 260px; + color: @project-panel-text-2; + line-height: 1.45; + } + + .layers-message-btn { + margin-top: 1.1rem; + padding: 6px 14px; + border: 1px solid @layers-accent; + border-radius: 6px; + background: @layers-accent-soft; + color: @project-panel-text-1; + font-family: inherit; + font-size: @sidebar-small-font-size; + font-weight: 500; + cursor: pointer; + outline: none; + + &:hover { + background: @layers-accent-strong; + } + } + + .layers-panel { + display: flex; + flex-direction: column; + -webkit-box-flex: 1; + flex: 1; + min-height: 0; + overflow: hidden; + } + + .layers-section { + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; + } + + .layers-section-elements { + flex: 0 0 var(--layers-elements-size, 60%); + } + + .layers-section-details { + flex: 1 1 auto; + } + + .layers-section.layers-collapsed { + flex: 0 0 auto; + } + + .layers-panel.layers-details-collapsed .layers-section-elements:not(.layers-collapsed) { + flex: 1 1 auto; + } + + .layers-section-header { + display: flex; + align-items: center; + gap: 2px; + height: 26px; + padding: 0 6px 0 2px; + flex-shrink: 0; + box-sizing: border-box; + cursor: pointer; + user-select: none; + font-size: @sidebar-xs-font-size; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: @project-panel-text-1; + + &:hover { + background: @layers-hover-bg; + } + } + + .layers-chevron { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + flex: none; + color: @project-panel-text-2; + + svg { + width: 18px; + height: 18px; + transition: transform 120ms ease; + } + } + + .layers-section.layers-collapsed .layers-chevron svg { + transform: rotate(-90deg); + } + + .layers-section-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .layers-section-actions { + display: flex; + align-items: center; + gap: 2px; + margin-left: auto; + flex-shrink: 0; + opacity: 0; + } + + .layers-section-header:hover .layers-section-actions, + .layers-section-actions:focus-within, + .layers-menu-open .layers-section-actions { + opacity: 1; + } + + .layers-hdr-btn { + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + margin: 0; + border: none; + border-radius: 4px; + background: none; + color: @project-panel-text-2; + cursor: pointer; + outline: none; + + svg { + width: 15px; + height: 15px; + display: block; + flex: none; + } + + &:hover { + background: @layers-hover-bg; + color: @project-panel-text-1; + } + + &.layers-active { + background: @layers-accent-soft; + color: @layers-accent; + + &:hover { + background: @layers-accent-strong; + } + } + } + + + .layers-section-content { + flex: 1; + min-height: 0; + overflow: auto; + } + + .layers-section.layers-collapsed .layers-section-content, + .layers-section.layers-collapsed .layers-search, + .layers-section.layers-collapsed .layers-tabs { + display: none; + } + + .layers-divider { + position: relative; + flex: 0 0 5px; + height: 5px; + cursor: row-resize; + + &::after { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 2px; + height: 1px; + background: @layers-border; + } + + &:hover::after, + &.layers-dragging::after { + height: 2px; + background: @layers-accent; + } + } + + .layers-panel.layers-no-resize .layers-divider { + cursor: default; + + &:hover::after { + height: 1px; + background: @layers-border; + } + } + + + .layers-search { + display: flex; + align-items: center; + flex-shrink: 0; + padding: 2px 8px 6px; + } + + .layers-search-box { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + height: 26px; + padding: 0 6px 0 8px; + box-sizing: border-box; + border: 1px solid @layers-border; + border-radius: 6px; + background: @layers-input-bg; + + &:focus-within { + border-color: @layers-accent; + background: @layers-input-bg-focus; + } + + > svg { + width: 13px; + height: 13px; + flex: none; + color: @project-panel-text-2; + } + + input[type="text"] { + flex: 1; + min-width: 0; + height: 100%; + padding: 0; + margin: 0; + border: none; + outline: none; + box-shadow: none; + background: transparent; + color: @project-panel-text-1; + font-family: inherit; + font-size: @sidebar-small-font-size; + + &::placeholder { + color: @project-panel-text-2; + opacity: 0.75; + } + } + } + + .layers-search-clear { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + padding: 0; + margin: 0; + border: none; + border-radius: 4px; + background: transparent; + color: @project-panel-text-2; + cursor: pointer; + outline: none; + flex: none; + + svg { + width: 11px; + height: 11px; + display: block; + } + + &:hover { + background: @layers-hover-bg; + color: @project-panel-text-1; + } + + &.layers-hidden { + display: none; + } + } + + .layers-tree { + padding-bottom: 6px; + min-width: min-content; + } + + .layers-node { + display: flex; + align-items: center; + height: @layers-row-height; + padding-right: 6px; + box-sizing: border-box; + white-space: nowrap; + cursor: default; + user-select: none; + + &:hover { + background: @layers-hover-bg; + } + + &.layers-selected { + background: @bc-sidebar-selection; + } + } + + .layers-twisty { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + flex: none; + color: @project-panel-text-2; + + svg { + width: 14px; + height: 14px; + } + } + + .layers-node.layers-collapsed-node .layers-twisty svg { + transform: rotate(-90deg); + } + + .layers-node-name { + margin-left: 4px; + color: @project-panel-text-2; + overflow: hidden; + text-overflow: ellipsis; + } + + .layers-node-icon { + display: flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + flex: none; + margin-left: 5px; + color: @project-panel-text-2; + + svg { + width: 14px; + height: 14px; + display: block; + } + } + + .layers-node-tag { + overflow: hidden; + text-overflow: ellipsis; + color: @project-panel-text-1; + } + + .layers-node-text { + color: @project-panel-text-2; + font-style: italic; + overflow: hidden; + text-overflow: ellipsis; + } + + .layers-empty { + padding: 10px 12px; + color: @project-panel-text-2; + } + + .layers-tabs { + display: flex; + align-items: stretch; + flex-shrink: 0; + padding: 0 8px; + border-bottom: 1px solid @layers-border; + } + + .layers-tab { + padding: 4px 8px 5px; + margin: 0; + border: none; + border-bottom: 2px solid transparent; + background: none; + color: @project-panel-text-2; + font-family: inherit; + font-size: @sidebar-xs-font-size; + font-weight: 600; + cursor: pointer; + outline: none; + + &:hover { + color: @project-panel-text-1; + } + + &.layers-active { + color: @project-panel-text-1; + border-bottom-color: @layers-accent; + } + } + + .layers-pane { + display: none; + padding: 6px 8px 10px; + + &.layers-active { + display: block; + } + } + + .layers-label { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: @project-panel-text-2; + user-select: none; + } + + .layers-prop-row { + display: flex; + align-items: baseline; + gap: 8px; + min-height: 20px; + + .layers-label { + flex: 0 0 44px; + } + } + + .layers-prop-value { + flex: 1; + min-width: 0; + font-family: @layers-mono; + font-size: @sidebar-xs-font-size; + color: @project-panel-text-1; + word-break: break-all; + user-select: text; + } + + .layers-group { + margin-top: 8px; + padding-top: 6px; + border-top: 1px solid @layers-border; + + > .layers-label { + display: block; + margin-bottom: 2px; + } + } + + .layers-decl { + display: flex; + align-items: baseline; + gap: 4px; + min-height: 20px; + padding-left: 8px; + font-family: @layers-mono; + font-size: @sidebar-xs-font-size; + user-select: text; + + &:hover { + background: @layers-hover-bg; + } + } + + .layers-decl-name { + flex: none; + color: @layers-prop-name; + } + + .layers-decl-sep { + flex: none; + color: @project-panel-text-2; + } + + .layers-decl-value { + flex: 1; + min-width: 0; + color: @layers-prop-value; + word-break: break-all; + } + + .layers-decl.layers-overridden { + .layers-decl-name, + .layers-decl-value { + text-decoration: line-through; + color: @project-panel-text-2; + } + } + + .layers-icon-btn { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + padding: 0; + margin: 0; + border: none; + border-radius: 4px; + background: none; + color: @project-panel-text-2; + cursor: pointer; + outline: none; + flex: none; + align-self: center; + + svg { + width: 12px; + height: 12px; + display: block; + } + + &:hover { + background: @layers-hover-bg; + color: @project-panel-text-1; + } + } + + .layers-decl .layers-icon-btn { + opacity: 0; + } + + .layers-decl:hover .layers-icon-btn, + .layers-decl .layers-icon-btn:focus { + opacity: 1; + } + + .layers-pane-footer { + display: flex; + justify-content: flex-end; + margin-top: 8px; + } + + .layers-rule { + margin-bottom: 6px; + padding-bottom: 6px; + border-bottom: 1px solid @layers-border; + + &:last-child { + margin-bottom: 0; + border-bottom: none; + } + + &.layers-inactive { + opacity: 0.6; + } + } + + .layers-rule-header { + display: flex; + align-items: center; + gap: 6px; + min-height: 20px; + } + + .layers-rule-selector { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: @layers-mono; + font-size: @sidebar-xs-font-size; + color: @project-panel-text-1; + } + + .layers-rule-source { + flex: none; + max-width: 45%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 10px; + color: @project-panel-text-2; + cursor: pointer; + + &:hover { + color: @layers-accent; + text-decoration: underline; + } + } +} + +body.layers-resizing { + cursor: row-resize; + user-select: none; +} + +.layers-menu-popup .layers-menu-check { + display: inline-block; + width: 12px; +} diff --git a/src/styles/brackets.less b/src/styles/brackets.less index c85c9bbc36..003730747e 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -50,6 +50,7 @@ @import "Extn-SidebarTabs.less"; @import "Extn-BottomPanelTabs.less"; @import "Extn-AIChatPanel.less"; +@import "Extn-LayersPanel.less"; @import "CentralControlBar.less"; @import "Extn-PhoenixTour.less"; @import "Extn-Terminal.less"; From 2ed05983b0916776d1b0eec4a9d0654c0ca2d2c0 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 29 Aug 2026 23:34:28 +0530 Subject: [PATCH 05/20] feat: simplify layers element rows --- src/styles/Extn-LayersPanel.less | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index 8a8be940c3..3de4f6de32 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -403,13 +403,6 @@ transform: rotate(-90deg); } - .layers-node-name { - margin-left: 4px; - color: @project-panel-text-2; - overflow: hidden; - text-overflow: ellipsis; - } - .layers-node-icon { display: flex; align-items: center; @@ -417,7 +410,7 @@ width: 14px; height: 14px; flex: none; - margin-left: 5px; + margin-right: 5px; color: @project-panel-text-2; svg { From 12681f3f86f5dab7c42ed1f56dd55dd922ed5356 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 29 Aug 2026 23:44:10 +0530 Subject: [PATCH 06/20] feat: row actions and drag reorder in layers panel --- src/nls/root/strings.js | 3 + src/styles/Extn-LayersPanel.less | 113 +++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 71b48c1898..eabb702cbd 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -828,6 +828,9 @@ define({ "LIVE_PREVIEW_LAYERS_LEVEL_ALL": "All Levels", "LIVE_PREVIEW_LAYERS_SHOW_TEXT_NODES": "Show Text Nodes", "LIVE_PREVIEW_LAYERS_HIDE_TEXT_NODES": "Hide Text Nodes", + "LIVE_PREVIEW_LAYERS_MOVE_UP": "Move Up", + "LIVE_PREVIEW_LAYERS_MOVE_DOWN": "Move Down", + "LIVE_PREVIEW_LAYERS_REVEAL_IN_CODE": "Reveal in Code", "LIVE_PREVIEW_LAYERS_TAB_PROPERTIES": "Properties", "LIVE_PREVIEW_LAYERS_TAB_STYLES": "Styles", "LIVE_PREVIEW_LAYERS_NO_SELECTION": "Select an element to see its details", diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index 3de4f6de32..3a7bd42d88 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -232,6 +232,7 @@ .layers-section-content { + position: relative; flex: 1; min-height: 0; overflow: auto; @@ -421,11 +422,65 @@ } .layers-node-tag { + min-width: 0; overflow: hidden; text-overflow: ellipsis; color: @project-panel-text-1; } + .layers-node-actions { + display: none; + align-items: center; + gap: 1px; + margin-left: auto; + padding-left: 6px; + flex: none; + } + + .layers-node:hover .layers-node-actions, + .layers-node.layers-actions-open .layers-node-actions, + .layers-node-actions:focus-within { + display: flex; + } + + .layers-node-action { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + padding: 0; + margin: 0; + border: none; + border-radius: 3px; + background: none; + color: @project-panel-text-2; + cursor: pointer; + outline: none; + flex: none; + + svg { + width: 12px; + height: 12px; + display: block; + } + + &:hover { + background: @layers-hover-bg; + color: @project-panel-text-1; + } + + &.layers-disabled { + opacity: 0.3; + cursor: default; + pointer-events: none; + } + } + + .layers-action-moveDown svg { + transform: rotate(180deg); + } + .layers-node-text { color: @project-panel-text-2; font-style: italic; @@ -433,6 +488,32 @@ text-overflow: ellipsis; } + .layers-drop-line { + position: absolute; + right: 0; + height: 2px; + margin-top: -1px; + background: @layers-accent; + pointer-events: none; + z-index: 2; + + &::before { + content: ""; + position: absolute; + left: -3px; + top: -2px; + width: 6px; + height: 6px; + border-radius: 50%; + background: @layers-accent; + } + } + + .layers-node.layers-drop-into { + box-shadow: inset 0 0 0 1px @layers-accent; + background: @layers-accent-soft; + } + .layers-empty { padding: 10px 12px; color: @project-panel-text-2; @@ -662,3 +743,35 @@ body.layers-resizing { display: inline-block; width: 12px; } + +body.layers-dnd { + cursor: grabbing; + user-select: none; +} + +body.layers-dnd-invalid { + cursor: no-drop; +} + +.layers-drag-ghost { + position: fixed; + z-index: 1000; + display: flex; + align-items: center; + gap: 5px; + padding: 2px 8px 2px 6px; + border: 1px solid rgba(107, 158, 255, 0.5); + border-radius: 4px; + background: #2f2f2f; + color: #e0e0e0; + font-size: 12px; + white-space: nowrap; + pointer-events: none; + opacity: 0.92; + + svg { + width: 14px; + height: 14px; + display: block; + } +} From 7ff6057f3d361d8824ba8fe074daadcc63126786 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 29 Aug 2026 23:48:08 +0530 Subject: [PATCH 07/20] feat: replace layers details tabs with collapsible sections --- src/nls/root/strings.js | 5 +- src/styles/Extn-LayersPanel.less | 85 +++++++++++++++----------------- 2 files changed, 41 insertions(+), 49 deletions(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index eabb702cbd..8017f53b36 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -817,7 +817,6 @@ define({ "LIVE_PREVIEW_LAYERS_NEEDS_DESIGN_MODE": "The Layers panel needs design mode.", "LIVE_PREVIEW_LAYERS_ENABLE_DESIGN_MODE": "Turn on Design Mode", "LIVE_PREVIEW_LAYERS_ELEMENTS": "Elements", - "LIVE_PREVIEW_LAYERS_DETAILS": "Details", "LIVE_PREVIEW_LAYERS_SEARCH": "Search elements", "LIVE_PREVIEW_LAYERS_NO_ELEMENTS": "No elements to show", "LIVE_PREVIEW_LAYERS_NO_MATCHES": "No matching elements", @@ -831,8 +830,8 @@ define({ "LIVE_PREVIEW_LAYERS_MOVE_UP": "Move Up", "LIVE_PREVIEW_LAYERS_MOVE_DOWN": "Move Down", "LIVE_PREVIEW_LAYERS_REVEAL_IN_CODE": "Reveal in Code", - "LIVE_PREVIEW_LAYERS_TAB_PROPERTIES": "Properties", - "LIVE_PREVIEW_LAYERS_TAB_STYLES": "Styles", + "LIVE_PREVIEW_LAYERS_PROPERTIES": "Properties", + "LIVE_PREVIEW_LAYERS_STYLES": "Styles", "LIVE_PREVIEW_LAYERS_NO_SELECTION": "Select an element to see its details", "LIVE_PREVIEW_LAYERS_NOT_IN_SOURCE": "This element is not in the source file", "LIVE_PREVIEW_LAYERS_NO_STYLES": "No styles applied", diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index 3a7bd42d88..5849de2909 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -119,16 +119,50 @@ flex: 0 0 var(--layers-elements-size, 60%); } - .layers-section-details { + .layers-details { + display: flex; + flex-direction: column; flex: 1 1 auto; + min-height: 0; + overflow: auto; } .layers-section.layers-collapsed { flex: 0 0 auto; } - .layers-panel.layers-details-collapsed .layers-section-elements:not(.layers-collapsed) { - flex: 1 1 auto; + .layers-panel.layers-details-collapsed { + .layers-details { + flex: 0 0 auto; + overflow: hidden; + } + + .layers-section-elements:not(.layers-collapsed) { + flex: 1 1 auto; + } + } + + .layers-detail-groups { + display: flex; + flex-direction: column; + } + + .layers-group { + display: flex; + flex-direction: column; + flex: none; + } + + .layers-group-body { + padding: 2px 8px 10px; + } + + .layers-group.layers-collapsed .layers-group-body { + display: none; + } + + .layers-details-empty { + flex: none; } .layers-section-header { @@ -168,7 +202,7 @@ } } - .layers-section.layers-collapsed .layers-chevron svg { + .layers-collapsed > .layers-section-header .layers-chevron svg { transform: rotate(-90deg); } @@ -239,8 +273,7 @@ } .layers-section.layers-collapsed .layers-section-content, - .layers-section.layers-collapsed .layers-search, - .layers-section.layers-collapsed .layers-tabs { + .layers-section.layers-collapsed .layers-search { display: none; } @@ -519,46 +552,6 @@ color: @project-panel-text-2; } - .layers-tabs { - display: flex; - align-items: stretch; - flex-shrink: 0; - padding: 0 8px; - border-bottom: 1px solid @layers-border; - } - - .layers-tab { - padding: 4px 8px 5px; - margin: 0; - border: none; - border-bottom: 2px solid transparent; - background: none; - color: @project-panel-text-2; - font-family: inherit; - font-size: @sidebar-xs-font-size; - font-weight: 600; - cursor: pointer; - outline: none; - - &:hover { - color: @project-panel-text-1; - } - - &.layers-active { - color: @project-panel-text-1; - border-bottom-color: @layers-accent; - } - } - - .layers-pane { - display: none; - padding: 6px 8px 10px; - - &.layers-active { - display: block; - } - } - .layers-label { font-size: 10px; font-weight: 700; From 75d09c43a35f0de7548a10c16ced581be40df4fc Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 29 Aug 2026 23:55:59 +0530 Subject: [PATCH 08/20] feat: inline property editing in layers panel --- src/nls/root/strings.js | 2 + src/styles/Extn-LayersPanel.less | 111 ++++++++++++++++++++++++++----- 2 files changed, 95 insertions(+), 18 deletions(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 8017f53b36..c868a0b3e4 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -830,6 +830,8 @@ define({ "LIVE_PREVIEW_LAYERS_MOVE_UP": "Move Up", "LIVE_PREVIEW_LAYERS_MOVE_DOWN": "Move Down", "LIVE_PREVIEW_LAYERS_REVEAL_IN_CODE": "Reveal in Code", + "LIVE_PREVIEW_LAYERS_ADD_ATTRIBUTE": "Add Attribute", + "LIVE_PREVIEW_LAYERS_REMOVE_ATTRIBUTE": "Remove Attribute", "LIVE_PREVIEW_LAYERS_PROPERTIES": "Properties", "LIVE_PREVIEW_LAYERS_STYLES": "Styles", "LIVE_PREVIEW_LAYERS_NO_SELECTION": "Select an element to see its details", diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index 5849de2909..17fc27ac5d 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -563,34 +563,115 @@ .layers-prop-row { display: flex; - align-items: baseline; - gap: 8px; - min-height: 20px; + align-items: center; + gap: 6px; + min-height: 24px; .layers-label { flex: 0 0 44px; } } - .layers-prop-value { + .layers-prop-input[type="text"] { flex: 1; min-width: 0; + height: 22px; + padding: 0 5px; + margin: 0; + border: 1px solid transparent; + border-radius: 4px; + outline: none; + box-shadow: none; + background: transparent; + color: @project-panel-text-1; font-family: @layers-mono; font-size: @sidebar-xs-font-size; - color: @project-panel-text-1; - word-break: break-all; - user-select: text; + text-overflow: ellipsis; + + &::placeholder { + color: @project-panel-text-2; + opacity: 0.6; + } + + &:hover:not([readonly]) { + border-color: @layers-border; + background: @layers-input-bg; + } + + &:focus { + border-color: @layers-accent; + background: @layers-input-bg-focus; + text-overflow: clip; + } + + &[readonly] { + color: @project-panel-text-2; + cursor: default; + } } - .layers-group { + .layers-attrs { margin-top: 8px; - padding-top: 6px; + padding-top: 4px; border-top: 1px solid @layers-border; + } - > .layers-label { - display: block; - margin-bottom: 2px; + .layers-attrs-header { + display: flex; + align-items: center; + min-height: 20px; + + .layers-label { + flex: 1; } + + .layers-icon-btn { + opacity: 0; + } + } + + .layers-attrs:hover .layers-attrs-header .layers-icon-btn, + .layers-attrs-header .layers-icon-btn:focus { + opacity: 1; + } + + .layers-attrs-none { + padding: 2px 0 2px 5px; + color: @project-panel-text-2; + opacity: 0.7; + } + + .layers-attr-row { + display: flex; + align-items: center; + gap: 4px; + min-height: 24px; + + .layers-icon-btn { + opacity: 0; + } + + &:hover .layers-icon-btn, + .layers-icon-btn:focus { + opacity: 1; + } + } + + .layers-attr-name { + flex: 0 0 44px; + min-width: 0; + padding-left: 5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: @layers-mono; + font-size: @sidebar-xs-font-size; + color: @layers-prop-name; + } + + .layers-attr-row [data-role="name"] { + flex: 0 0 44px; + color: @layers-prop-name; } .layers-decl { @@ -671,12 +752,6 @@ opacity: 1; } - .layers-pane-footer { - display: flex; - justify-content: flex-end; - margin-top: 8px; - } - .layers-rule { margin-bottom: 6px; padding-bottom: 6px; From 2864a8de62c2a0addd6d85d832bd318175aabb9b Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 30 Aug 2026 00:05:17 +0530 Subject: [PATCH 09/20] fix: give long attribute names room in layers properties --- src/styles/Extn-LayersPanel.less | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index 17fc27ac5d..172f46db23 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -658,8 +658,9 @@ } .layers-attr-name { - flex: 0 0 44px; - min-width: 0; + flex: 0 1 auto; + min-width: 44px; + max-width: 45%; padding-left: 5px; overflow: hidden; text-overflow: ellipsis; From 0f5fdebf067766fdb57999356ef445913cff2fdf Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 30 Aug 2026 00:45:33 +0530 Subject: [PATCH 10/20] fix: bound the layers elements section so it scrolls and resizes --- src/styles/Extn-LayersPanel.less | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index 172f46db23..7d34ce0839 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -116,13 +116,17 @@ } .layers-section-elements { - flex: 0 0 var(--layers-elements-size, 60%); + flex-grow: var(--layers-elements-size, 60); + flex-shrink: 1; + flex-basis: 0; } .layers-details { display: flex; flex-direction: column; - flex: 1 1 auto; + flex-grow: calc(100 - var(--layers-elements-size, 60)); + flex-shrink: 1; + flex-basis: 0; min-height: 0; overflow: auto; } From 81912dfc33f654b27a6597bec53b5f3cddf870c4 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 30 Aug 2026 00:50:58 +0530 Subject: [PATCH 11/20] feat: keyboard navigation in the layers elements tree --- src/styles/Extn-LayersPanel.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index 7d34ce0839..bded5a73c7 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -401,6 +401,11 @@ .layers-tree { padding-bottom: 6px; min-width: min-content; + outline: none; + } + + .layers-tree:focus .layers-node.layers-selected { + box-shadow: inset 0 0 0 1px @layers-accent; } .layers-node { From ab12a3c3e34c6aa527d428ea9fca7fa6e0caea28 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 30 Aug 2026 00:51:11 +0530 Subject: [PATCH 12/20] fix: keep the layers row actions in view when the tree scrolls sideways --- src/styles/Extn-LayersPanel.less | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index bded5a73c7..335ad205fb 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -32,6 +32,9 @@ @layers-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; @layers-prop-name: #9cdcfe; @layers-prop-value: #ce9178; +@layers-row-bg: @bc-ai-sidebar-bg; +@layers-row-hover-bg: mix(#ffffff, @bc-ai-sidebar-bg, 6%); +@layers-row-select-bg: mix(fade(@bc-sidebar-selection, 100%), @bc-ai-sidebar-bg, 18%); .layers-tab-container { display: flex; @@ -474,9 +477,12 @@ display: none; align-items: center; gap: 1px; + position: sticky; + right: 0; margin-left: auto; padding-left: 6px; flex: none; + background: @layers-row-bg; } .layers-node:hover .layers-node-actions, @@ -485,6 +491,14 @@ display: flex; } + .layers-node:hover .layers-node-actions { + background: @layers-row-hover-bg; + } + + .layers-node.layers-selected .layers-node-actions { + background: @layers-row-select-bg; + } + .layers-node-action { display: flex; align-items: center; From 11c7603fd6ff244e909de79d0c8435c9cfcd1a08 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 30 Aug 2026 01:14:14 +0530 Subject: [PATCH 13/20] fix: rename the layers mode gate strings to edit mode --- src/nls/root/strings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index c868a0b3e4..56dd5bd233 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -814,8 +814,8 @@ define({ "LIVE_PREVIEW_LAYERS_NEEDS_PREVIEW": "The Layers panel needs a live preview.", "LIVE_PREVIEW_LAYERS_OPEN_PREVIEW": "Open Live Preview", "LIVE_PREVIEW_LAYERS_NEEDS_HTML": "Open an HTML file to see its layers.", - "LIVE_PREVIEW_LAYERS_NEEDS_DESIGN_MODE": "The Layers panel needs design mode.", - "LIVE_PREVIEW_LAYERS_ENABLE_DESIGN_MODE": "Turn on Design Mode", + "LIVE_PREVIEW_LAYERS_NEEDS_EDIT_MODE": "The Layers panel needs edit mode.", + "LIVE_PREVIEW_LAYERS_ENABLE_EDIT_MODE": "Turn on Edit Mode", "LIVE_PREVIEW_LAYERS_ELEMENTS": "Elements", "LIVE_PREVIEW_LAYERS_SEARCH": "Search elements", "LIVE_PREVIEW_LAYERS_NO_ELEMENTS": "No elements to show", From 0e18e7f12be300fdf4317797560090a6e2bdab48 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 30 Aug 2026 21:28:04 +0530 Subject: [PATCH 14/20] feat: split layers styles into its own resizable section --- src/styles/Extn-LayersPanel.less | 56 ++++---------------------------- 1 file changed, 6 insertions(+), 50 deletions(-) diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index 335ad205fb..691cf7204b 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -114,64 +114,22 @@ .layers-section { display: flex; flex-direction: column; - min-height: 0; - overflow: hidden; - } - - .layers-section-elements { - flex-grow: var(--layers-elements-size, 60); - flex-shrink: 1; - flex-basis: 0; - } - - .layers-details { - display: flex; - flex-direction: column; - flex-grow: calc(100 - var(--layers-elements-size, 60)); + flex-grow: var(--layers-grow, 1); flex-shrink: 1; flex-basis: 0; min-height: 0; - overflow: auto; + overflow: hidden; } .layers-section.layers-collapsed { flex: 0 0 auto; } - .layers-panel.layers-details-collapsed { - .layers-details { - flex: 0 0 auto; - overflow: hidden; - } - - .layers-section-elements:not(.layers-collapsed) { - flex: 1 1 auto; - } - } - - .layers-detail-groups { - display: flex; - flex-direction: column; - } - - .layers-group { - display: flex; - flex-direction: column; - flex: none; - } - - .layers-group-body { + .layers-section-properties .layers-section-content, + .layers-section-styles .layers-section-content { padding: 2px 8px 10px; } - .layers-group.layers-collapsed .layers-group-body { - display: none; - } - - .layers-details-empty { - flex: none; - } - .layers-section-header { display: flex; align-items: center; @@ -307,7 +265,7 @@ } } - .layers-panel.layers-no-resize .layers-divider { + .layers-divider.layers-no-resize { cursor: default; &:hover::after { @@ -634,9 +592,7 @@ } .layers-attrs { - margin-top: 8px; - padding-top: 4px; - border-top: 1px solid @layers-border; + margin-top: 6px; } .layers-attrs-header { From fded05ca7416b11c08ba4f36b93f04f840ad7292 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 30 Aug 2026 21:34:45 +0530 Subject: [PATCH 15/20] fix: even out the layers selection highlight --- src/styles/Extn-LayersPanel.less | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index 691cf7204b..c635bd4a8a 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -34,7 +34,8 @@ @layers-prop-value: #ce9178; @layers-row-bg: @bc-ai-sidebar-bg; @layers-row-hover-bg: mix(#ffffff, @bc-ai-sidebar-bg, 6%); -@layers-row-select-bg: mix(fade(@bc-sidebar-selection, 100%), @bc-ai-sidebar-bg, 18%); +@layers-row-select-bg: mix(#ffffff, @bc-ai-sidebar-bg, 9%); +@layers-row-focus-bg: mix(fade(@bc-sidebar-selection, 100%), @bc-ai-sidebar-bg, 30%); .layers-tab-container { display: flex; @@ -366,7 +367,7 @@ } .layers-tree:focus .layers-node.layers-selected { - box-shadow: inset 0 0 0 1px @layers-accent; + background: @layers-row-focus-bg; } .layers-node { @@ -384,7 +385,7 @@ } &.layers-selected { - background: @bc-sidebar-selection; + background: @layers-row-select-bg; } } @@ -457,6 +458,10 @@ background: @layers-row-select-bg; } + .layers-tree:focus .layers-node.layers-selected .layers-node-actions { + background: @layers-row-focus-bg; + } + .layers-node-action { display: flex; align-items: center; From 41e4ce7734f36f748b86e65b9afcdda995026a51 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 30 Aug 2026 21:35:11 +0530 Subject: [PATCH 16/20] fix: keep focus where it was when live preview is asked to select --- .../protocol/LiveDevProtocol.js | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index f62b91229b..036eafb3a3 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -209,8 +209,24 @@ define(function (require, exports, module) { document.body.dispatchEvent(event); } + // A selection Phoenix asked the live preview to make - the layers panel picking + // an element - never took focus away from the editor side, so there is nothing + // to restore and pulling focus into the editor would take it off whatever asked + // for the selection. Time boxed so a selection that never reports back cannot + // leave the next real click in the preview without its focus. + const KEEP_FOCUS_WINDOW_MS = 1500; + let _keepFocusUntil = 0; + + function keepFocusOnNextSelect() { + _keepFocusUntil = Date.now() + KEEP_FOCUS_WINDOW_MS; + } + + function _shouldKeepFocus() { + return Date.now() < _keepFocusUntil; + } + function _focusEditorIfNeeded(editor, tagName, contentEditable) { - if (WorkspaceManager.isInDesignMode()) { + if (WorkspaceManager.isInDesignMode() || _shouldKeepFocus()) { return; } const focusShouldBeInLivePreview = ['INPUT', 'TEXTAREA'].includes(tagName) || contentEditable; @@ -271,7 +287,7 @@ define(function (require, exports, module) { activeEditorPath = activeEditor ? activeEditor.document.file.fullPath : null, activeFullEditorPath = activeFullEditor ? activeFullEditor.document.file.fullPath : null; if(!liveDocPath){ - if (activeEditor && !WorkspaceManager.isInDesignMode()) { + if (activeEditor && !WorkspaceManager.isInDesignMode() && !_shouldKeepFocus()) { activeEditor.focus(); // restore focus from live preview } return; @@ -384,6 +400,7 @@ define(function (require, exports, module) { } catch (e) { console.error("error in tag selection", e); } + _keepFocusUntil = 0; editMode && liveDoc && liveDoc.disableHighlightOnCursorActivity(false); liveDoc && liveDoc.updateHighlight(); } else { @@ -767,6 +784,7 @@ define(function (require, exports, module) { exports.setLivePreviewMessageHandler = setLivePreviewMessageHandler; exports.setCustomRemoteFunctionProvider = setCustomRemoteFunctionProvider; // lp communication functions + exports.keepFocusOnNextSelect = keepFocusOnNextSelect; exports.registerPhoenixFn = registerPhoenixFn; exports.triggerLPFn = triggerLPFn; exports.LIVE_DEV_REMOTE_SCRIPTS_FILE_NAME = LIVE_DEV_REMOTE_SCRIPTS_FILE_NAME; From e0fbe288de597a3b89add03dc9c21b11be864057 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 30 Aug 2026 21:35:25 +0530 Subject: [PATCH 17/20] feat: mark the editor cursor's element in the layers tree --- src/styles/Extn-LayersPanel.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index c635bd4a8a..3c2df5bb68 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -370,6 +370,11 @@ background: @layers-row-focus-bg; } + .layers-node.layers-cursor:not(.layers-selected) { + background: @layers-hover-bg; + box-shadow: inset 2px 0 0 fade(@layers-accent, 55%); + } + .layers-node { display: flex; align-items: center; From 88782df7115a1525d436425550beec0843701364 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 30 Aug 2026 21:40:59 +0530 Subject: [PATCH 18/20] feat: flash the rows a layers edit touched --- src/styles/Extn-LayersPanel.less | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index 3c2df5bb68..b3ae6c6c3e 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -375,6 +375,10 @@ box-shadow: inset 2px 0 0 fade(@layers-accent, 55%); } + .layers-node.layers-flash { + animation: layers-flash-row 800ms ease-out; + } + .layers-node { display: flex; align-items: center; @@ -833,3 +837,24 @@ body.layers-dnd-invalid { display: block; } } + +@keyframes layers-flash-row { + 0% { + background-color: fade(@layers-accent, 45%); + } + + 60% { + background-color: fade(@layers-accent, 32%); + } + + 100% { + background-color: transparent; + } +} + +@media (prefers-reduced-motion: reduce) { + .layers-tab-container .layers-node.layers-flash { + animation-duration: 1ms; + background-color: fade(@layers-accent, 32%); + } +} From 4a796854cd894531952bdc0b4e4731baa9cc6aa5 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 30 Aug 2026 23:20:57 +0530 Subject: [PATCH 19/20] fix: let the editor select a live preview element without clicking it --- .../BrowserScripts/RemoteFunctions.js | 74 +++++++++++++++---- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 29862d96b7..69e5737dc1 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -42,6 +42,8 @@ function RemoteFunctions(config = {}) { // we need this so that we can remove click styling from the previous element when a new element is clicked let previouslySelectedElement = null; let _selectedFromEditor = false; + // the selected element the `phcode-no-lp-edit` opt-out is lifted for, see _isEditOptedOut + let _editOptOutOverride = null; // Expose the currently selected element globally for external access window.__current_ph_lp_selected = null; @@ -121,6 +123,9 @@ function RemoteFunctions(config = {}) { * Elements opted out via `phcode-no-lp-edit` (cascades to descendants) or * `phcode-no-lp-edit-this` (this element only) are also non-inspectable so * every downstream tool inherits the opt-out automatically. + * + * @param {DOMElement} element + * @param {boolean} [onlyHighlight=false] - If true, bypasses the mode check */ function isElementInspectable(element, onlyHighlight = false) { if(config.mode !== 'edit' && !onlyHighlight) { @@ -133,13 +138,31 @@ function RemoteFunctions(config = {}) { // this attribute is used by phoenix internal elements !element.closest(`[${GLOBALS.PHCODE_INTERNAL_ATTR}]`) && !_isInsideHeadTag(element) && // shouldn't be inside the head tag like meta tags and all - !element.closest('.phcode-no-lp-edit') && - !(element.classList && element.classList.contains('phcode-no-lp-edit-this'))) { + !_isEditOptedOut(element)) { return true; } return false; } + /** + * `phcode-no-lp-edit` cascades to descendants, `phcode-no-lp-edit-this` covers + * the one element. + * + * The opt-out exists so that a pointer landing on the page is read as the page's + * own business rather than as an edit, and a click still is: handleElementClick + * tests for it before anything else. But an element the editor named outright - + * a row picked in the layers panel - is being edited on purpose and there is + * nothing ambiguous to protect, so while such an element holds the selection + * every tool treats it like any other. + */ + function _isEditOptedOut(element) { + if (element === _editOptOutOverride) { + return false; + } + return !!(element.closest('.phcode-no-lp-edit') || + (element.classList && element.classList.contains('phcode-no-lp-edit-this'))); + } + /** * This is a checker function for editable elements, it makes sure that the element satisfies all the required check * - When onlyHighlight is false → config.mode must be 'edit' @@ -185,6 +208,7 @@ function RemoteFunctions(config = {}) { isElementVisible: isElementVisible, screenOffset: screenOffset, selectElement: selectElement, + sendSelectionToEditor: sendSelectionToEditor, brieflyDisableHoverListeners: brieflyDisableHoverListeners, handleElementClick: handleElementClick, cleanupPreviousElementState: cleanupPreviousElementState, @@ -668,8 +692,11 @@ function RemoteFunctions(config = {}) { * @param {boolean} [fromEditor] - If true, this is an editor-cursor-driven selection; * only lightweight highlights (outline, margin/padding overlay) are shown, not interactive * UI like control box, spacing handles, or measurements. + * @param {boolean} [ignoreEditOptOut] - Edit this element even though it opted + * out of live preview editing. For selections asked for by name from the + * editor side; holds only while the element stays selected. */ - function selectElement(element, fromEditor) { + function selectElement(element, fromEditor, ignoreEditOptOut) { // When a cursor-based highlight re-selects the already-selected element, // just refresh the highlight overlay without dismissing existing UI panels // (control box, editor box, element-info). This prevents cursor activity @@ -685,6 +712,8 @@ function RemoteFunctions(config = {}) { } dismissUIAndCleanupState(); + // set after the dismissal, which clears the previous selection's exemption + _editOptOutOverride = ignoreEditOptOut ? element : null; // this should also be there when users are in highlight mode scrollElementToViewPort(element); @@ -804,24 +833,36 @@ function RemoteFunctions(config = {}) { selection.removeAllRanges(); } - // send cursor movement message to editor so cursor jumps to clicked element - if (element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR) && - config.syncSourceAndPreview !== false) { - MessageBroker.send({ - "tagId": element.getAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR), - "nodeID": element.id, - "nodeClassList": element.classList, - "nodeName": element.nodeName, - "allSelectors": window.getAllInheritedSelectorsInOrder(element), - "contentEditable": element.contentEditable === "true", - "clicked": true - }); - } + sendSelectionToEditor(element); brieflyDisableHoverListeners(); selectElement(element); } + /** + * Tells the editor which element is now selected, so the cursor jumps to it and + * the css reverse highlight follows. Split out of the click handler because a + * selection can also be asked for from the editor side, which must report itself + * the same way without a pointer gesture ever touching the page. + * + * @param {HTMLElement} element + */ + function sendSelectionToEditor(element) { + if (!element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR) || + config.syncSourceAndPreview === false) { + return; + } + MessageBroker.send({ + "tagId": element.getAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR), + "nodeID": element.id, + "nodeClassList": element.classList, + "nodeName": element.nodeName, + "allSelectors": window.getAllInheritedSelectorsInOrder(element), + "contentEditable": element.contentEditable === "true", + "clicked": true + }); + } + // clear CSS selector highlights function clearCssSelectorHighlight() { if (_cssSelectorHighlightTimer) { @@ -1500,6 +1541,7 @@ function RemoteFunctions(config = {}) { previouslySelectedElement = null; window.__current_ph_lp_selected = null; } + _editOptOutOverride = null; // Reset hover tracking so the same-element skip doesn't suppress // re-highlighting after a full state cleanup (e.g. Escape, dismiss). From 8fc1a3c70282552a7081419efeecff074c5f5e61 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 30 Aug 2026 23:21:21 +0530 Subject: [PATCH 20/20] fix: drop row hover while walking the layers tree by keyboard --- src/styles/Extn-LayersPanel.less | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index b3ae6c6c3e..03cb38d94c 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -375,6 +375,14 @@ box-shadow: inset 2px 0 0 fade(@layers-accent, 55%); } + .layers-tree.layers-kbd-nav .layers-node:hover:not(.layers-selected):not(.layers-cursor) { + background: transparent; + } + + .layers-tree.layers-kbd-nav .layers-node:hover .layers-node-actions { + display: none; + } + .layers-node.layers-flash { animation: layers-flash-row 800ms ease-out; }