diff --git a/src/LiveDevelopment/BrowserScripts/LivePreviewTransportRemote.js b/src/LiveDevelopment/BrowserScripts/LivePreviewTransportRemote.js index a4812f3cd1..6bb986b0b4 100644 --- a/src/LiveDevelopment/BrowserScripts/LivePreviewTransportRemote.js +++ b/src/LiveDevelopment/BrowserScripts/LivePreviewTransportRemote.js @@ -526,7 +526,8 @@ window.parent.postMessage({ handlerName: "ph-liveServer", eventName: 'whoAmIframePhoenix', - href: location.href + href: location.href, + clientID: clientID }, "*"); } }(this)); diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 96c5929bfb..f8834985d8 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -127,9 +127,8 @@ function RemoteFunctions(config = {}) { * inspectable elements are those which doesn't have GLOBALS.DATA_BRACKETS_ID_ATTR ('data-brackets-id'), * this normally happens when content is DOM content is inserted by some scripting language * - * 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. + * The `phcode-no-lp-edit` opt-out is not part of this check: it only keeps the + * pointer off an element, see isPointerEditOptedOut. * * @param {DOMElement} element * @param {boolean} [onlyHighlight=false] - If true, bypasses the mode check @@ -144,8 +143,7 @@ function RemoteFunctions(config = {}) { element.tagName.toLowerCase() !== "html" && // shouldn't be the HTML tag // 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 - !_isEditOptedOut(element)) { + !_isInsideHeadTag(element)) { // shouldn't be inside the head tag like meta tags and all return true; } return false; @@ -157,21 +155,24 @@ function RemoteFunctions(config = {}) { return !!(element && element.tagName && element.tagName.toLowerCase() === "body"); } - // a named selection lifts the `phcode-no-lp-edit` opt-out and the body block, both pointer-only guards + // a named selection lifts the body block, a pointer-only guard function _isNamedSelection(element) { return !!element && element === _namedSelection; } /** + * Whether the page keeps pointer actions on this element to itself: hover, + * click, double click and drops in the live preview leave it alone, so its + * own widgets keep working. It is still an element like any other to every + * path that is not the pointer - the layers panel, the caret, the tools. * `phcode-no-lp-edit` cascades to descendants, `phcode-no-lp-edit-this` covers * the one element. + * @param {DOMElement} element + * @returns {boolean} */ - function _isEditOptedOut(element) { - if (_isNamedSelection(element)) { - return false; - } - return !!(element.closest('.phcode-no-lp-edit') || - (element.classList && element.classList.contains('phcode-no-lp-edit-this'))); + function isPointerEditOptedOut(element) { + return !!(element && element.closest && (element.closest('.phcode-no-lp-edit') || + (element.classList && element.classList.contains('phcode-no-lp-edit-this')))); } /** @@ -250,6 +251,7 @@ function RemoteFunctions(config = {}) { getAllToolHandlers: getAllToolHandlers, isElementEditable: isElementEditable, isElementInspectable: isElementInspectable, + isPointerEditOptedOut: isPointerEditOptedOut, isBodyElement: isBodyElement, isSourceless: isSourceless, getElementRef: getElementRef, @@ -751,7 +753,7 @@ function RemoteFunctions(config = {}) { return; } if(isBodyElement(element) || !LivePreviewView.isElementInspectable(element) || - element.nodeType !== Node.ELEMENT_NODE) { + isPointerEditOptedOut(element) || element.nodeType !== Node.ELEMENT_NODE) { return; } _lastHoverTarget = element; @@ -830,7 +832,7 @@ function RemoteFunctions(config = {}) { } dismissUIAndCleanupState(); - // set after the dismissal, which clears the previous selection's exemption + // set after the dismissal, which clears the previous selection's body exemption _namedSelection = byName ? element : null; // this should also be there when users are in highlight mode scrollElementToViewPort(element); @@ -1008,8 +1010,7 @@ function RemoteFunctions(config = {}) { return; } // Opted-out elements: silent no-op so the user's existing selection isn't dismissed. - // (isElementInspectable would also reject them, but that path runs dismissUIAndCleanupState.) - if(element && (element.closest('.phcode-no-lp-edit') || element.classList.contains('phcode-no-lp-edit-this'))) { + if(isPointerEditOptedOut(element)) { return; } // a blank-space click lands on the body and deselects, even a body selected by name @@ -1037,8 +1038,10 @@ function RemoteFunctions(config = {}) { * the same way without a pointer gesture ever touching the page. * * @param {HTMLElement} element + * @param {boolean=} requested - the editor side asked for this selection, so it keeps its + * focus and reads the report as the echo of its own pick */ - function sendSelectionToEditor(element) { + function sendSelectionToEditor(element, requested) { if (config.syncSourceAndPreview === false) { return; } @@ -1052,7 +1055,8 @@ function RemoteFunctions(config = {}) { "nodeName": element.nodeName, "allSelectors": window.getAllInheritedSelectorsInOrder(element), "contentEditable": element.contentEditable === "true", - "clicked": true + "clicked": true, + "requested": !!requested }); } diff --git a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js index 85ff6e3920..f97446f072 100644 --- a/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js +++ b/src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js @@ -212,21 +212,9 @@ define(function (require, exports, module) { // 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() || _shouldKeepFocus()) { + // for the selection. The page flags the report of such a selection as requested. + function _focusEditorIfNeeded(editor, tagName, contentEditable, keepFocus) { + if (WorkspaceManager.isInDesignMode() || keepFocus) { return; } const focusShouldBeInLivePreview = ['INPUT', 'TEXTAREA'].includes(tagName) || contentEditable; @@ -274,7 +262,7 @@ define(function (require, exports, module) { } } - function _tagSelectedInLivePreview(tagId, nodeName, contentEditable, allSelectors) { + function _tagSelectedInLivePreview(tagId, nodeName, contentEditable, allSelectors, keepFocus) { const livePreviewMode = PreferencesManager.get(CONSTANTS.PREFERENCE_LIVE_PREVIEW_MODE); if(livePreviewMode === CONSTANTS.LIVE_PREVIEW_MODE){ // hilights are enabled only in edit and highlight mode @@ -287,7 +275,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() && !_shouldKeepFocus()) { + if (activeEditor && !WorkspaceManager.isInDesignMode() && !keepFocus) { activeEditor.focus(); // restore focus from live preview } return; @@ -306,7 +294,7 @@ define(function (require, exports, module) { const position = positionResult.from; const masterEditor = fullHtmlEditor.document._masterEditor || fullHtmlEditor; masterEditor.setCursorPos(position.line, position.ch, true); - _focusEditorIfNeeded(masterEditor, nodeName, contentEditable); + _focusEditorIfNeeded(masterEditor, nodeName, contentEditable, keepFocus); } } if(liveDocPath === activeFullEditorPath) { @@ -316,7 +304,7 @@ define(function (require, exports, module) { // the active editor takes the priority in the workflow. If a css related file is active, // then we dont need to open the html live doc. For less files, we dont check if its related as // its not directly linked usually and needs a compile step. so we just do a fuzzy search. - _focusEditorIfNeeded(activeEditor, nodeName, contentEditable); + _focusEditorIfNeeded(activeEditor, nodeName, contentEditable, keepFocus); _searchAndCursorIfCSS(activeEditor, allSelectors, nodeName); // in this case, see if we need to do any css reverse highlight magic here } else if(!allOpenFileCount){ @@ -402,12 +390,12 @@ define(function (require, exports, module) { const liveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); editMode && liveDoc && liveDoc.disableHighlightOnCursorActivity(true); try { - _tagSelectedInLivePreview(msg.tagId, msg.nodeName, msg.contentEditable, msg.allSelectors); + _tagSelectedInLivePreview(msg.tagId, msg.nodeName, msg.contentEditable, msg.allSelectors, + !!msg.requested); exports.trigger(EVENT_LIVE_PREVIEW_CLICKED, msg); } catch (e) { console.error("error in tag selection", e); } - _keepFocusUntil = 0; editMode && liveDoc && liveDoc.disableHighlightOnCursorActivity(false); // the caret did not move for a script-added element, re-highlighting would drop its selection liveDoc && !msg.sourceless && liveDoc.updateHighlight(); @@ -792,7 +780,6 @@ 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; diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js index 13f96a0b15..2697c75793 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/main.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js @@ -239,9 +239,12 @@ define(function (require, exports, module) { } } - StaticServer.on(EVENT_EMBEDDED_IFRAME_WHO_AM_I, function () { + StaticServer.on(EVENT_EMBEDDED_IFRAME_WHO_AM_I, function (_ev, event) { if($iframe && $iframe[0]) { const iframeDom = $iframe[0]; + if (event && event.source === iframeDom.contentWindow && event.data.clientID) { + _dockedClientID = event.data.clientID; + } iframeDom.contentWindow.postMessage({ type: "WHO_AM_I_RESPONSE", isTauri: Phoenix.isNativeApp, @@ -616,6 +619,7 @@ define(function (require, exports, module) { currentPreviewFile = '', _loadGeneration = 0, _isMdviewrActive = false, + _dockedClientID = null, $mdviewrIframe = null; // persistent md iframe, survives HTML preview switches /** @@ -631,6 +635,17 @@ define(function (require, exports, module) { exports.trigger(EVENT_PREVIEWED_FILE_CHANGE, fullPath); } + // A removed iframe never fires beforeunload, so its connection would linger until the tab heartbeat expires. + function _dropDockedConnection() { + if (!_dockedClientID) { + return; + } + const clientID = _dockedClientID; + _dockedClientID = null; + StaticServer.livePreviewTabs.delete(clientID); + StaticServer.trigger('BROWSER_CLOSE', { data: { message: {clientID}}}); + } + 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. @@ -656,6 +671,7 @@ define(function (require, exports, module) { } else { let newIframe = $(LIVE_PREVIEW_IFRAME_HTML); newIframe.insertAfter($iframe); + _dropDockedConnection(); $iframe.remove(); $iframe = newIframe; } @@ -717,7 +733,7 @@ define(function (require, exports, module) { // In design mode the LP panel fills the editor area — hiding it would // leave the user staring at a blank workspace. Keep the panel open and // just open the popout alongside it. - const closePanelAfterPopout = !WorkspaceManager.isInDesignMode(); + const closePanelAfterPopout = !WorkspaceManager.isInDesignMode() && panel.isVisible(); if(browserName && ALLOWED_BROWSERS_NAMES.includes(browserName)){ Metrics.countEvent(Metrics.EVENT_TYPE.LIVE_PREVIEW, "popout", browserName); NodeUtils.openUrlInBrowser(openURL, browserName) @@ -746,6 +762,33 @@ define(function (require, exports, module) { } } + function _isPopoutSupported() { + return Phoenix.isNativeApp || Phoenix.browser.desktop.isChromeBased || Phoenix.browser.desktop.isFirefox; + } + + /** + * Whether the previewed page can be opened in a browser: the platform allows it and a page has been previewed. + * @return {boolean} + */ + function canPopoutLivePreview() { + return _isPopoutSupported() && !!currentLivePreviewURL; + } + + /** + * Opens the previewed page in the default browser, starting the live preview session if it is off. + * @return {boolean} false when there is no page to open + */ + function popoutLivePreview() { + if (!canPopoutLivePreview()) { + return false; + } + if (LiveDevelopment.isInactive()) { + LiveDevelopment.openLivePreview(); + } + _popoutLivePreview(); + return true; + } + function _setTitle(fileName, fullPath, currentLivePreviewURL) { let message = Strings.LIVE_DEV_SELECT_FILE_TO_PREVIEW, tooltip = message; @@ -909,9 +952,7 @@ define(function (require, exports, module) { Metrics.countEvent(Metrics.EVENT_TYPE.LIVE_PREVIEW, "settingsBtn", "click"); }); - const popoutSupported = Phoenix.isNativeApp - || Phoenix.browser.desktop.isChromeBased || Phoenix.browser.desktop.isFirefox; - if(!popoutSupported){ + if(!_isPopoutSupported()){ // live preview can be popped out currently in only chrome based browsers. The cross domain iframe // that serves the live preview(phcode.live) is sandboxed to the tab in which phcode.dev resides. // all iframes in the tab can communicate between each other, but when you popout another tab, it forms @@ -1790,6 +1831,8 @@ define(function (require, exports, module) { exports.showInterstitial = showInterstitial; exports.hideInterstitial = hideInterstitial; exports.getPreviewedFilePath = getPreviewedFilePath; + exports.canPopoutLivePreview = canPopoutLivePreview; + exports.popoutLivePreview = popoutLivePreview; }); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index c88cd4cf02..0eead6303b 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -457,7 +457,15 @@ define({ "LIVE_DEV_INSERT_NO_RESULTS": "No matching elements", "LIVE_DEV_INSERT_SHOW_MORE": "Show more", "LIVE_DEV_INSERT_SHOW_LESS": "Show less", - "LIVE_DEV_INSERT_CREATE": "Create", + "LIVE_DEV_INSERT_CREATE": "Create {0}", + "LIVE_DEV_INSERT_NAME_ACCORDION": "Accordion", + "LIVE_DEV_INSERT_NAME_PASSWORD": "Password", + "LIVE_DEV_INSERT_NAME_MAIN_CONTENT": "Main Content", + "LIVE_DEV_INSERT_NAME_CODE_BLOCK": "Code Block", + "LIVE_DEV_INSERT_NAME_DEF_LIST": "Def. List", + "LIVE_DEV_INSERT_NAME_FILE_UPLOAD": "File Upload", + "LIVE_DEV_INSERT_NAME_DATE_PICKER": "Date Picker", + "LIVE_DEV_INSERT_NAME_IFRAME": "iFrame", "LIVE_DEV_STARTER_BAR_LABEL": "Your page is empty — add an element", "LIVE_DEV_IMAGE_GALLERY_USE_IMAGE": "Download image", "LIVE_DEV_IMAGE_GALLERY_SELECT_DOWNLOAD_FOLDER": "Choose image download folder", @@ -788,7 +796,12 @@ define({ "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_OPEN_IN_BROWSER": "Open in Browser", "LIVE_PREVIEW_LAYERS_NEEDS_HTML": "Open an HTML file to see its layers.", + "LIVE_PREVIEW_LAYERS_PICK_HTML": "Pick an HTML file to see its layers.", + "LIVE_PREVIEW_LAYERS_FILTER_FILES": "Filter files", + "LIVE_PREVIEW_LAYERS_NO_FILE_MATCHES": "No matching files", + "LIVE_PREVIEW_LAYERS_FILES_TRUNCATED": "Showing the first {0} files", "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", @@ -806,8 +819,7 @@ define({ "LIVE_PREVIEW_LAYERS_SCROLL_ON_HOVER": "Scroll to Element on Hover", "LIVE_PREVIEW_LAYERS_MOVE_UP": "Move Up", "LIVE_PREVIEW_LAYERS_MOVE_DOWN": "Move Down", - "LIVE_PREVIEW_LAYERS_COPIED_HINT": "Copied. Right-click a row to paste below it.", - "LIVE_PREVIEW_LAYERS_CUT_HINT": "Cut. Right-click a row to paste below it.", + "LIVE_PREVIEW_LAYERS_CUT_DONE": "Cut", "LIVE_PREVIEW_LAYERS_ADD_ATTRIBUTE": "Add Attribute", "LIVE_PREVIEW_LAYERS_REMOVE_ATTRIBUTE": "Remove Attribute", "LIVE_PREVIEW_LAYERS_PROPERTIES": "Properties", @@ -820,15 +832,21 @@ define({ "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_OVERRIDDEN_IMPORTANT": "Overridden by {0} in {1} ({2})", + "LIVE_PREVIEW_LAYERS_OVERRIDDEN_IN_RULE": "Overridden by the later {0} declaration in this rule", + "LIVE_PREVIEW_LAYERS_OVERRIDDEN_INLINE": "Overridden by the element's inline style ({0})", + "LIVE_PREVIEW_LAYERS_OVERRIDDEN_SPECIFIC": "Overridden by the more specific rule {0} ({1})", + "LIVE_PREVIEW_LAYERS_OVERRIDDEN_LATER": "Overridden by {0} ({1}), which has the same specificity but comes later", + "LIVE_PREVIEW_LAYERS_OVERRIDDEN_JUMP": "Click to go to the winning declaration", "LIVE_PREVIEW_LAYERS_EDIT_STYLE": "Edit in Styles Bar", + "LIVE_PREVIEW_LAYERS_STYLES_BAR_POPPED_OUT": "Styles bar opened in your browser preview", "LIVE_PREVIEW_LAYERS_ADD_DECLARATION": "Add Declaration", "LIVE_PREVIEW_LAYERS_REMOVE_DECLARATION": "Remove Declaration", "LIVE_PREVIEW_LAYERS_EDIT_DECLARATION": "Edit Declaration", "LIVE_PREVIEW_LAYERS_SHOW_SCRIPT_NODES": "Show Script-Generated Elements", "LIVE_PREVIEW_LAYERS_SCRIPT_GENERATED": "Generated by script, not in the source file", - "LIVE_PREVIEW_LAYERS_TAG_TOOLTIP": "Show Tag Name Tooltip", "LIVE_PREVIEW_LAYERS_INSERT_ELEMENT": "Insert Element", - "LIVE_PREVIEW_LAYERS_CHANGE_COLOR": "Change color", + "LIVE_PREVIEW_LAYERS_CHANGE_COLOR": "Change color: {0}", "LIVE_PREVIEW_LAYERS_COPY_DECLARATION": "Copy property", "LIVE_PREVIEW_LAYERS_COPY_RULE": "Copy rule", "LIVE_PREVIEW_LAYERS_PASTE_PROPERTIES": "Paste properties", @@ -842,6 +860,9 @@ define({ "LIVE_PREVIEW_LAYERS_INSERT_INTO_PAGE": "Add to the page", "LIVE_PREVIEW_LAYERS_INSERT_LOADING": "Loading elements…", "LIVE_PREVIEW_LAYERS_INSERT_UNAVAILABLE": "The live preview did not answer. Try again in a moment.", + "LIVE_PREVIEW_LAYERS_LOADING": "Loading elements…", + "LIVE_PREVIEW_LAYERS_UNAVAILABLE": "The live preview did not answer.", + "LIVE_PREVIEW_LAYERS_RETRY": "Try again", "LIVE_PREVIEW_LAYERS_EMPTY_PAGE": "This page has no elements yet.", "LIVE_PREVIEW_LAYERS_ADD_ELEMENT": "Add an element", "LIVE_PREVIEW_LAYERS_EMPTY_PAGE_PROPERTIES": "The page is empty. Add an element to see its properties.", diff --git a/src/styles/Extn-LayersPanel.less b/src/styles/Extn-LayersPanel.less index 473cf2e205..e1d4dad204 100644 --- a/src/styles/Extn-LayersPanel.less +++ b/src/styles/Extn-LayersPanel.less @@ -25,6 +25,7 @@ @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-border-strong: rgba(255, 255, 255, 0.14); @layers-input-bg: rgba(255, 255, 255, 0.04); @layers-input-bg-focus: rgba(255, 255, 255, 0.06); @layers-font-size: 13px; @@ -146,26 +147,21 @@ white-space: normal; overflow-wrap: break-word; word-break: break-word; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } .layers-message-icon { - display: flex; - align-items: center; - justify-content: center; flex: none; - width: 44px; - height: 44px; margin-bottom: 14px; - border: 1px solid @layers-border; - border-radius: 50%; - background: @layers-accent-soft; - color: @layers-accent; - font-size: 17px; + color: @project-panel-text-2; + font-size: 24px; + line-height: 1; } .layers-message-title { max-width: 100%; - margin-bottom: 5px; + margin-bottom: 6px; font-size: @layers-font-tree; font-weight: 600; line-height: 1.35; @@ -173,43 +169,184 @@ } .layers-message-text { - max-width: 240px; + max-width: 230px; color: @project-panel-text-2; - line-height: 1.5; + line-height: 1.55; + } + + .layers-message-actions { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + max-width: 220px; + margin-top: 20px; + + .layers-message-btn { + width: 100%; + margin-top: 0; + } } .layers-message-btn { box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; max-width: 100%; + min-height: 36px; margin-top: 16px; - padding: 7px 16px; - border: 1px solid @layers-accent; - border-radius: 6px; - background: @layers-accent-soft; - color: @project-panel-text-1; + padding: 9px 16px; + border: 1px solid transparent; + border-radius: 7px; + background: #ececec; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); + color: #1e1e1e; font-family: inherit; font-size: @layers-font-size; - font-weight: 500; + font-weight: 400; line-height: 1.35; white-space: normal; cursor: pointer; outline: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-stroke: 0.25px currentColor; transition: background-color 120ms ease, border-color 120ms ease; &:hover { - background: @layers-accent-strong; + background: #ffffff; + } + + &:active { + background: #dcdcdc; + } + + &:focus-visible { + box-shadow: 0 0 0 2px fade(@layers-accent, 55%); + } + } + + .layers-message-btn-secondary { + background: transparent; + border-color: @layers-border-strong; + box-shadow: none; + color: @project-panel-text-1; + + i { + font-size: 11px; + color: @project-panel-text-2; + } + + &:hover { + background: @layers-hover-bg; + border-color: rgba(255, 255, 255, 0.22); } &:active { - background: @layers-accent-strong; - transform: translateY(1px); + background: @layers-input-bg; + } + } + + .layers-message.layers-message-files { + justify-content: flex-start; + padding: 32px 8px 8px; + + .layers-message-text { + margin-bottom: 14px; + } + } + + .layers-files { + display: flex; + flex-direction: column; + flex: 0 1 auto; + width: 100%; + min-height: 0; + text-align: left; + + .layers-search { + padding: 0 0 6px; + } + } + + .layers-files-list { + flex: 0 1 auto; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; + } + + .layers-file { + box-sizing: border-box; + display: flex; + align-items: center; + gap: 8px; + width: 100%; + height: @layers-row-height; + padding: 0 8px; + margin: 0; + border: none; + border-radius: 5px; + background: transparent; + color: @project-panel-text-1; + font-family: inherit; + font-size: @layers-font-size; + text-align: left; + white-space: nowrap; + cursor: pointer; + outline: none; + + &:hover { + background: @layers-row-hover-bg; } &:focus-visible { - box-shadow: 0 0 0 2px @layers-accent-soft; + background: @layers-row-hover-bg; + box-shadow: inset 0 0 0 1px @layers-accent; } } + .layers-file-icon { + flex: none; + width: 14px; + font-size: 13px; + text-align: center; + color: @project-panel-text-2; + } + + .layers-file-icon-html { + color: #e44d26; + } + + .layers-file-icon-php { + color: #8892bf; + } + + .layers-file-name { + flex: 0 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + } + + .layers-file-dir { + flex: 0 1000 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + color: @project-panel-text-2; + font-size: @layers-font-xs; + } + + .layers-files-note { + padding: 8px; + color: @project-panel-text-2; + font-size: @layers-font-xs; + text-align: center; + } + .layers-panel { display: flex; flex-direction: column; @@ -347,10 +484,6 @@ .layers-section.layers-collapsed .layers-section-content, .layers-section.layers-collapsed .layers-search, - .layers-section.layers-collapsed .layers-notice { - display: none; - } - .layers-divider { position: relative; flex: 0 0 5px; @@ -501,6 +634,7 @@ height: @layers-row-height; content-visibility: auto; contain-intrinsic-size: auto @layers-row-intrinsic-width auto @layers-row-height; + margin-right: 2px; padding-right: 6px; box-sizing: border-box; white-space: nowrap; @@ -555,47 +689,6 @@ } } - .layers-notice { - display: flex; - align-items: center; - gap: 7px; - flex-shrink: 0; - box-sizing: border-box; - margin: 0 8px 8px; - padding: 5px 8px 5px 5px; - border: 1px solid @layers-border; - border-radius: 6px; - background: @layers-input-bg; - color: @project-panel-text-1; - font-size: @layers-font-xs; - line-height: 1.4; - white-space: normal; - animation: layers-notice-in 180ms ease-out; - - .layers-notice-icon { - display: flex; - align-items: center; - justify-content: center; - flex: none; - width: 22px; - height: 22px; - border-radius: 4px; - background: @layers-accent-soft; - color: @layers-accent; - - svg { - width: 13px; - height: 13px; - display: block; - } - } - - .layers-notice-text { - flex: 1; - min-width: 0; - } - } - .layers-twisty { display: flex; align-items: center; @@ -747,6 +840,12 @@ background: @layers-row-holder-bg; } + .layers-node.layers-caret-target .layers-node-tools { + box-shadow: inset -1px 0 0 fade(@layers-accent, 55%), + inset 0 1px 0 fade(@layers-accent, 55%), + inset 0 -1px 0 fade(@layers-accent, 55%); + } + .layers-holder-chip { position: absolute; left: 0; @@ -839,6 +938,22 @@ } } + .layers-loading-page { + animation: layers-loading-in 150ms ease-out 300ms both; + } + + .layers-loading-spinner { + flex: none; + width: 18px; + height: 18px; + margin-bottom: 6px; + box-sizing: border-box; + border: 2px solid @layers-border-strong; + border-top-color: @layers-accent; + border-radius: 50%; + animation: layers-spin 800ms linear infinite; + } + .layers-decl-value.layers-decl-numeric { cursor: ew-resize; } @@ -1028,12 +1143,16 @@ .layers-decl-color { position: relative; flex: none; - width: 13px; - height: 13px; + box-sizing: border-box; + width: 14px; + height: 14px; + margin: 0; padding: 0; - border: 1px solid rgba(255, 255, 255, 0.25); - border-radius: 2px; - background: repeating-conic-gradient(#999 0% 25%, #fff 0% 50%) 0 0 / 6px 6px; + border: none; + border-radius: 0; + outline: none; + box-shadow: none; + background: repeating-conic-gradient(#999 0% 25%, #fff 0% 50%) 0 0 / 7px 7px; cursor: pointer; &::after { @@ -1042,11 +1161,6 @@ inset: 0; background: var(--layers-color); } - - &:focus-visible { - outline: 1px solid @layers-accent; - outline-offset: 2px; - } } input.layers-decl-input[type="text"] { @@ -1158,6 +1272,65 @@ } } + .layers-why { + display: inline-flex; + align-items: center; + justify-content: center; + flex: none; + width: 16px; + height: 16px; + margin-left: 4px; + padding: 0; + border: none; + border-radius: 50%; + background: none; + color: @project-panel-text-2; + cursor: pointer; + outline: none; + transform: translateY(1px); + + svg { + display: block; + width: 12px; + height: 12px; + } + + &:hover, + &:focus-visible { + color: @layers-accent; + } + } + + .layers-rule-header .layers-why { + margin-left: 0; + } + + .layers-why-tip { + position: fixed; + z-index: 10; + box-sizing: border-box; + max-width: 280px; + padding: 5px 8px; + border: 1px solid fade(@layers-accent, 55%); + border-radius: 6px; + background: #2a2a2a; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35); + color: @project-panel-text-1; + font-size: @layers-font-size; + line-height: 18px; + white-space: normal; + pointer-events: none; + + .layers-why-hint { + margin-top: 2px; + color: @project-panel-text-2; + } + } + + .layers-decl.layers-decl-winner { + animation: layers-flash-row 800ms ease-out; + } + .layers-icon-btn { .layers-icon-button(); align-self: center; @@ -1270,21 +1443,6 @@ } } -.layers-tag-tip { - position: fixed; - z-index: 1000; - padding: 2px 6px; - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 4px; - background: #2f2f2f; - color: @project-panel-text-1; - font-family: @layers-mono; - font-size: 12px; - line-height: 16px; - white-space: nowrap; - pointer-events: none; -} - .layers-node-text { color: @project-panel-text-2; font-style: italic; @@ -1314,15 +1472,15 @@ body.layers-scrubbing { display: flex; flex-direction: column; box-sizing: border-box; - border: 1px solid @layers-border; - border-radius: 3px; + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 6px; background: @bc-ai-sidebar-bg; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(0, 0, 0, 0.45); color: @project-panel-text-1; color-scheme: dark; font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - font-size: 12.5px; - line-height: 1.3; + font-size: 13px; + line-height: 1.35; letter-spacing: normal; text-shadow: none; animation: layers-notice-in 90ms ease-out; @@ -1344,29 +1502,31 @@ body.layers-scrubbing { align-items: center; justify-content: space-between; gap: 8px; - min-height: 30px; - padding: 0 8px; + min-height: 38px; + padding: 0 10px; + border-bottom: 1px solid @layers-border; + border-radius: 5px 5px 0 0; background: @layers-input-bg; flex: none; } .layers-icon-btn { .layers-icon-button(); - width: 22px; - height: 22px; + width: 24px; + height: 24px; } .layers-insert-target { display: flex; align-items: center; - gap: 4px; + gap: 6px; flex: 0 1 auto; min-width: 0; } .layers-insert-target-name, .layers-insert-where { - font-size: 11.5px; + font-size: 12.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; @@ -1384,52 +1544,90 @@ body.layers-scrubbing { } .layers-insert-position { - width: auto; - max-width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; box-sizing: border-box; - height: 24px; + min-width: 84px; + max-width: 100%; + height: 26px; margin: 0; - padding: 2px 24px 2px 7px; - border: 1px solid @layers-border; - border-radius: 3px; - box-shadow: none; + padding: 0 6px 0 9px; + border: 1px solid @layers-border-strong; + border-radius: 4px; background: @layers-input-bg; color: @project-panel-text-1; - font: inherit; - font-size: 11.5px; - text-overflow: ellipsis; - appearance: none; + font-size: 12.5px; cursor: pointer; - &:hover { + &:hover, + &[aria-expanded="true"] { background: @layers-hover-bg; - border-color: fade(@layers-insert-accent, 40%); } + } - &:focus-visible { - outline: 1px solid @layers-insert-accent; - outline-offset: -1px; - } + .layers-insert-position-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } - option { - background: @bc-ai-sidebar-bg; - color: @project-panel-text-1; + .layers-insert-chevron { + display: inline-flex; + flex: none; + color: @project-panel-text-2; + + svg { + width: 14px; + height: 14px; } } - .layers-insert-chevron { + .layers-insert-position-list { position: absolute; - top: 0; - right: 6px; - bottom: 0; + top: calc(100% + 4px); + left: 0; + z-index: 1; + display: flex; + flex-direction: column; + gap: 1px; + box-sizing: border-box; + min-width: 100%; + padding: 4px; + border: 1px solid @layers-border-strong; + border-radius: 4px; + background: @layers-row-holder-bg; + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45); + } + + .layers-insert-option { display: flex; align-items: center; - color: @project-panel-text-2; - pointer-events: none; + height: 28px; + padding: 0 8px; + border: none; + border-radius: 3px; + background: transparent; + color: @project-panel-text-1; + font-size: 12.5px; + white-space: nowrap; + cursor: pointer; - svg { - width: 12px; - height: 12px; + &:hover, + &:focus-visible { + background: @layers-hover-bg; + outline: none; + } + + &.layers-insert-option-active { + background: @layers-accent-soft; + } + + &:disabled { + opacity: 0.4; + background: transparent; + cursor: default; } } @@ -1438,12 +1636,12 @@ body.layers-scrubbing { display: flex; align-items: center; position: relative; - margin-bottom: 6px; + margin-bottom: 10px; } .layers-insert-search-icon { position: absolute; - left: 7px; + left: 9px; display: flex; align-items: center; color: @project-panel-text-2; @@ -1451,8 +1649,8 @@ body.layers-scrubbing { } .layers-insert-search-icon > svg { - width: 13px; - height: 13px; + width: 14px; + height: 14px; } .layers-insert-search input[type="text"] { @@ -1465,8 +1663,8 @@ body.layers-scrubbing { border: 1px solid @layers-border; border-radius: 3px; color: @project-panel-text-1; - font-size: 12px; - padding: 4px 8px 4px 26px; + font-size: 13px; + padding: 7px 10px 7px 30px; outline: none; font-family: inherit; box-sizing: border-box; @@ -1493,7 +1691,7 @@ body.layers-scrubbing { flex-direction: column; flex: 1; min-height: 0; - padding: 8px; + padding: 12px; } .layers-insert-list { @@ -1501,6 +1699,8 @@ body.layers-scrubbing { overflow-y: scroll; overflow-x: hidden; min-height: 0; + margin-right: -12px; + padding-right: 6px; scrollbar-width: thin; scrollbar-color: @layers-insert-scrollbar @bc-ai-sidebar-bg; scrollbar-gutter: stable; @@ -1543,16 +1743,16 @@ body.layers-scrubbing { .layers-insert-grid { display: grid; grid-template-columns: 1fr 1fr; - gap: 8px 12px; + gap: 8px; } .layers-insert-tile { display: flex; align-items: center; - gap: 7px; - padding: 6px; + gap: 8px; + padding: 8px 10px; border: 1px solid @layers-border; - border-radius: 3px; + border-radius: 4px; cursor: pointer; min-width: 0; background: transparent; @@ -1591,7 +1791,7 @@ body.layers-scrubbing { .layers-insert-item-text-inner { display: inline-flex; align-items: baseline; - gap: 4px; + gap: 6px; white-space: nowrap; width: 100%; justify-content: space-between; @@ -1603,19 +1803,19 @@ body.layers-scrubbing { @keyframes layers-insert-text-scroll { 0%, 15% { transform: translateX(0); } - 50% { transform: translateX(min(0px, calc(-100% + 140px))); } + 50% { transform: translateX(min(0px, calc(-100% + 150px))); } 85%, 100% { transform: translateX(0); } } .layers-insert-item-name { - font-size: 11.5px; + font-size: 12.5px; color: @project-panel-text-1; white-space: nowrap; line-height: 1.3; } .layers-insert-item-tag { - font-size: 9.5px; + font-size: 11px; color: @layers-insert-accent; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; line-height: 1.3; @@ -1624,11 +1824,11 @@ body.layers-scrubbing { .layers-insert-create { display: flex; align-items: center; - gap: 6px; - padding: 5px 8px 5px 26px; + gap: 8px; + padding: 7px 10px 7px 30px; cursor: pointer; border-radius: 0 0 3px 3px; - margin: 0 0 6px; + margin: 0 0 10px; border: 1px solid @layers-border; border-top: none; background: transparent; @@ -1640,7 +1840,7 @@ body.layers-scrubbing { } .layers-insert-empty { - font-size: 11px; + font-size: 12px; color: @project-panel-text-2; font-style: italic; padding: 12px 4px; @@ -1662,7 +1862,7 @@ body.layers-scrubbing { } .layers-insert-item-name { - font-size: 12px; + font-size: 13px; color: @project-panel-text-2; span { @@ -1917,7 +2117,31 @@ body.layers-dnd-invalid { } } +@keyframes layers-loading-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes layers-spin { + to { + transform: rotate(360deg); + } +} + @media (prefers-reduced-motion: reduce) { + .layers-tab-container .layers-loading-page { + animation-duration: 1ms; + } + + .layers-tab-container .layers-loading-spinner { + animation-duration: 1600ms; + } + .layers-tab-container .layers-node.layers-flash { animation-duration: 1ms; background-color: fade(@layers-accent, 32%); @@ -1932,8 +2156,7 @@ body.layers-dnd-invalid { } .layers-tab-container .layers-node.layers-removing, - .layers-tab-container .layers-node.layers-clone-in, - .layers-tab-container .layers-notice { + .layers-tab-container .layers-node.layers-clone-in { animation-duration: 1ms; } } @@ -1942,96 +2165,342 @@ body.layers-dnd-invalid { position: fixed; z-index: 1000; box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 10px; padding: 12px; overflow: auto; border: 1px solid @layers-border; border-radius: 10px; background: @bc-ai-sidebar-bg; color: @project-panel-text-1; + font-size: @layers-font-xs; box-shadow: 0 8px 28px rgba(0, 0, 0, 0.3); + outline: none; .layers-color-head { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 12px; + gap: 10px; + margin-top: -4px; + padding-bottom: 6px; + border-bottom: 1px solid @layers-border-strong; + } + + .layers-color-title { + flex: 1; + overflow: hidden; font-size: @sidebar-small-font-size; font-weight: 500; + color: @project-panel-text-2; + text-overflow: ellipsis; + white-space: nowrap; + } + + .layers-color-actions { + display: flex; + align-items: center; + gap: 4px; + flex: none; } .layers-icon-btn { .layers-icon-button(); + width: 24px; + height: 24px; + border-radius: 6px; + + svg { + width: 15px; + height: 15px; + } + + &.layers-on, + &.layers-on:hover { + background: @layers-accent; + color: #0b0b0b; + } + } + + .layers-cp-main { + display: flex; + align-items: stretch; + gap: 10px; + } + + .layers-cp-right { + display: flex; + flex-direction: column; + gap: 10px; + flex: 1; + min-width: 0; + } + + .layers-cp-checker { + background-image: repeating-conic-gradient(#666666 0% 25%, #bbbbbb 0% 50%); + background-size: 8px 8px; + } + + .layers-cp-sv { + position: relative; + height: 168px; + border-radius: 6px; + cursor: crosshair; + touch-action: none; + } + + .layers-cp-sv-white, + .layers-cp-sv-black { + position: absolute; + inset: 0; + border-radius: 6px; + } + + .layers-cp-sv-white { + background: linear-gradient(to right, #ffffff, transparent); + } + + .layers-cp-sv-black { + background: linear-gradient(to top, #000000, transparent); + } + + .layers-cp-sv-thumb, + .layers-cp-thumb { + position: absolute; + box-sizing: border-box; + width: 14px; + height: 14px; + border: 2px solid #ffffff; + border-radius: 50%; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55); + transform: translate(-50%, -50%); + pointer-events: none; + } + + .layers-cp-thumb { + top: 50%; + background: transparent; + } + + .layers-cp-bar { + position: relative; + height: 12px; + border-radius: 6px; + cursor: pointer; + touch-action: none; + } + + .layers-cp-hue { + background: linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, + #0000ff 67%, #ff00ff 83%, #ff0000 100%); + } + + .layers-cp-alpha-grad { + position: absolute; + inset: 0; + border-radius: 6px; + pointer-events: none; + } + + .layers-cp-divider, + .layers-cp-swatch-divider { + height: 1px; + flex: none; + background: @layers-border-strong; + } + + .layers-cp-format-row { + display: flex; + align-items: center; + gap: 6px; + } + + .layers-cp-select { + display: flex; + align-items: center; + justify-content: space-between; + gap: 4px; + flex: none; + box-sizing: border-box; + width: 64px; + height: 28px; + margin: 0; + padding: 0 6px; + border: 1px solid @layers-border-strong; + border-radius: 6px; + background: @layers-input-bg; + color: @project-panel-text-1; + font: inherit; + font-size: @layers-font-xs; + cursor: pointer; + + &:hover { + background: @layers-hover-bg; + } + } + + .layers-cp-select-chevron { + display: inline-flex; + flex: none; + opacity: 0.8; + + svg { + width: 16px; + height: 16px; + } } - .color-editor { + .layers-cp-fields { + display: flex; + align-items: center; + gap: 4px; + flex: 1 1 auto; min-width: 0; + } + + input.layers-cp-chan[type="text"] { + flex: 1 1 0; + box-sizing: border-box; width: 100%; - height: auto; - padding: 0; + min-width: 0; + height: 28px; + margin: 0; + padding: 0 6px; + border: 1px solid @layers-border-strong; + border-radius: 6px; + background: @layers-input-bg; + box-shadow: none; + color: @project-panel-text-1; + font-family: inherit; + font-size: @layers-font-xs; + font-variant-numeric: tabular-nums; + text-align: center; + outline: none; - aside { - display: none; + &:focus { + border-color: @layers-accent; + background: @layers-input-bg-focus; + box-shadow: none; } - section { - display: block; - width: 100%; + &.layers-cp-chan-hex { + text-align: left; + text-transform: uppercase; + } + } - .sliders { - display: flex; - align-items: stretch; - } + .layers-cp-alpha-field { + position: relative; + flex: none; + width: 56px; - .color-selection-field { - flex: 1; - min-width: 0; - width: auto; - margin-right: 12px; - float: none; - } + input.layers-cp-chan[type="text"] { + padding-right: 18px; + text-align: left; + } + } - .slider { - flex: none; - margin-right: 12px; - float: none; - } + .layers-cp-alpha-pct { + position: absolute; + top: 0; + right: 8px; + bottom: 0; + display: flex; + align-items: center; + color: @project-panel-text-2; + font-size: @layers-font-xs; + line-height: 18px; + pointer-events: none; + } - .opacity-slider { - margin-right: 0; - } + .layers-cp-swatches { + display: flex; + flex-direction: column; + gap: 6px; + flex: none; + width: 68px; + } - footer { - display: flex; - align-items: center; - gap: 8px; - padding-top: 12px; - } + .layers-cp-swatch-label { + overflow: hidden; + color: @project-panel-text-2; + font-size: 10px; + font-weight: 500; + letter-spacing: 0.04em; + text-overflow: ellipsis; + text-transform: uppercase; + white-space: nowrap; + } - footer input.color-value { - flex: 1; - min-width: 0; - width: 0; - height: 26px; - margin: 0; - padding: 0 6px; - border-color: @layers-border; - background: @layers-input-bg; - color: @project-panel-text-1; - font-family: @layers-mono; - font-size: 11px; - - &:focus { - border-color: @layers-accent; - background: @layers-input-bg-focus; - box-shadow: none; - } - } + .layers-cp-swatch-grid { + display: grid; + grid-template-columns: repeat(3, 20px); + gap: 4px; + align-content: start; + } + + .layers-cp-swatch { + box-sizing: border-box; + width: 20px; + height: 20px; + margin: 0; + padding: 0; + border: 1px solid @layers-border-strong; + border-radius: 6px; + cursor: pointer; + + &:hover { + transform: scale(1.15); + border-color: @layers-accent; } - ul.button-bar { - flex: none; - margin-left: 0; - white-space: nowrap; + &.layers-cp-swatch-active { + border-color: @layers-accent; + box-shadow: 0 0 0 2px @layers-accent-strong; + } + } + + .layers-cp-select-list { + position: fixed; + z-index: 1; + display: flex; + flex-direction: column; + gap: 1px; + box-sizing: border-box; + width: 64px; + padding: 4px; + border: 1px solid @layers-border-strong; + border-radius: 6px; + background: @layers-row-holder-bg; + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45); + } + + .layers-cp-option { + display: flex; + align-items: center; + gap: 8px; + height: 29px; + margin: 0; + padding: 6px 8px; + border: none; + border-radius: 6px; + background: transparent; + color: @project-panel-text-1; + font: inherit; + text-align: left; + cursor: pointer; + + &:hover { + background: @layers-hover-bg; } + + &.layers-cp-option-active { + background: @layers-accent-soft; + } + } + + .layers-cp-option-label { + flex: 1; } }