diff --git a/docs/API-Reference/view/PanelView.md b/docs/API-Reference/view/PanelView.md index cd9055fc02..0d92aa3432 100644 --- a/docs/API-Reference/view/PanelView.md +++ b/docs/API-Reference/view/PanelView.md @@ -52,7 +52,8 @@ Determines if the panel is visible ### panel.registerCanBeShownHandler(canShowHandlerFn) ⇒ boolean -Registers a call back function that will be called just before panel is shown. The handler should return true if the panel can be shown, else return false and the panel will not be shown. +Registers a call back function that will be called just before panel is shown. The handler should return true +if the panel can be shown, else return false and the panel will not be shown. **Kind**: instance method of [Panel](#Panel) **Returns**: boolean - true if visible, false if not @@ -70,7 +71,8 @@ Returns true if th panel can be shown, else false. ### panel.registerOnCloseRequestedHandler(handler) -Registers an async handler that is called before the panel is closed via user interaction (e.g. clicking the tab close button). The handler should return `true` to allow the close, or `false` to prevent it. +Registers an async handler that is called before the panel is closed via user interaction (e.g. clicking the +tab close button). The handler should return `true` to allow the close, or `false` to prevent it. **Kind**: instance method of [Panel](#Panel) @@ -81,7 +83,9 @@ Registers an async handler that is called before the panel is closed via user in ### panel.requestClose() ⇒ Promise.<boolean> -Requests the panel to hide, invoking the registered onCloseRequested handler first (if any). If the handler returns false, the panel stays open. If it returns true or no handler is registered, `hide()` is called. +Requests the panel to hide, invoking the registered onCloseRequested handler first (if any). +If the handler returns false, the panel stays open. If it returns true or no handler is +registered, `hide()` is called. **Kind**: instance method of [Panel](#Panel) **Returns**: Promise.<boolean> - Resolves to true if the panel was hidden, false if prevented. @@ -100,7 +104,8 @@ Hides the panel ### panel.focus() ⇒ boolean -Attempts to focus the panel. Override this in panels that support focus (e.g. terminal). The default implementation returns false. +Attempts to focus the panel. Override this in panels that support focus +(e.g. terminal). The default implementation returns false. **Kind**: instance method of [Panel](#Panel) **Returns**: boolean - true if the panel accepted focus, false otherwise @@ -129,7 +134,8 @@ Updates the display title shown in the tab bar for this panel. ### panel.destroy() -Destroys the panel, removing it from the tab bar, internal maps, and the DOM. After calling this, the Panel instance should not be reused. +Destroys the panel, removing it from the tab bar, internal maps, and the DOM. +After calling this, the Panel instance should not be reused. **Kind**: instance method of [Panel](#Panel) @@ -231,13 +237,17 @@ type for bottom panel ## MAXIMIZE\_THRESHOLD : number -Pixel threshold for detecting near-maximize state during resize. If the editor holder height is within this many pixels of zero, the panel is treated as maximized. Keeps the maximize icon responsive during drag without being overly sensitive. +Pixel threshold for detecting near-maximize state during resize. +If the editor holder height is within this many pixels of zero, the +panel is treated as maximized. Keeps the maximize icon responsive +during drag without being overly sensitive. **Kind**: global constant ## MIN\_PANEL\_HEIGHT : number -Minimum panel height (matches Resizer minSize) used as a floor when computing a sensible restore height. +Minimum panel height (matches Resizer minSize) used as a floor +when computing a sensible restore height. **Kind**: global constant @@ -249,7 +259,8 @@ Preference key for persisting the maximize state across reloads. ## init($container, $tabBar, $tabsOverflow, $editorHolder, recomputeLayoutFn, defaultPanelId) -Initializes the PanelView module with references to the bottom panel container DOM elements. Called by WorkspaceManager during htmlReady. +Initializes the PanelView module with references to the bottom panel container DOM elements. +Called by WorkspaceManager during htmlReady. **Kind**: global function @@ -265,19 +276,26 @@ Initializes the PanelView module with references to the bottom panel container D ## exitMaximizeOnResize() -Exit maximize state without resizing (for external callers like drag-resize). Clears internal maximize state and resets the button icon. +Exit maximize state without resizing (for external callers like drag-resize). +Clears internal maximize state and resets the button icon. **Kind**: global function ## enterMaximizeOnResize() -Enter maximize state during a drag-resize that reaches the maximum height. No pre-maximize height is stored because the user arrived here via continuous dragging; a sensible default will be computed if they later click the Restore button. +Enter maximize state during a drag-resize that reaches the maximum +height. No pre-maximize height is stored because the user arrived +here via continuous dragging; a sensible default will be computed if +they later click the Restore button. **Kind**: global function ## restoreIfMaximized() -Restore the container's CSS height to the pre-maximize value and clear maximize state. Must be called BEFORE Resizer.hide() so the Resizer reads the correct height. If not maximized, this is a no-op. When the saved height is near-max or unknown, a sensible default is used. +Restore the container's CSS height to the pre-maximize value and clear maximize state. +Must be called BEFORE Resizer.hide() so the Resizer reads the correct height. +If not maximized, this is a no-op. +When the saved height is near-max or unknown, a sensible default is used. **Kind**: global function @@ -308,7 +326,8 @@ Returns the currently active (visible) bottom panel, or null if none. ## showNextPanel() ⇒ boolean -Cycle to the next open bottom panel tab. If the container is hidden or no panels are open, does nothing and returns false. +Cycle to the next open bottom panel tab. If the container is hidden +or no panels are open, does nothing and returns false. **Kind**: global function **Returns**: boolean - true if a panel switch occurred diff --git a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js index 67702ce857..9f97dc165d 100644 --- a/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js +++ b/src/LiveDevelopment/BrowserScripts/LiveDevProtocolRemote.js @@ -539,6 +539,13 @@ // as a live select. return; } + if (window._LD && !window._LD.isSyncEnabled()) { + // When sync is disabled, highlight the element directly in the browser + // without doing a round-trip through the editor (which would move the cursor) + var tagId = element.getAttribute('data-brackets-id'); + window._LD.highlightRule("[data-brackets-id='" + tagId + "']"); + return; + } MessageBroker.send({ "tagId": element.getAttribute('data-brackets-id'), "nodeID": element.id, diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index cf054caa3d..6f56eb2392 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -33,7 +33,7 @@ function RemoteFunctions(config = {}) { let _clickHighlight; let _cssSelectorHighlight; // temporary highlight for CSS selector matches in edit mode let _hoverLockTimer = null; - let _cssSelectorHighlightTimer = null; // timer for clearing temporary CSS selector highlights + let _cssSelectorHighlightTimer = null; // this will store the element that was clicked previously (before the new click) // we need this so that we can remove click styling from the previous element when a new element is clicked @@ -41,19 +41,12 @@ function RemoteFunctions(config = {}) { // Expose the currently selected element globally for external access window.__current_ph_lp_selected = null; - var req, timeout; - function animateHighlight(time) { - if(req) { - window.cancelAnimationFrame(req); - window.clearTimeout(timeout); - } - req = window.requestAnimationFrame(redrawHighlights); - - timeout = setTimeout(function () { - window.cancelAnimationFrame(req); - req = null; - }, time * 1000); - } + const COLORS = { + highlightPadding: "rgba(147, 196, 125, 0.55)", + highlightMargin: "rgba(246, 178, 107, 0.66)", + outlineEditable: "#4285F4", + outlineNonEditable: "#3C3F41" + }; // the following fucntions can be in the handler and live preview will call those functions when the below // events happen @@ -61,6 +54,7 @@ function RemoteFunctions(config = {}) { "dismiss", // when handler gets this event, it should dismiss all ui it renders in the live preview "createToolBox", "createInfoBox", + "createHoverBox", "createMoreOptionsDropdown", // render an icon or html when the selected element toolbox appears in edit mode. "renderToolBoxItem", @@ -181,7 +175,9 @@ function RemoteFunctions(config = {}) { handleElementClick: handleElementClick, cleanupPreviousElementState: cleanupPreviousElementState, disableHoverListeners: disableHoverListeners, - enableHoverListeners: enableHoverListeners + enableHoverListeners: enableHoverListeners, + redrawHighlights: redrawHighlights, + redrawEverything: redrawEverything }; /** @@ -265,267 +261,16 @@ function RemoteFunctions(config = {}) { return element.offsetTop + (element.offsetParent ? getDocumentOffsetTop(element.offsetParent) : 0); } - function Highlight(color, trigger) { - this.color = color; + function Highlight(trigger) { this.trigger = !!trigger; this.elements = []; this.selector = ""; + this._divs = []; } Highlight.prototype = { - _elementExists: function (element) { - var i; - for (i in this.elements) { - if (this.elements[i] === element) { - return true; - } - } - return false; - }, - _makeHighlightDiv: function (element, doAnimation) { - const remoteHighlight = { - animateStartValue: { - "background-color": "rgba(0, 162, 255, 0.5)", - "opacity": 0 - }, - animateEndValue: { - "background-color": "rgba(0, 162, 255, 0)", - "opacity": 0.6 - }, - paddingStyling: { - "background-color": "rgba(200, 249, 197, 0.7)" - }, - marginStyling: { - "background-color": "rgba(249, 204, 157, 0.7)" - }, - borderColor: "rgba(200, 249, 197, 0.85)", - showPaddingMargin: true - }; - var elementBounds = element.getBoundingClientRect(), - highlightDiv = window.document.createElement("div"), - elementStyling = window.getComputedStyle(element), - transitionDuration = parseFloat(elementStyling.getPropertyValue('transition-duration')), - animationDuration = parseFloat(elementStyling.getPropertyValue('animation-duration')); - - highlightDiv.trackingElement = element; // save which node are we highlighting - - if (doAnimation) { - if (transitionDuration) { - animateHighlight(transitionDuration); - } - - if (animationDuration) { - animateHighlight(animationDuration); - } - } - - // Don't highlight elements with 0 width & height - if (elementBounds.width === 0 && elementBounds.height === 0) { - return; - } - - var realElBorder = { - right: elementStyling.getPropertyValue('border-right-width'), - left: elementStyling.getPropertyValue('border-left-width'), - top: elementStyling.getPropertyValue('border-top-width'), - bottom: elementStyling.getPropertyValue('border-bottom-width') - }; - - var borderBox = elementStyling.boxSizing === 'border-box'; - - var innerWidth = parseFloat(elementStyling.width), - innerHeight = parseFloat(elementStyling.height), - outerHeight = innerHeight, - outerWidth = innerWidth; - - if (!borderBox) { - innerWidth += parseFloat(elementStyling.paddingLeft) + parseFloat(elementStyling.paddingRight); - innerHeight += parseFloat(elementStyling.paddingTop) + parseFloat(elementStyling.paddingBottom); - outerWidth = innerWidth + parseFloat(realElBorder.right) + - parseFloat(realElBorder.left), - outerHeight = innerHeight + parseFloat(realElBorder.bottom) + parseFloat(realElBorder.top); - } - - - var visualisations = { - horizontal: "left, right", - vertical: "top, bottom" - }; - - var drawPaddingRect = function (side) { - var elStyling = {}; - - if (visualisations.horizontal.indexOf(side) >= 0) { - elStyling["width"] = elementStyling.getPropertyValue("padding-" + side); - elStyling["height"] = innerHeight + "px"; - elStyling["top"] = 0; - - if (borderBox) { - elStyling["height"] = - innerHeight - parseFloat(realElBorder.top) - parseFloat(realElBorder.bottom) + "px"; - } - } else { - elStyling["height"] = elementStyling.getPropertyValue("padding-" + side); - elStyling["width"] = innerWidth + "px"; - elStyling["left"] = 0; - - if (borderBox) { - elStyling["width"] = - innerWidth - parseFloat(realElBorder.left) - parseFloat(realElBorder.right) + "px"; - } - } - - elStyling[side] = 0; - elStyling["position"] = "absolute"; - - return elStyling; - }; - - var drawMarginRect = function (side) { - var elStyling = {}; - - var margin = []; - margin["right"] = parseFloat(elementStyling.getPropertyValue("margin-right")); - margin["top"] = parseFloat(elementStyling.getPropertyValue("margin-top")); - margin["bottom"] = parseFloat(elementStyling.getPropertyValue("margin-bottom")); - margin["left"] = parseFloat(elementStyling.getPropertyValue("margin-left")); - - if (visualisations["horizontal"].indexOf(side) >= 0) { - elStyling["width"] = elementStyling.getPropertyValue("margin-" + side); - elStyling["height"] = outerHeight + margin["top"] + margin["bottom"] + "px"; - elStyling["top"] = "-" + (margin["top"] + parseFloat(realElBorder.top)) + "px"; - } else { - elStyling["height"] = elementStyling.getPropertyValue("margin-" + side); - elStyling["width"] = outerWidth + "px"; - elStyling["left"] = "-" + realElBorder.left; - } - - elStyling[side] = "-" + (margin[side] + parseFloat(realElBorder[side])) + "px"; - elStyling["position"] = "absolute"; - - return elStyling; - }; - - var setVisibility = function (el) { - if ( - !remoteHighlight.showPaddingMargin || - parseInt(el.height, 10) <= 0 || - parseInt(el.width, 10) <= 0 - ) { - el.display = 'none'; - } else { - el.display = 'block'; - } - }; - - var paddingVisualisations = [ - drawPaddingRect("top"), - drawPaddingRect("right"), - drawPaddingRect("bottom"), - drawPaddingRect("left") - ]; - - var marginVisualisations = [ - drawMarginRect("top"), - drawMarginRect("right"), - drawMarginRect("bottom"), - drawMarginRect("left") - ]; - - var setupVisualisations = function (arr, visualConfig) { - var i; - for (i = 0; i < arr.length; i++) { - setVisibility(arr[i]); - - // Applies to every visualisationElement (padding or margin div) - arr[i]["transform"] = "none"; - var el = window.document.createElement("div"), - styles = Object.assign({}, visualConfig, arr[i]); - - _setStyleValues(styles, el.style); - - highlightDiv.appendChild(el); - } - }; - - setupVisualisations( - marginVisualisations, - remoteHighlight.marginStyling - ); - setupVisualisations( - paddingVisualisations, - remoteHighlight.paddingStyling - ); - - highlightDiv.className = GLOBALS.HIGHLIGHT_CLASSNAME; - - var offset = LivePreviewView.screenOffset(element); - - // some code to find element left/top was removed here. This seems to be relevant to box model - // live highlights. firether reading: https://github.com/adobe/brackets/pull/13357/files - // we removed this in phoenix because it was throwing the rendering of live highlight boxes in phonix - // default project at improper places. Some other cases might fail as the above code said they - // introduces that removed computation for fixing some box-model regression. If you are here to fix a - // related bug, check history of this changes in git. - - var stylesToSet = { - "left": offset.left + "px", - "top": offset.top + "px", - "width": elementBounds.width + "px", - "height": elementBounds.height + "px", - "z-index": 2147483645, - "margin": 0, - "padding": 0, - "position": "absolute", - "pointer-events": "none", - "box-shadow": "0 0 1px #fff", - "box-sizing": elementStyling.getPropertyValue('box-sizing'), - "border-right": elementStyling.getPropertyValue('border-right'), - "border-left": elementStyling.getPropertyValue('border-left'), - "border-top": elementStyling.getPropertyValue('border-top'), - "border-bottom": elementStyling.getPropertyValue('border-bottom'), - "border-color": remoteHighlight.borderColor - }; - - var mergedStyles = Object.assign({}, stylesToSet, remoteHighlight.stylesToSet); - - var animateStartValues = remoteHighlight.animateStartValue; - - var animateEndValues = remoteHighlight.animateEndValue; - - var transitionValues = { - "transition-property": "opacity, background-color, transform", - "transition-duration": "300ms, 2.3s" - }; - - function _setStyleValues(styleValues, obj) { - var prop; - - for (prop in styleValues) { - obj.setProperty(prop, styleValues[prop]); - } - } - - _setStyleValues(mergedStyles, highlightDiv.style); - _setStyleValues( - doAnimation ? animateStartValues : animateEndValues, - highlightDiv.style - ); - - - if (doAnimation) { - _setStyleValues(transitionValues, highlightDiv.style); - - window.setTimeout(function () { - _setStyleValues(animateEndValues, highlightDiv.style); - }, 20); - } - - window.document.body.appendChild(highlightDiv); - }, - - add: function (element, doAnimation) { - if (this._elementExists(element) || element === window.document) { + add: function (element) { + if (this.elements.includes(element) || element === window.document) { return; } if (this.trigger) { @@ -533,42 +278,158 @@ function RemoteFunctions(config = {}) { } this.elements.push(element); - this._makeHighlightDiv(element, doAnimation); + this._createOverlay(element); }, clear: function () { - var i, highlights = window.document.querySelectorAll("." + GLOBALS.HIGHLIGHT_CLASSNAME), - body = window.document.body; - - for (i = 0; i < highlights.length; i++) { - body.removeChild(highlights[i]); - } - - for (i = 0; i < this.elements.length; i++) { - if (this.trigger) { - _trigger(this.elements[i], "highlight", 0); + this._divs.forEach(function (div) { + if (div.parentNode) { + div.parentNode.removeChild(div); } - clearElementHoverHighlight(this.elements[i]); + }); + this._divs = []; + + if (this.trigger) { + this.elements.forEach(function (el) { + _trigger(el, "highlight", 0); + }); } this.elements = []; }, redraw: function () { - var i, highlighted; + const elements = this.selector + ? Array.from(window.document.querySelectorAll(this.selector)) + : this.elements.slice(); + this.clear(); + elements.forEach(function (el) { this.add(el); }, this); + }, - // When redrawing a selector-based highlight, run a new selector - // query to ensure we have the latest set of elements to highlight. - if (this.selector) { - highlighted = window.document.querySelectorAll(this.selector); - } else { - highlighted = this.elements.slice(0); - } + _createOverlay: function (element) { + const bounds = element.getBoundingClientRect(); + if (bounds.width === 0 && bounds.height === 0) { return; } + + const cs = window.getComputedStyle(element); + + // Parse box model values (getComputedStyle always resolves to px) + const bt = parseFloat(cs.borderTopWidth) || 0, + br = parseFloat(cs.borderRightWidth) || 0, + bb = parseFloat(cs.borderBottomWidth) || 0, + bl = parseFloat(cs.borderLeftWidth) || 0; + const pt = parseFloat(cs.paddingTop) || 0, + pr = parseFloat(cs.paddingRight) || 0, + pb = parseFloat(cs.paddingBottom) || 0, + pl = parseFloat(cs.paddingLeft) || 0; + const mt = parseFloat(cs.marginTop) || 0, + mr = parseFloat(cs.marginRight) || 0, + mb = parseFloat(cs.marginBottom) || 0, + ml = parseFloat(cs.marginLeft) || 0; + + // Compute the 4 absolute boxes exactly like dev tools: + // getBoundingClientRect() always returns the border box regardless of box-sizing. + const scroll = LivePreviewView.screenOffset(element); + const borderBox = { + left: scroll.left, + top: scroll.top, + width: bounds.width, + height: bounds.height + }; + const paddingBox = { + left: borderBox.left + bl, + top: borderBox.top + bt, + width: borderBox.width - bl - br, + height: borderBox.height - bt - bb + }; + const contentBox = { + left: paddingBox.left + pl, + top: paddingBox.top + pt, + width: paddingBox.width - pl - pr, + height: paddingBox.height - pt - pb + }; + const marginBox = { + left: borderBox.left - ml, + top: borderBox.top - mt, + width: borderBox.width + ml + mr, + height: borderBox.height + mt + mb + }; - this.clear(); - for (i = 0; i < highlighted.length; i++) { - this.add(highlighted[i], false); + // Container div — sized to the margin box so all rects fit inside it + const div = window.document.createElement("div"); + div.className = GLOBALS.HIGHLIGHT_CLASSNAME; + div.trackingElement = element; + const divStyle = div.style; + divStyle.position = "absolute"; + divStyle.left = marginBox.left + "px"; + divStyle.top = marginBox.top + "px"; + divStyle.width = marginBox.width + "px"; + divStyle.height = marginBox.height + "px"; + divStyle.zIndex = 2147483645; + divStyle.margin = "0"; + divStyle.padding = "0"; + divStyle.border = "none"; + divStyle.pointerEvents = "none"; + divStyle.boxSizing = "border-box"; + + // Helper to create a colored rect at absolute page coordinates, offset by the container origin + function makeRect(left, top, width, height, color) { + if (width <= 0 || height <= 0) { return; } + const r = window.document.createElement("div"); + r.style.position = "absolute"; + r.style.left = (left - marginBox.left) + "px"; + r.style.top = (top - marginBox.top) + "px"; + r.style.width = width + "px"; + r.style.height = height + "px"; + r.style.backgroundColor = color; + div.appendChild(r); } + + // Padding region: 4 rects filling paddingBox minus contentBox + const padColor = COLORS.highlightPadding; + // top padding + makeRect(paddingBox.left, paddingBox.top, + paddingBox.width, pt, padColor); + // bottom padding + makeRect(paddingBox.left, contentBox.top + contentBox.height, + paddingBox.width, pb, padColor); + // left padding + makeRect(paddingBox.left, contentBox.top, + pl, contentBox.height, padColor); + // right padding + makeRect(contentBox.left + contentBox.width, contentBox.top, + pr, contentBox.height, padColor); + + // Margin region: 4 rects filling marginBox minus borderBox + const margColor = COLORS.highlightMargin; + // top margin + makeRect(marginBox.left, marginBox.top, + marginBox.width, mt, margColor); + // bottom margin + makeRect(marginBox.left, borderBox.top + borderBox.height, + marginBox.width, mb, margColor); + // left margin + makeRect(marginBox.left, borderBox.top, + ml, borderBox.height, margColor); + // right margin + makeRect(borderBox.left + borderBox.width, borderBox.top, + mr, borderBox.height, margColor); + + // Selection outline: 1px border at the border-box edge (drawn inside the border area) + const isEditable = element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR); + const outlineColor = isEditable ? COLORS.outlineEditable : COLORS.outlineNonEditable; + const outlineDiv = window.document.createElement("div"); + outlineDiv.style.position = "absolute"; + outlineDiv.style.left = (borderBox.left - marginBox.left) + "px"; + outlineDiv.style.top = (borderBox.top - marginBox.top) + "px"; + outlineDiv.style.width = borderBox.width + "px"; + outlineDiv.style.height = borderBox.height + "px"; + outlineDiv.style.border = `1px solid ${outlineColor}`; + outlineDiv.style.boxSizing = "border-box"; + outlineDiv.style.pointerEvents = "none"; + div.appendChild(outlineDiv); + + window.document.body.appendChild(div); + this._divs.push(div); } }; @@ -583,14 +444,6 @@ function RemoteFunctions(config = {}) { return getHighlightMode() !== "click"; } - // helper function to clear element hover outline highlighting - function clearElementHoverHighlight(element) { - if (element._originalHoverOutline !== undefined) { - element.style.outline = element._originalHoverOutline; - } - delete element._originalHoverOutline; - } - function onElementHover(event) { // don't want highlighting and stuff when auto scrolling or when dragging (svgs) // for dragging normal html elements its already taken care of...so we just add svg drag checking @@ -608,25 +461,27 @@ function RemoteFunctions(config = {}) { // if _hoverHighlight is uninitialized, initialize it if (!_hoverHighlight && shouldShowHighlightOnHover()) { - _hoverHighlight = new Highlight("#c8f9c5", true); + _hoverHighlight = new Highlight(true); } // this is to check the user's settings, if they want to show the elements highlights on hover or click if (_hoverHighlight && shouldShowHighlightOnHover()) { _hoverHighlight.clear(); - // Store original outline to restore on hover out, then apply a blue border - element._originalHoverOutline = element.style.outline; - const outlineColor = element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR) ? "#4285F4" : "#3C3F41"; - element.style.outline = `1px solid ${outlineColor}`; - - _hoverHighlight.add(element, false); + // Skip hover overlay for the currently click-selected element. + // It already has its own overlay from the click/selection flow, + // and adding hover state on top would stack duplicate overlays. + if (element !== previouslySelectedElement) { + _hoverHighlight.add(element); + } - // create the info box for the hovered element - const infoBoxHandler = LivePreviewView.getToolHandler("InfoBox"); - if (infoBoxHandler) { - infoBoxHandler.dismiss(); - infoBoxHandler.createInfoBox(element); + // Show minimal hover tooltip (tag + dimensions) + const hoverBoxHandler = LivePreviewView.getToolHandler("HoverBox"); + if (hoverBoxHandler) { + hoverBoxHandler.dismiss(); + if (element !== previouslySelectedElement) { + hoverBoxHandler.createHoverBox(element); + } } } } @@ -636,15 +491,16 @@ function RemoteFunctions(config = {}) { if (SHARED_STATE.isAutoScrolling) { return; } const element = event.target; - if(LivePreviewView.isElementEditable(element) && element.nodeType === Node.ELEMENT_NODE) { + // Use isElementInspectable (not isElementEditable) so that JS-rendered + // elements also get their hover highlight and hover box properly dismissed. + if(LivePreviewView.isElementInspectable(element) && element.nodeType === Node.ELEMENT_NODE) { // this is to check the user's settings, if they want to show the elements highlights on hover or click if (_hoverHighlight && shouldShowHighlightOnHover()) { _hoverHighlight.clear(); - clearElementHoverHighlight(element); - // dismiss the info box - const infoBoxHandler = LivePreviewView.getToolHandler("InfoBox"); - if (infoBoxHandler) { - infoBoxHandler.dismiss(); + // dismiss the hover box + const hoverBoxHandler = LivePreviewView.getToolHandler("HoverBox"); + if (hoverBoxHandler) { + hoverBoxHandler.dismiss(); } } } @@ -668,44 +524,47 @@ function RemoteFunctions(config = {}) { /** * this function is responsible to select an element in the live preview * @param {Element} element - The DOM element to select + * @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. */ - function selectElement(element) { + function selectElement(element, fromEditor) { dismissUIAndCleanupState(); // this should also be there when users are in highlight mode scrollElementToViewPort(element); - if(!LivePreviewView.isElementInspectable(element)) { + if(!LivePreviewView.isElementInspectable(element, true)) { return false; } - // when user clicks on a non-editable element - if (!element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR)) { - getAllToolHandlers().forEach(handler => { - if (handler.onNonEditableElementClick) { - handler.onNonEditableElementClick(element); - } - }); - } + // Only invoke tool handlers for user-initiated clicks in the live preview, + // not for editor cursor movements which should only show lightweight highlights + if (!fromEditor) { + // when user clicks on a non-editable element + if (!element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR)) { + getAllToolHandlers().forEach(handler => { + if (handler.onNonEditableElementClick) { + handler.onNonEditableElementClick(element); + } + }); + } - // make sure that the element is actually visible to the user - if (isElementVisible(element)) { - // Notify handlers about element selection - getAllToolHandlers().forEach(handler => { - if (handler.onElementSelected) { - handler.onElementSelected(element); - } - }); + // make sure that the element is actually visible to the user + if (isElementVisible(element)) { + // Notify handlers about element selection + getAllToolHandlers().forEach(handler => { + if (handler.onElementSelected) { + handler.onElementSelected(element); + } + }); + } } - element._originalOutline = element.style.outline; - const outlineColor = element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR) ? "#4285F4" : "#3C3F41"; - element.style.outline = `1px solid ${outlineColor}`; - if (!_clickHighlight) { - _clickHighlight = new Highlight("#cfc"); + _clickHighlight = new Highlight(); } _clickHighlight.clear(); - _clickHighlight.add(element, true); + _clickHighlight.add(element); previouslySelectedElement = element; window.__current_ph_lp_selected = element; @@ -778,7 +637,8 @@ function RemoteFunctions(config = {}) { } // send cursor movement message to editor so cursor jumps to clicked element - if (element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR)) { + if (element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR) && + config.syncSourceAndPreview !== false) { MessageBroker.send({ "tagId": element.getAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR), "nodeID": element.id, @@ -794,7 +654,7 @@ function RemoteFunctions(config = {}) { selectElement(element); } - // clear temporary CSS selector highlights + // clear CSS selector highlights function clearCssSelectorHighlight() { if (_cssSelectorHighlightTimer) { clearTimeout(_cssSelectorHighlightTimer); @@ -806,22 +666,22 @@ function RemoteFunctions(config = {}) { } } - // create temporary CSS selector highlights for edit mode + // create CSS selector highlights for edit mode function createCssSelectorHighlight(nodes, rule) { - // Clear any existing temporary highlights + // Clear any existing highlights clearCssSelectorHighlight(); - // Create new temporary highlight for all matching elements - _cssSelectorHighlight = new Highlight("#cfc"); - for (var i = 0; i < nodes.length; i++) { - if (LivePreviewView.isElementInspectable(nodes[i], true) && nodes[i].nodeType === Node.ELEMENT_NODE) { - _cssSelectorHighlight.add(nodes[i], true); + // Highlight all matching elements except the selected one + // (it already has a click highlight) + _cssSelectorHighlight = new Highlight(); + for (let i = 0; i < nodes.length; i++) { + if (nodes[i] !== previouslySelectedElement && + LivePreviewView.isElementInspectable(nodes[i], true) && + nodes[i].nodeType === Node.ELEMENT_NODE) { + _cssSelectorHighlight.add(nodes[i]); } } _cssSelectorHighlight.selector = rule; - - // Clear temporary highlights after 2 seconds - _cssSelectorHighlightTimer = setTimeout(clearCssSelectorHighlight, 2000); } // remove active highlights @@ -840,13 +700,13 @@ function RemoteFunctions(config = {}) { // highlight an element function highlight(element, clear) { if (!_clickHighlight) { - _clickHighlight = new Highlight("#cfc"); + _clickHighlight = new Highlight(); } if (clear) { _clickHighlight.clear(); } if (LivePreviewView.isElementInspectable(element, true) && element.nodeType === Node.ELEMENT_NODE) { - _clickHighlight.add(element, true); + _clickHighlight.add(element); } } @@ -903,6 +763,17 @@ function RemoteFunctions(config = {}) { */ function highlightRule(rule) { hideHighlight(); + + // Filter out the universal selector (*) from the rule - highlighting everything + // is not useful, similar to how we skip html/body in isElementInspectable. + // The rule can be a comma-separated list of selectors (from multi-cursor), + // so we filter out any standalone * segments and keep valid ones. + rule = rule.split(",").map(s => s.trim()).filter(s => s !== "*").join(","); + if (!rule) { + dismissUIAndCleanupState(); + return; + } + const nodes = window.document.querySelectorAll(rule); // Highlight all matching nodes @@ -914,43 +785,44 @@ function RemoteFunctions(config = {}) { _clickHighlight.selector = rule; } - // Find and select the best element - const { element, skipSelection } = findBestElementToSelect(nodes, rule); + // In edit mode, select the best element and create temporary highlights for the rest. + // In highlight mode, skip selection so all matching elements stay highlighted equally. + if (config.mode === 'edit') { + const { element, skipSelection } = findBestElementToSelect(nodes, rule); - if (!skipSelection) { - if (element) { - selectElement(element); - } else { - // No valid element found, dismiss UI - dismissUIAndCleanupState(); + if (!skipSelection) { + if (element) { + selectElement(element, true); + } else { + // No valid element found, dismiss UI + dismissUIAndCleanupState(); + } } - } - // In edit mode, create temporary highlights AFTER selection to avoid clearing - if (config.mode === 'edit') { createCssSelectorHighlight(nodes, rule); } } // recreate UI boxes so that they are placed properly function redrawUIBoxes() { - if (SHARED_STATE._toolBox) { - const element = SHARED_STATE._toolBox.element; - const toolBoxHandler = LivePreviewView.getToolHandler("ToolBox"); - if (toolBoxHandler) { - toolBoxHandler.dismiss(); - toolBoxHandler.createToolBox(element); - } - } - - if (SHARED_STATE._infoBox) { - const element = SHARED_STATE._infoBox.element; - const infoBoxHandler = LivePreviewView.getToolHandler("InfoBox"); - if (infoBoxHandler) { - infoBoxHandler.dismiss(); - infoBoxHandler.createInfoBox(element); - } - } + // commented out for unified box redesign + // if (SHARED_STATE._toolBox) { + // const element = SHARED_STATE._toolBox.element; + // const toolBoxHandler = LivePreviewView.getToolHandler("ToolBox"); + // if (toolBoxHandler) { + // toolBoxHandler.dismiss(); + // toolBoxHandler.createToolBox(element); + // } + // } + + // if (SHARED_STATE._infoBox) { + // const element = SHARED_STATE._infoBox.element; + // const infoBoxHandler = LivePreviewView.getToolHandler("InfoBox"); + // if (infoBoxHandler) { + // infoBoxHandler.dismiss(); + // infoBoxHandler.createInfoBox(element); + // } + // } } // redraw active highlights @@ -1261,10 +1133,60 @@ function RemoteFunctions(config = {}) { redrawEverything(); } } else { - // Suppression is active - re-apply outline since attrChange may have wiped it - if (previouslySelectedElement && previouslySelectedElement.isConnected) { - const outlineColor = previouslySelectedElement.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR) ? "#4285F4" : "#3C3F41"; - previouslySelectedElement.style.outline = `1px solid ${outlineColor}`; + // Suppression is active (e.g., control box initiated a source edit) + if (previouslySelectedElement && !previouslySelectedElement.isConnected) { + let freshElement = null; + + // Strategy 1: Tree path (most reliable — works even with duplicate + // text content and tag changes). Stored when suppression was activated. + if (SHARED_STATE._suppressedElementPath) { + freshElement = _getElementByTreePath(SHARED_STATE._suppressedElementPath); + } + + // Strategy 2: brackets-id (works when IDs are preserved) + if (!freshElement) { + const bracketsId = previouslySelectedElement.getAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR); + if (bracketsId) { + freshElement = document.querySelector( + '[' + GLOBALS.DATA_BRACKETS_ID_ATTR + '="' + bracketsId + '"]' + ); + } + } + + // Strategy 3: Text + tag match (fallback — search reverse for deepest match) + if (!freshElement) { + const oldText = previouslySelectedElement.textContent; + const oldTag = previouslySelectedElement.tagName; + let candidates = document.querySelectorAll( + oldTag.toLowerCase() + '[' + GLOBALS.DATA_BRACKETS_ID_ATTR + ']' + ); + for (let i = candidates.length - 1; i >= 0; i--) { + if (candidates[i].textContent === oldText) { + freshElement = candidates[i]; + break; + } + } + // Broaden if tag changed (e.g. h2→footer) + if (!freshElement) { + candidates = document.querySelectorAll('[' + GLOBALS.DATA_BRACKETS_ID_ATTR + ']'); + for (let i = candidates.length - 1; i >= 0; i--) { + if (candidates[i].textContent === oldText) { + freshElement = candidates[i]; + break; + } + } + } + } + + if (freshElement) { + if (_clickHighlight) { + _clickHighlight.clear(); + _clickHighlight.add(freshElement); + } + previouslySelectedElement = freshElement; + window.__current_ph_lp_selected = freshElement; + redrawEverything(); + } } } }; @@ -1301,7 +1223,21 @@ function RemoteFunctions(config = {}) { _handleConfigurationChange(); } + // Preserve the currently selected element across re-registration + // so that toggling options (e.g. show measurements, show spacing handles) + // doesn't clear the element highlighting. + const selectedBeforeReregister = previouslySelectedElement; registerHandlers(); + if (!isModeChanged && !highlightModeChanged && selectedBeforeReregister + && config.mode === 'edit') { + // Restore the click highlight for the previously selected element + if (!_clickHighlight) { + _clickHighlight = new Highlight(true); + } + _clickHighlight.add(selectedBeforeReregister); + previouslySelectedElement = selectedBeforeReregister; + window.__current_ph_lp_selected = selectedBeforeReregister; + } return JSON.stringify(config); } @@ -1318,19 +1254,14 @@ function RemoteFunctions(config = {}) { */ function cleanupPreviousElementState() { if (previouslySelectedElement) { - if (previouslySelectedElement._originalOutline !== undefined) { - previouslySelectedElement.style.outline = previouslySelectedElement._originalOutline; - } else { - previouslySelectedElement.style.outline = ""; - } - delete previouslySelectedElement._originalOutline; previouslySelectedElement = null; window.__current_ph_lp_selected = null; } - if (config.mode === 'edit') { - hideHighlight(); + // Highlight.clear() removes all overlay divs (outline + margin/padding rects) + hideHighlight(); + if (config.mode === 'edit') { // Notify handlers about cleanup getAllToolHandlers().forEach(handler => { if (handler.onElementCleanup) { @@ -1340,6 +1271,43 @@ function RemoteFunctions(config = {}) { } } + /** + * Compute the tree path of an element as an array of child indices + * from down. Used to re-locate the element after re-instrumentation + * when data-brackets-id changes and text matching is ambiguous. + * E.g. [1, 0, 0, 1] means html > 2nd child > 1st child > 1st child > 2nd child. + */ + function _getTreePath(element) { + const path = []; + let el = element; + while (el && el.parentElement) { + const parent = el.parentElement; + const children = parent.children; + for (let i = 0; i < children.length; i++) { + if (children[i] === el) { + path.unshift(i); + break; + } + } + el = parent; + } + return path; + } + + /** + * Find an element by its tree path (array of child indices from ). + */ + function _getElementByTreePath(path) { + let el = document.documentElement; + for (let i = 0; i < path.length; i++) { + if (!el || !el.children || !el.children[path[i]]) { + return null; + } + el = el.children[path[i]]; + } + return el; + } + /** * Temporarily suppress the DOM edit dismissal check in apply() * Used when source is modified from UI panels to prevent @@ -1352,9 +1320,14 @@ function RemoteFunctions(config = {}) { clearTimeout(SHARED_STATE._suppressDOMEditDismissalTimeout); } SHARED_STATE._suppressDOMEditDismissal = true; + // Store the tree path while the element is still connected + if (previouslySelectedElement && previouslySelectedElement.isConnected) { + SHARED_STATE._suppressedElementPath = _getTreePath(previouslySelectedElement); + } SHARED_STATE._suppressDOMEditDismissalTimeout = setTimeout(function() { SHARED_STATE._suppressDOMEditDismissal = false; SHARED_STATE._suppressDOMEditDismissalTimeout = null; + SHARED_STATE._suppressedElementPath = null; }, durationMs); } @@ -1380,11 +1353,8 @@ function RemoteFunctions(config = {}) { }); if (config.mode === 'edit') { - // Initialize hover highlight with Chrome-like colors - _hoverHighlight = new Highlight("#c8f9c5", true); // Green similar to Chrome's padding color - - // Initialize click highlight with animation - _clickHighlight = new Highlight("#cfc", true); // Light green for click highlight + _hoverHighlight = new Highlight(true); + _clickHighlight = new Highlight(true); // register the event handlers enableHoverListeners(); @@ -1439,7 +1409,7 @@ function RemoteFunctions(config = {}) { customReturns = { // we have to do this else the minifier will strip the customReturns variable ...customReturns, "DOMEditHandler": DOMEditHandler, - "hideHighlight": hideHighlight, + "hideHighlight": dismissUIAndCleanupState, "highlight": highlight, "highlightRule": highlightRule, "redrawHighlights": redrawHighlights, @@ -1449,7 +1419,17 @@ function RemoteFunctions(config = {}) { "dismissUIAndCleanupState": dismissUIAndCleanupState, "escapeKeyPressInEditor": _handleEscapeKeyPress, "getMode": function() { return config.mode; }, - "suppressDOMEditDismissal": suppressDOMEditDismissal + "isSyncEnabled": function() { return config.syncSourceAndPreview !== false; }, + "suppressDOMEditDismissal": suppressDOMEditDismissal, + "setHotCornerHidden": function(hidden) { + if (SHARED_STATE._hotCorner && SHARED_STATE._hotCorner.hotCorner) { + if (hidden) { + SHARED_STATE._hotCorner.hotCorner.classList.add('hc-hidden'); + } else { + SHARED_STATE._hotCorner.hotCorner.classList.remove('hc-hidden'); + } + } + } }; // the below code comment is replaced by added scripts for extensibility diff --git a/src/LiveDevelopment/LiveDevMultiBrowser.js b/src/LiveDevelopment/LiveDevMultiBrowser.js index 50f3e9bd90..2b0f8bc417 100644 --- a/src/LiveDevelopment/LiveDevMultiBrowser.js +++ b/src/LiveDevelopment/LiveDevMultiBrowser.js @@ -737,9 +737,27 @@ define(function (require, exports, module) { * Update configuration in the remote browser */ function updateConfig(config) { + const previousConfig = _config; _config = config; _updateVirtualServerScripts(); refreshConfig(); + // Clear the highlight cache only when mode or highlight-mode changes. + // These changes wipe browser-side highlights (via _handleConfigurationChange), + // so the editor-side cache must be reset too, otherwise highlightRule() + // skips re-highlighting the same element on the next cursor activity. + // We intentionally skip this for other config changes (e.g. toggling + // measurements) where the selected element is preserved + const modeChanged = !previousConfig || previousConfig.mode !== config.mode; + const oldHighlights = previousConfig && previousConfig.elemHighlights + ? previousConfig.elemHighlights.toLowerCase() : "hover"; + const newHighlights = config.elemHighlights + ? config.elemHighlights.toLowerCase() : "hover"; + if (modeChanged || oldHighlights !== newHighlights) { + const doc = getLiveDocForEditor(EditorManager.getActiveEditor()); + if (doc) { + doc._lastHighlight = null; + } + } } /** diff --git a/src/LiveDevelopment/LivePreviewConstants.js b/src/LiveDevelopment/LivePreviewConstants.js index ecfc9a95db..756d8bb1e1 100644 --- a/src/LiveDevelopment/LivePreviewConstants.js +++ b/src/LiveDevelopment/LivePreviewConstants.js @@ -41,4 +41,7 @@ define(function main(require, exports, module) { exports.HIGHLIGHT_CLICK = "click"; exports.PREFERENCE_SHOW_RULER_LINES = "livePreviewShowMeasurements"; + exports.PREFERENCE_SHOW_SPACING_HANDLES = "livePreviewShowSpacingHandles"; + + exports.PREFERENCE_LIVE_PREVIEW_SYNC = "livePreviewSyncSourceAndPreview"; }); diff --git a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js index 9d5beb6260..48faddc261 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js +++ b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js @@ -192,7 +192,8 @@ define(function (require, exports, module) { if (!this.editor) { return; } - if(!_disableHighlightOnCursor){ + if(!_disableHighlightOnCursor && + PreferencesManager.get(CONSTANTS.PREFERENCE_LIVE_PREVIEW_SYNC) !== false){ this.updateHighlight(); } }; diff --git a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveHTMLDocument.js b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveHTMLDocument.js index 16730b2d7d..4ff3a1fadc 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/documents/LiveHTMLDocument.js +++ b/src/LiveDevelopment/MultiBrowserImpl/documents/LiveHTMLDocument.js @@ -34,6 +34,7 @@ define(function (require, exports, module) { _ = require("thirdparty/lodash"), LiveDocument = require("LiveDevelopment/MultiBrowserImpl/documents/LiveDocument"), HTMLInstrumentation = require("LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation"), + HTMLUtils = require("language/HTMLUtils"), CSSUtils = require("language/CSSUtils"); /** @@ -161,19 +162,28 @@ define(function (require, exports, module) { selectors = []; // check if the cursor is in a stylesheet context (internal styles) + // but skip CSS selector lookup for inline style attributes (style="...") + // since they have no selector — the element itself should be highlighted instead if (mode === "css" || mode === "text/x-scss" || mode === "text/x-less") { - // find the css selector - _.each(this.editor.getSelections(), function (sel) { - let selector = CSSUtils.findSelectorAtDocumentPos(editor, (sel.reversed ? sel.end : sel.start)); - if (selector) { - selectors.push(selector); - } - }); + var primarySel = editor.getSelection(); + var tagInfo = HTMLUtils.getTagInfo(editor, primarySel.start, true); + var isInlineStyle = tagInfo.position.tokenType === HTMLUtils.ATTR_VALUE && + tagInfo.attr.name.toLowerCase() === "style"; + + if (!isInlineStyle) { + // find the css selector + _.each(this.editor.getSelections(), function (sel) { + let selector = CSSUtils.findSelectorAtDocumentPos(editor, (sel.reversed ? sel.end : sel.start)); + if (selector) { + selectors.push(selector); + } + }); - if (selectors.length) { - // to highlight the elements that match the css selectors - this.highlightRule(selectors.join(",")); - return; + if (selectors.length) { + // to highlight the elements that match the css selectors + this.highlightRule(selectors.join(",")); + return; + } } } diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index 8f0ab12a93..70565d3340 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -240,6 +240,9 @@ define(function (require, exports, module) { // hilights are enabled only in edit and highlight mode return; } + if(PreferencesManager.get(CONSTANTS.PREFERENCE_LIVE_PREVIEW_SYNC) === false){ + return; + } const liveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(), activeEditor = EditorManager.getActiveEditor(), // this can be an inline editor activeFullEditor = EditorManager.getCurrentFullEditor(); diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 60d4caebf5..f4d2cc3527 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -70,6 +70,8 @@ define(function main(require, exports, module) { mode: LIVE_HIGHLIGHT_MODE, // will be updated when we fetch entitlements elemHighlights: CONSTANTS.HIGHLIGHT_HOVER, // default value, this will get updated when the extension loads showRulerLines: false, // default value, this will get updated when the extension loads + showSpacingHandles: true, // default value, this will get updated when the extension loads + syncSourceAndPreview: true, // default value, this will get updated when the extension loads isPaidUser: false, // will be updated when we fetch entitlements isLoggedIn: false, // will be updated when we fetch entitlements hasLiveEditCapability: false // handled inside _liveEditCapabilityChanged function @@ -324,6 +326,20 @@ define(function main(require, exports, module) { MultiBrowserLiveDev.updateConfig(config); } + function updateSpacingHandlesConfig() { + const prefValue = PreferencesManager.get(CONSTANTS.PREFERENCE_SHOW_SPACING_HANDLES); + const config = MultiBrowserLiveDev.getConfig(); + config.showSpacingHandles = prefValue !== false; + MultiBrowserLiveDev.updateConfig(config); + } + + function updateSyncConfig() { + const prefValue = PreferencesManager.get(CONSTANTS.PREFERENCE_LIVE_PREVIEW_SYNC); + const config = MultiBrowserLiveDev.getConfig(); + config.syncSourceAndPreview = prefValue !== false; + MultiBrowserLiveDev.updateConfig(config); + } + EventDispatcher.makeEventDispatcher(exports); // private api @@ -347,6 +363,8 @@ define(function main(require, exports, module) { exports.setLivePreviewTransportBridge = setLivePreviewTransportBridge; exports.updateElementHighlightConfig = updateElementHighlightConfig; exports.updateRulerLinesConfig = updateRulerLinesConfig; + exports.updateSpacingHandlesConfig = updateSpacingHandlesConfig; + exports.updateSyncConfig = updateSyncConfig; exports.getConnectionIds = MultiBrowserLiveDev.getConnectionIds; exports.getLivePreviewDetails = MultiBrowserLiveDev.getLivePreviewDetails; exports.hideHighlight = MultiBrowserLiveDev.hideHighlight; diff --git a/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css b/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css index 2a0d040ee6..c59f9b47e5 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css +++ b/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css @@ -80,13 +80,9 @@ } #live-preview-plugin-toolbar:hover .lp-settings-icon { - display: flex; - align-items: center; - color: #a0a0a0; opacity: 1; visibility: visible; transition: unset; - padding-left: 7.5px; } .live-preview-settings input.error, .live-preview-settings input:focus.error{ @@ -98,25 +94,37 @@ .lp-settings-icon { opacity: 0; color: #a0a0a0; - display: flex; - align-items: center; visibility: hidden; transition: opacity 1s, visibility 0s linear 1s; /* Fade-out effect */ - padding-left: 7.5px; + width: 30px; + height: 22px; + padding: 1px 6px; + flex-shrink: 0; + margin-top: 3.5px; } .lp-device-size-icon { - color: #a0a0a0; + min-width: fit-content; display: flex; align-items: center; - padding-left: 7.5px; - margin-right: 7.5px; + margin: 3.5px 4px 0 3px; + cursor: pointer; + background: #3C3F41; + box-shadow: none; + border: 1px solid #3C3F41; + box-sizing: border-box; + color: #a0a0a0; + padding: 0 0.35em; +} + +.lp-device-size-icon:hover { + border: 1px solid rgba(0, 0, 0, 0.24) !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12) !important; } #deviceSizeBtn.btn-dropdown::after { position: static; - margin-top: 2px; - margin-left: 3px; + margin-left: 5px; } .device-size-item-icon { @@ -154,7 +162,7 @@ min-width: fit-content; display: flex; align-items: center; - margin: 3px 4px 0 3px; + margin: 3.5px 4px 0 3px; max-width: 80%; text-overflow: ellipsis; overflow: hidden; diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js index 6b63e3080f..5ecbb0031c 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/main.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js @@ -110,6 +110,18 @@ define(function (require, exports, module) { description: Strings.LIVE_DEV_SETTINGS_SHOW_RULER_LINES_PREFERENCE }); + // live preview spacing handles preference (show/hide spacing handles on element selection) + const PREFERENCE_SHOW_SPACING_HANDLES = CONSTANTS.PREFERENCE_SHOW_SPACING_HANDLES; + PreferencesManager.definePreference(PREFERENCE_SHOW_SPACING_HANDLES, "boolean", true, { + description: Strings.LIVE_DEV_SETTINGS_SHOW_SPACING_HANDLES_PREFERENCE + }); + + // live preview sync source and preview preference + const PREFERENCE_LIVE_PREVIEW_SYNC = CONSTANTS.PREFERENCE_LIVE_PREVIEW_SYNC; + PreferencesManager.definePreference(PREFERENCE_LIVE_PREVIEW_SYNC, "boolean", true, { + description: Strings.LIVE_DEV_SETTINGS_SYNC_SOURCE_AND_PREVIEW_PREFERENCE + }); + const LIVE_PREVIEW_PANEL_ID = "live-preview-panel"; const LIVE_PREVIEW_IFRAME_ID = "panel-live-preview-frame"; const LIVE_PREVIEW_IFRAME_HTML = ` @@ -327,21 +339,28 @@ define(function (require, exports, module) { function _showModeSelectionDropdown(event) { const isEditFeaturesActive = isProEditUser; + const currentMode = LiveDevelopment.getCurrentMode(); + const isNotPreviewMode = currentMode !== LiveDevelopment.CONSTANTS.LIVE_PREVIEW_MODE; const items = [ Strings.LIVE_PREVIEW_MODE_PREVIEW, Strings.LIVE_PREVIEW_MODE_HIGHLIGHT, Strings.LIVE_PREVIEW_MODE_EDIT ]; - // Only add edit highlight option if edit features are active - if (isEditFeaturesActive) { + // Add sync toggle for highlight and edit modes + if (isNotPreviewMode) { items.push("---"); + items.push(Strings.LIVE_PREVIEW_SYNC_SOURCE_AND_PREVIEW); + } + + // Only add edit-specific options when in edit mode and edit features are active + const isEditMode = currentMode === LiveDevelopment.CONSTANTS.LIVE_EDIT_MODE; + if (isEditFeaturesActive && isEditMode) { items.push(Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON); items.push(Strings.LIVE_PREVIEW_SHOW_RULER_LINES); + items.push(Strings.LIVE_PREVIEW_SHOW_SPACING_HANDLES); } - const currentMode = LiveDevelopment.getCurrentMode(); - $dropdown = new DropdownButton.DropdownButton("", items, function(item, index) { if (item === Strings.LIVE_PREVIEW_MODE_PREVIEW) { // using empty spaces to keep content aligned @@ -359,6 +378,12 @@ define(function (require, exports, module) { html: `${checkmark}${item}${crownIcon}`, enabled: true }; + } else if (item === Strings.LIVE_PREVIEW_SYNC_SOURCE_AND_PREVIEW) { + const isEnabled = PreferencesManager.get(PREFERENCE_LIVE_PREVIEW_SYNC) !== false; + if(isEnabled) { + return `✓ ${Strings.LIVE_PREVIEW_SYNC_SOURCE_AND_PREVIEW}`; + } + return `${'\u00A0'.repeat(4)}${Strings.LIVE_PREVIEW_SYNC_SOURCE_AND_PREVIEW}`; } else if (item === Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON) { const isHoverMode = PreferencesManager.get(PREFERENCE_PROJECT_ELEMENT_HIGHLIGHT) === CONSTANTS.HIGHLIGHT_HOVER; @@ -372,6 +397,12 @@ define(function (require, exports, module) { return `✓ ${Strings.LIVE_PREVIEW_SHOW_RULER_LINES}`; } return `${'\u00A0'.repeat(4)}${Strings.LIVE_PREVIEW_SHOW_RULER_LINES}`; + } else if (item === Strings.LIVE_PREVIEW_SHOW_SPACING_HANDLES) { + const isEnabled = PreferencesManager.get(PREFERENCE_SHOW_SPACING_HANDLES); + if(isEnabled) { + return `✓ ${Strings.LIVE_PREVIEW_SHOW_SPACING_HANDLES}`; + } + return `${'\u00A0'.repeat(4)}${Strings.LIVE_PREVIEW_SHOW_SPACING_HANDLES}`; } return item; }); @@ -409,6 +440,11 @@ define(function (require, exports, module) { Metrics.countEvent(Metrics.EVENT_TYPE.PRO, "proUpsellDlg", "fail"); } } + } else if (item === Strings.LIVE_PREVIEW_SYNC_SOURCE_AND_PREVIEW) { + // Toggle sync source and preview on/off + const currentValue = PreferencesManager.get(PREFERENCE_LIVE_PREVIEW_SYNC); + PreferencesManager.set(PREFERENCE_LIVE_PREVIEW_SYNC, currentValue === false); + return; // Don't dismiss for this option } else if (item === Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON) { // Don't allow edit highlight toggle if edit features are not active if (!isEditFeaturesActive) { @@ -429,6 +465,15 @@ define(function (require, exports, module) { const currentValue = PreferencesManager.get(PREFERENCE_SHOW_RULER_LINES); PreferencesManager.set(PREFERENCE_SHOW_RULER_LINES, !currentValue); return; // Don't dismiss highlights for this option + } else if (item === Strings.LIVE_PREVIEW_SHOW_SPACING_HANDLES) { + // Don't allow spacing handles toggle if edit features are not active + if (!isEditFeaturesActive) { + return; + } + // Toggle spacing handles on/off + const currentValue = PreferencesManager.get(PREFERENCE_SHOW_SPACING_HANDLES); + PreferencesManager.set(PREFERENCE_SHOW_SPACING_HANDLES, !currentValue); + return; // Don't dismiss highlights for this option } }); @@ -1227,10 +1272,18 @@ define(function (require, exports, module) { PreferencesManager.on("change", PREFERENCE_SHOW_RULER_LINES, function() { LiveDevelopment.updateRulerLinesConfig(); }); + PreferencesManager.on("change", PREFERENCE_SHOW_SPACING_HANDLES, function() { + LiveDevelopment.updateSpacingHandlesConfig(); + }); + PreferencesManager.on("change", PREFERENCE_LIVE_PREVIEW_SYNC, function() { + LiveDevelopment.updateSyncConfig(); + }); - // Initialize element highlight and ruler lines config on startup + // Initialize element highlight, ruler lines, spacing handles, and sync config on startup LiveDevelopment.updateElementHighlightConfig(); LiveDevelopment.updateRulerLinesConfig(); + LiveDevelopment.updateSpacingHandlesConfig(); + LiveDevelopment.updateSyncConfig(); LiveDevelopment.openLivePreview(); LiveDevelopment.on(LiveDevelopment.EVENT_OPEN_PREVIEW_URL, _openLivePreviewURL); diff --git a/src/language/CSSUtils.js b/src/language/CSSUtils.js index 99e7067206..c217474942 100644 --- a/src/language/CSSUtils.js +++ b/src/language/CSSUtils.js @@ -1590,7 +1590,7 @@ define(function (require, exports, module) { function findSelectorAtDocumentPos(editor, pos) { var cm = editor._codeMirror; var ctx = TokenUtils.getInitialContext(cm, $.extend({}, pos)); - var selector = "", foundChars = false; + var selector = "", foundChars = false, encounteredSemicolon = false; var isPreprocessorDoc = isCSSPreprocessorFile(editor.document.file.fullPath); var selectorArray = []; @@ -1687,6 +1687,12 @@ define(function (require, exports, module) { break; } } else { + // Track semicolons encountered before any selector content. + // A ; at the top level (e.g. after @import) means the cursor + // is after a complete statement, not inside a selector. + if (!isPreprocessorDoc && ctx.token.string === ";" && !foundChars) { + encounteredSemicolon = true; + } if (!isPreprocessorDoc && _hasNonWhitespace(ctx.token.string)) { foundChars = true; } @@ -1699,6 +1705,22 @@ define(function (require, exports, module) { selector = _stripAtRules(selector); + // If no selector found and cursor is on a line that contains only }, + // find the rule that this } closes. The backward scan may have missed + // } when the cursor is at ch:0 (getTokenAt returns an empty token), + // or it may have hit } and broken without parsing. + if (!selector && !isPreprocessorDoc) { + var closingLineText = cm.getLine(pos.line).trim(); + if (closingLineText === "}") { + var braceCtx = TokenUtils.getInitialContext(cm, + {line: pos.line, ch: cm.getLine(pos.line).indexOf("}") + 1}); + _skipToOpeningBracket(braceCtx, "}"); + if (braceCtx.token.string === "{") { + selector = _stripAtRules(_parseSelector(braceCtx)); + } + } + } + // Reset the context to original scan position ctx = TokenUtils.getInitialContext(cm, $.extend({}, pos)); @@ -1718,7 +1740,11 @@ define(function (require, exports, module) { // scanning, assume we are in the middle of a selector. For a preprocessor // document we also need to collect the current selector if the cursor is // within the selector or whitespaces immediately before or after it. - if ((!selector || isPreprocessorDoc) && foundChars) { + // However, if the backward scan encountered a ; before any selector content + // and the cursor is on an empty/whitespace-only line, we are between + // statements (e.g. after @import;), not inside a selector. + if ((!selector || isPreprocessorDoc) && foundChars && + (!encounteredSemicolon || _hasNonWhitespace(cm.getLine(pos.line)))) { // scan forward to see if the cursor is in a selector while (true) { if (ctx.token.type !== "comment") { diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 421764934d..ee438335b4 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -190,12 +190,19 @@ define({ "LIVE_DEV_HYPERLINK_OPENS_NEW_TAB": "Opens in new tab", "LIVE_DEV_TOOLBOX_DUPLICATE": "Duplicate", "LIVE_DEV_TOOLBOX_DELETE": "Delete", - "LIVE_DEV_TOOLBOX_AI": "Edit with AI", "LIVE_DEV_TOOLBOX_IMAGE_GALLERY": "Image Gallery", "LIVE_DEV_TOOLBOX_MORE_OPTIONS": "More Options", "LIVE_DEV_MORE_OPTIONS_CUT": "Cut", "LIVE_DEV_MORE_OPTIONS_COPY": "Copy", "LIVE_DEV_MORE_OPTIONS_PASTE": "Paste", + "LIVE_DEV_INSERT_ELEMENT": "Insert Element", + "LIVE_DEV_INSERT_BEFORE": "Before", + "LIVE_DEV_INSERT_AFTER": "After", + "LIVE_DEV_INSERT_INSIDE": "Inside", + "LIVE_DEV_INSERT_WRAP": "Wrap", + "LIVE_DEV_INSERT_SEARCH_PLACEHOLDER": "Search elements\u2026", + "LIVE_DEV_INSERT_COMMON": "Common", + "LIVE_DEV_INSERT_NO_RESULTS": "No matching elements", "LIVE_DEV_IMAGE_GALLERY_USE_IMAGE": "Download image", "LIVE_DEV_IMAGE_GALLERY_SELECT_DOWNLOAD_FOLDER": "Choose image download folder", "LIVE_DEV_IMAGE_GALLERY_SEARCH_PLACEHOLDER": "Search images\u2026", @@ -232,6 +239,150 @@ define({ "LIVE_DEV_STYLES_PANEL_NO_STYLES": "No styles found", "LIVE_DEV_STYLES_PANEL_PROPERTY_PLACEHOLDER": "property", "LIVE_DEV_STYLES_PANEL_VALUE_PLACEHOLDER": "value", + "LIVE_DEV_STYLES_TAB_ADVANCED": "Styles", + "LIVE_DEV_STYLES_TAB_COMPUTED": "Computed", + "LIVE_DEV_STYLES_FILTER_ALL": "All", + "LIVE_DEV_STYLES_FILTER_LAYOUT": "Layout", + "LIVE_DEV_STYLES_FILTER_TYPOGRAPHY": "Typography", + "LIVE_DEV_STYLES_FILTER_COLOR": "Color", + "LIVE_DEV_STYLES_FILTER_EFFECTS": "Effects", + "LIVE_DEV_STYLES_FILTER_BOX_MODEL": "Box Model", + "LIVE_DEV_STYLES_COMPUTED_SEARCH": "Filter properties\u2026", + "LIVE_DEV_STYLES_COMPUTED_NO_RESULTS": "No results found", + "LIVE_DEV_STYLES_COMPUTED_USER_AGENT": "User Agent", + "LIVE_DEV_FORMAT_BOLD": "Bold", + "LIVE_DEV_FORMAT_ITALIC": "Italic", + "LIVE_DEV_FORMAT_UNDERLINE": "Underline", + "LIVE_DEV_FORMAT_STRIKETHROUGH": "Strikethrough", + "LIVE_DEV_ELEMENT_PROPS_TITLE": "Element Properties", + "LIVE_DEV_ELEMENT_PROPS_TAG": "Tag", + "LIVE_DEV_ELEMENT_PROPS_SIZE": "Size", + "LIVE_DEV_ELEMENT_PROPS_CLASS": "Class", + "LIVE_DEV_ELEMENT_PROPS_ID": "ID", + "LIVE_DEV_ELEMENT_PROPS_HREF": "Link", + "LIVE_DEV_ELEMENT_PROPS_ADD_CLASS": "+ add class", + "LIVE_DEV_ELEMENT_PROPS_SEARCH_TAGS": "Search tags\u2026", + "LIVE_DEV_ELEMENT_PROPS_COMPUTED": "Computed:", + "LIVE_DEV_ELEMENT_PROPS_USE_CUSTOM": "Use \u201c{0}\u201d", + "LIVE_DEV_CB_EXCEEDED_CLASSES": "+{0} more", + "LIVE_DEV_CB_TIP_TEXT_COLOR": "Text Color", + "LIVE_DEV_CB_TIP_BG_COLOR": "Background Color", + "LIVE_DEV_CB_TIP_FONT": "Font", + "LIVE_DEV_CB_TIP_TEXT_SPACING": "Text Spacing", + "LIVE_DEV_CB_TIP_APPEARANCE": "Border & Opacity", + "LIVE_DEV_CB_TIP_LAYOUT": "Layout", + "LIVE_DEV_CB_TIP_POSITION": "Position", + "LIVE_DEV_CB_TIP_OBJECT_FIT": "Object Fit", + "LIVE_DEV_CB_TIP_IMAGE": "Image", + "LIVE_DEV_CB_CHANGE_IMAGE": "Change Image", + "LIVE_DEV_CB_TIP_LIST_STYLE": "List Style", + "LIVE_DEV_CB_TIP_ALL_STYLES": "All Styles", + "LIVE_DEV_CB_LABEL_FAMILY": "Family", + "LIVE_DEV_CB_LABEL_SIZE": "Size", + "LIVE_DEV_CB_LABEL_WEIGHT": "Weight", + "LIVE_DEV_CB_LABEL_STYLE": "Style", + "LIVE_DEV_CB_LABEL_ALIGN": "Text Align", + "LIVE_DEV_CB_LABEL_TRANSFORM": "Transform", + "LIVE_DEV_CB_LABEL_LETTER_SPACING": "Letter Spacing", + "LIVE_DEV_CB_LABEL_WORD_SPACING": "Word Spacing", + "LIVE_DEV_CB_LABEL_LINE_HEIGHT": "Line Height", + "LIVE_DEV_CB_LABEL_TEXT_INDENT": "Text Indent", + "LIVE_DEV_CB_LABEL_WIDTH": "Width", + "LIVE_DEV_CB_LABEL_RADIUS": "Radius", + "LIVE_DEV_CB_LABEL_COLOR": "Color", + "LIVE_DEV_CB_LABEL_OPACITY": "Opacity", + "LIVE_DEV_CB_LABEL_DISPLAY": "Display", + "LIVE_DEV_CB_LABEL_DIRECTION": "Direction", + "LIVE_DEV_CB_LABEL_WRAP": "Wrap", + "LIVE_DEV_CB_LABEL_JUSTIFY": "Justify", + "LIVE_DEV_CB_LABEL_GAP": "Gap", + "LIVE_DEV_CB_LABEL_OVERFLOW": "Overflow", + "LIVE_DEV_CB_LABEL_POSITION": "Position", + "LIVE_DEV_CB_LABEL_TOP": "Top", + "LIVE_DEV_CB_LABEL_RIGHT": "Right", + "LIVE_DEV_CB_LABEL_BOTTOM": "Bottom", + "LIVE_DEV_CB_LABEL_LEFT": "Left", + "LIVE_DEV_CB_LABEL_Z_INDEX": "Z-Index", + "LIVE_DEV_CB_LABEL_FIT": "Fit", + "LIVE_DEV_CB_LABEL_TYPE": "Type", + "LIVE_DEV_CB_WEIGHT_THIN": "Thin", + "LIVE_DEV_CB_WEIGHT_REGULAR": "Regular", + "LIVE_DEV_CB_WEIGHT_BOLD": "Bold", + "LIVE_DEV_CB_WEIGHT_BLACK": "Black", + "LIVE_DEV_CB_STYLE_ITALIC": "Italic", + "LIVE_DEV_CB_STYLE_UNDERLINE": "Underline", + "LIVE_DEV_CB_STYLE_STRIKETHROUGH": "Strikethrough", + "LIVE_DEV_CB_STYLE_OVERLINE": "Overline", + "LIVE_DEV_CB_ALIGN_LEFT": "Left", + "LIVE_DEV_CB_ALIGN_CENTER": "Center", + "LIVE_DEV_CB_ALIGN_RIGHT": "Right", + "LIVE_DEV_CB_ALIGN_JUSTIFY": "Justify", + "LIVE_DEV_CB_TRANSFORM_NONE": "None", + "LIVE_DEV_CB_TRANSFORM_CAPITALIZE": "Capitalize", + "LIVE_DEV_CB_TRANSFORM_UPPERCASE": "Uppercase", + "LIVE_DEV_CB_TRANSFORM_LOWERCASE": "Lowercase", + "LIVE_DEV_CB_SEARCH_FONTS": "Search fonts\u2026", + "LIVE_DEV_CB_BACK": "Back", + "LIVE_DEV_CB_BACK_ESC": "Back (Esc)", + "LIVE_DEV_CB_RESET": "Reset", + "LIVE_DEV_CB_APPLIES_TO": "Applies to:", + "LIVE_DEV_CB_INLINE": "Inline", + "LIVE_DEV_CB_LIST_DISC": "Disc", + "LIVE_DEV_CB_LIST_CIRCLE": "Circle", + "LIVE_DEV_CB_LIST_SQUARE": "Square", + "LIVE_DEV_CB_LIST_NONE": "None", + "LIVE_DEV_CB_LIST_DECIMAL": "Decimal", + "LIVE_DEV_CB_LIST_LOWER_ALPHA": "Lower Alpha", + "LIVE_DEV_CB_LIST_UPPER_ALPHA": "Upper Alpha", + "LIVE_DEV_CB_LIST_LOWER_ROMAN": "Lower Roman", + "LIVE_DEV_CB_LIST_UPPER_ROMAN": "Upper Roman", + "LIVE_DEV_CB_LIST_OUTSIDE": "Outside", + "LIVE_DEV_CB_LIST_INSIDE": "Inside", + "LIVE_DEV_CB_TIP_SPACING": "Margin & Padding", + "LIVE_DEV_CB_LABEL_MARGIN": "Margin", + "LIVE_DEV_CB_LABEL_PADDING": "Padding", + "LIVE_DEV_CB_LABEL_CONTENT": "content", + "LIVE_DEV_CB_SPACING_LINK_INDEPENDENT": "Separate", + "LIVE_DEV_CB_SPACING_LINK_AXIS": "Pairs", + "LIVE_DEV_CB_SPACING_LINK_ALL": "All equal", + "LIVE_DEV_CB_LABEL_BORDER_TOP": "Top", + "LIVE_DEV_CB_LABEL_BORDER_RIGHT": "Right", + "LIVE_DEV_CB_LABEL_BORDER_BOTTOM": "Bottom", + "LIVE_DEV_CB_LABEL_BORDER_LEFT": "Left", + "LIVE_DEV_CB_LABEL_BORDER_TL": "TL", + "LIVE_DEV_CB_LABEL_BORDER_TR": "TR", + "LIVE_DEV_CB_LABEL_BORDER_BR": "BR", + "LIVE_DEV_CB_LABEL_BORDER_BL": "BL", + "LIVE_DEV_CB_PER_SIDE": "Per side", + "LIVE_DEV_CB_PER_CORNER": "Per corner", + "LIVE_DEV_CB_COLOR_RECENT": "Recent", + "LIVE_DEV_CB_COLOR_FROM_PAGE": "From page", + "LIVE_DEV_CB_COLOR_SUGGESTED": "Suggested", + "LIVE_DEV_CB_LABEL_COLUMNS": "Columns", + "LIVE_DEV_CB_LABEL_ROWS": "Rows", + "LIVE_DEV_CB_LABEL_JUSTIFY_ITEMS": "Justify", + "LIVE_DEV_CB_LABEL_ALIGN_ITEMS": "Align", + "LIVE_DEV_CB_LABEL_ROW_GAP": "Row Gap", + "LIVE_DEV_CB_LABEL_COL_GAP": "Col Gap", + "LIVE_DEV_CB_WEIGHT_EXTRA_LIGHT": "Extra Light", + "LIVE_DEV_CB_WEIGHT_LIGHT": "Light", + "LIVE_DEV_CB_WEIGHT_MEDIUM": "Medium", + "LIVE_DEV_CB_WEIGHT_SEMI_BOLD": "Semi Bold", + "LIVE_DEV_CB_WEIGHT_EXTRA_BOLD": "Extra Bold", + "LIVE_DEV_CB_GO_TO_SOURCE": "Go to source", + "LIVE_DEV_CB_TIP_AI_STYLE": "Style with AI", + "LIVE_DEV_CB_AI_PLACEHOLDER": "Describe the style you want\u2026", + "LIVE_DEV_CB_AI_SEND": "Send", + "LIVE_DEV_CB_AI_THINKING": "Applying styles\u2026", + "LIVE_DEV_CB_MORE_OPTIONS": "More options", + "LIVE_DEV_CB_DIM_W": "W", + "LIVE_DEV_CB_DIM_H": "H", + "LIVE_DEV_CB_MINMAX_TOOLTIP": "Min/Max constraints", + "LIVE_DEV_CB_MINMAX_MIN": "Min", + "LIVE_DEV_CB_MINMAX_MAX": "Max", + "LIVE_DEV_CB_ID_PLACEHOLDER": "none", + "LIVE_DEV_CB_ALL_WEIGHTS": "All weights", + "LIVE_DEV_CB_ALL_STYLES": "All styles", "LIVE_DEV_TOAST_NOT_EDITABLE": "Element not editable - generated by script", "LIVE_DEV_COPY_TOAST_MESSAGE": "Element copied. Use 'Paste' to add it after the selected element", "LIVE_DEV_TOAST_FIXED_ELEMENT_DISMISSED": "Element doesn't scroll with page - edit boxes hidden", @@ -247,7 +398,6 @@ define({ "IMAGE_SEARCH_LIMIT_MESSAGE_THROTTLE": "Image search is temporarily unavailable due to high demand.
Start a paid Phoenix Pro plan to remove trial limits and continue searching.", "IMAGE_SEARCH_PRO_THROTTLE_TITLE": "Image search limit reached", "IMAGE_SEARCH_PRO_THROTTLE_MESSAGE": "Image search is temporarily unavailable due to high demand. This usually clears within an hour — please try again shortly.", - "LIVE_DEV_AI_PROMPT_PLACEHOLDER": "Ask Phoenix AI to modify this element...", "LIVE_PREVIEW_CUSTOM_SERVER_BANNER": "Getting preview from your custom server {0}", "LIVE_PREVIEW_MODE_TOGGLE_PREVIEW": "Toggle Preview Mode (F8)", "LIVE_PREVIEW_MODE_PREVIEW": "Preview Mode", @@ -255,6 +405,10 @@ define({ "LIVE_PREVIEW_MODE_EDIT": "Edit Mode", "LIVE_PREVIEW_EDIT_HIGHLIGHT_ON": "Inspect Element on Hover", "LIVE_PREVIEW_SHOW_RULER_LINES": "Show Measurements", + "LIVE_PREVIEW_SHOW_SPACING_HANDLES": "Show Spacing Handles", + "LIVE_DEV_SETTINGS_SHOW_SPACING_HANDLES_PREFERENCE": "Show spacing handles when elements are selected in live preview edit mode. Defaults to 'true'", + "LIVE_PREVIEW_SYNC_SOURCE_AND_PREVIEW": "Sync Code & Preview", + "LIVE_DEV_SETTINGS_SYNC_SOURCE_AND_PREVIEW_PREFERENCE": "Sync source code cursor with live preview element highlighting. When enabled, moving the cursor in the editor highlights the corresponding element in the live preview, and clicking an element in the live preview jumps the cursor to its source code. Defaults to 'true'", "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",