diff --git a/gulpfile.js/validate-build.js b/gulpfile.js/validate-build.js index 1f7bad73ea..95417a7adf 100644 --- a/gulpfile.js/validate-build.js +++ b/gulpfile.js/validate-build.js @@ -29,7 +29,7 @@ const DEV_MAX_TOTAL_SIZE_MB = 100; // Custom size limits for known large files (size in MB) For development builds const LARGE_FILE_LIST_DEV = { 'dist/thirdparty/no-minify/language-worker.js.map': 10, - 'dist/brackets-min.js': 15 + 'dist/brackets-min.js': 20 }; // Size limits for production/staging builds (in MB) diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index 3380e2e778..05475298c8 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -663,15 +663,34 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, "controlEditor). Never use relative paths." + "\n\nWhen a tool response mentions the user has typed a clarification, immediately " + "call getUserClarification to read it and incorporate the user's feedback into your current work." + - "\n\nYou are running inside Phoenix Code, a web-focused code editor with a built-in " + - "live preview for HTML/CSS/JS. When the user asks to create mockups, prototypes, " + - "or web pages, prefer vanilla HTML/CSS/JS so the live preview can render and " + - "edit them — unless the user specifically requests a framework. " + - "Build responsive layouts by default for web content." + - "\n\nWhen planning, consider if verification is needed. takeScreenshot can " + - "capture the full editor, specific panels, the code area, or the live preview. " + - "For HTML/CSS/JS with live preview, execJsInLivePreview can run JS in the " + - "browser to confirm behavior." + + "\n\nYou are running inside Phoenix Code, a web-focused code editor with built-in " + + "live preview for both HTML/CSS/JS/SVG and Markdown. When the user asks to create " + + "mockups, prototypes, or web pages, prefer vanilla HTML/CSS/JS so the live preview " + + "can render and edit them — unless the user specifically requests a framework. " + + "Build responsive layouts by default for web content. For images, prefer real " + + " tags over div background-image so the user can swap, inspect, and resize " + + "them in the editor — only fall back to background-image when an effect (parallax, " + + "cover-with-overlay, repeating tile) genuinely requires it." + + "\n\nYou can debug and inspect the live preview directly — these tools are for " + + "active iteration, not just final verification:" + + "\n- takeScreenshot: see the rendered HTML preview, the rendered Markdown preview, " + + "the editor, or any panel. Use it to confirm visual output, diagnose layout/styling " + + "bugs, or check that HTML or Markdown rendered as expected. Pass reload=true to " + + "force-reload the preview before capturing (useful after JS edits) — saves a tool " + + "call vs. reloading separately." + + "\n- execJsInLivePreview: run JS inside the HTML preview iframe to read the DOM, " + + "query computed styles, click elements, or capture console output. Use it to debug " + + "behavior, not just to verify." + + "\n- resizeLivePreview: change the preview viewport width to test responsive " + + "breakpoints." + + "\n- controlEditor: open files, move the cursor, change selection, toggle the live " + + "preview panel, or reload it (reloadLivePreview operation — use after JS edits if " + + "you're not also taking a screenshot)." + + "\n- getEditorState: report active file, working set, cursor/selection, and the " + + "livePreviewFile. The live preview normally follows the active editor file, so " + + "assume that. Rarely the user pins the preview to a specific file — if a " + + "screenshot doesn't match the file you just edited, check " + + "getEditorState.livePreviewFile to rule that out." + "\n\nUse your best judgement for when to enter plan mode. Use it when the task " + "involves creating new applications, extensive modifications, or architectural " + "changes — propose a plan for user approval before writing code." + diff --git a/src-node/mcp-editor-tools.js b/src-node/mcp-editor-tools.js index 4896a598f1..269eb605ac 100644 --- a/src-node/mcp-editor-tools.js +++ b/src-node/mcp-editor-tools.js @@ -128,10 +128,13 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) "or '.editor-holder' to capture only the code editor area. " + "Only omit the selector when you need to see the full editor application layout. " + "Note: live preview screenshots may include Phoenix toolbox overlays on selected elements. " + - "Use purePreview=true to temporarily hide these overlays and render the page as it would appear in a real browser.", + "Use purePreview=true to temporarily hide these overlays and render the page as it would appear in a real browser. " + + "Use reload=true to force-reload the live preview before capturing — useful after editing JS, " + + "and saves a tool call vs. calling controlEditor.reloadLivePreview separately.", { selector: z.string().optional().describe("CSS selector to capture a specific element. Use '#panel-live-preview-frame' for the preview panel (HTML live preview or markdown preview), '.editor-holder' for the code editor."), purePreview: z.boolean().optional().describe("When true, temporarily switches to preview mode to hide element highlight overlays and toolboxes before capturing, then restores the previous mode."), + reload: z.boolean().optional().describe("When true, force-reloads the live preview before capturing. Use this instead of a separate reloadLivePreview call when you're about to screenshot anyway."), filePath: z.string().optional().describe("Absolute path to save the screenshot as a PNG file. If specified, returns the file path instead of inline image data.") }, async function (args) { @@ -140,6 +143,7 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) const result = await _execPeerWithTimeout(nodeConnector, "takeScreenshot", { selector: args.selector || undefined, purePreview: args.purePreview || false, + reload: args.reload || false, filePath: args.filePath || undefined }, "takeScreenshot"); if (result.filePath) { @@ -212,10 +216,14 @@ function createEditorMcpServer(sdkModule, nodeConnector, clarificationAccessors) "- openInWorkingSet: Open a file and pin it to the working set. Params: filePath\n" + "- setSelection: Open a file and select a range. Params: filePath, startLine, startCh, endLine, endCh\n" + "- setCursorPos: Open a file and set cursor position. Params: filePath, line, ch\n" + - "- toggleLivePreview: Show or hide the live preview panel. Params: show (boolean)", + "- toggleLivePreview: Show or hide the live preview panel. Params: show (boolean)\n" + + "- reloadLivePreview: Force-reload the live preview iframe (and any popped-out preview tabs). " + + "Use after editing JS that doesn't appear to have hot-reloaded. Note: if you're about to call " + + "takeScreenshot anyway, prefer takeScreenshot({ reload: true }) — it reloads and captures in " + + "one step. No params.", { operations: z.array(z.object({ - operation: z.enum(["open", "close", "openInWorkingSet", "setSelection", "setCursorPos", "toggleLivePreview"]), + operation: z.enum(["open", "close", "openInWorkingSet", "setSelection", "setCursorPos", "toggleLivePreview", "reloadLivePreview"]), filePath: z.string().optional().describe("Absolute path to the file (not required for toggleLivePreview)"), startLine: z.number().optional().describe("Start line (1-based) for setSelection"), startCh: z.number().optional().describe("Start column (1-based) for setSelection"), diff --git a/src/extensionsIntegrated/Phoenix/phoenix-tour.js b/src/extensionsIntegrated/Phoenix/phoenix-tour.js index 2a0f156960..7eee1aadd8 100644 --- a/src/extensionsIntegrated/Phoenix/phoenix-tour.js +++ b/src/extensionsIntegrated/Phoenix/phoenix-tour.js @@ -32,6 +32,12 @@ define(function (require, exports, module) { const Strings = require("strings"), StringUtils = require("utils/StringUtils"), Metrics = require("utils/Metrics"), + SidebarView = require("project/SidebarView"), + ProjectManager = require("project/ProjectManager"), + EditorManager = require("editor/EditorManager"), + CommandManager = require("command/CommandManager"), + Commands = require("command/Commands"), + WorkspaceManager = require("view/WorkspaceManager"), CentralControlBar = require("view/CentralControlBar"); // Capture the kernel trust ring at module-load time — it's deleted from @@ -46,13 +52,6 @@ define(function (require, exports, module) { const STEP_START_DELAY_MS = 2500; const STEP1_INVITE_MS = 1800; const STEP1_DESIGN_MODE_HOLD_MS = 2000; - // Hard cap on how long we'll wait for the pro trial start dialog to be - // dismissed before starting the tour. The dialog is shown on every fresh - // first-run boot (where this tour also runs), so under normal conditions - // the wait is bounded by the user dismissing it. The cap protects edge - // cases where the dialog isn't shown at all (e.g. user already has a - // subscription / a prior expired trial). - const TRIAL_DIALOG_WAIT_TIMEOUT_MS = 60000; function _loadState() { const raw = PhStore.getItem(TOUR_STORAGE_KEY); @@ -101,7 +100,7 @@ define(function (require, exports, module) { } } - const TOTAL_STEPS = 3; + const TOTAL_STEPS = 4; function _ensureOverlay() { if ($overlay) { @@ -189,7 +188,21 @@ define(function (require, exports, module) { update(); } + /** + * Make sure the sidebar is showing before each step. Upgrade flows can + * boot Phoenix with the sidebar hidden (the user's last-session state), + * which would hide the AI tab and the new-project button this tour + * points at. Cheap to call when already visible — SidebarView.show() + * is a no-op then. + */ + function _ensureSidebarVisible() { + if (SidebarView && SidebarView.isVisible && !SidebarView.isVisible()) { + SidebarView.show(); + } + } + function _runStep1() { + _ensureSidebarVisible(); const $btn = $("#ccbCollapseEditorBtn"); if (!$btn.length) { _markComplete(); @@ -235,6 +248,7 @@ define(function (require, exports, module) { } function _runStep2() { + _ensureSidebarVisible(); const $tab = $('.sidebar-tab[data-tab-id="ai"]'); if (!$tab.length) { // No AI tab in this build — skip ahead to the next step. @@ -260,11 +274,12 @@ define(function (require, exports, module) { } function _runStep3() { + _ensureSidebarVisible(); const $newBtn = $("#newProject"); if (!$newBtn.length) { - // No new-project button — tour is effectively done. - _markComplete(); - _teardown(); + // No new-project button — skip to the live-preview step instead + // of giving up on the tour entirely. + _runStep4(); return; } _ensureOverlay(); @@ -272,6 +287,95 @@ define(function (require, exports, module) { _setStep(3); Metrics.countEvent(Metrics.EVENT_TYPE.GUIDE, "tour", "step3"); _setText(Strings.PHOENIX_TOUR_NEW_PROJECT); + _setActions([ + { + label: Strings.PHOENIX_TOUR_NEXT_BTN, + kind: "primary", + onClick: function () { + _runStep4(); + } + } + ]); + // Intentionally do NOT advance on a real click of the target — only + // the Next button advances. + } + + /** + * Bring the workspace to a state where the live-preview Edit Mode button + * is visible and meaningful: the welcome project must be open, the + * active editor file must be one the preview can render (the welcome + * project's index.html or phoenix-pro.html), and the LP panel must be + * showing. Each branch is a no-op when already true. + */ + async function _ensureLivePreviewReady() { + const welcomeRoot = ProjectManager.getWelcomeProjectPath(); + + // 1. Switch to the welcome project if we're not already there. + const currentRoot = ProjectManager.getProjectRoot(); + if (!currentRoot || currentRoot.fullPath !== welcomeRoot) { + await new Promise(function (resolve) { + ProjectManager.openProject(welcomeRoot) + .done(resolve).fail(resolve); + }); + } + + // 2. Make sure the active file is one the LP can render. Prefer + // phoenix-pro.html (Pro flow) and fall back to index.html. + const proPath = welcomeRoot + "phoenix-pro.html"; + const indexPath = welcomeRoot + "index.html"; + const editor = EditorManager.getActiveEditor(); + const currentFile = editor && editor.document && editor.document.file + ? editor.document.file.fullPath : null; + if (currentFile !== proPath && currentFile !== indexPath) { + let target = null; + try { + if (await Phoenix.VFS.existsAsync(proPath)) { + target = proPath; + } else if (await Phoenix.VFS.existsAsync(indexPath)) { + target = indexPath; + } + } catch (e) { /* fall through with target=null */ } + if (target) { + await new Promise(function (resolve) { + CommandManager.execute(Commands.FILE_OPEN, { fullPath: target }) + .done(resolve).fail(resolve); + }); + } + } + + // 3. Open the live-preview panel if it isn't visible. + const lpPanel = WorkspaceManager.getPanelForID("live-preview-panel"); + if (!lpPanel || !lpPanel.isVisible()) { + await new Promise(function (resolve) { + CommandManager.execute(Commands.FILE_LIVE_FILE_PREVIEW) + .done(resolve).fail(resolve); + }); + } + } + + async function _runStep4() { + _ensureSidebarVisible(); + try { + await _ensureLivePreviewReady(); + } catch (e) { /* best-effort prep — proceed and let the rect-zero + RAF gate sort out a missing target */ } + + const $btn = $("#previewModeLivePreviewButton"); + if (!$btn.length) { + // LP panel never came up (custom server, unsupported file, etc.) + // — finalize the tour rather than stalling on a missing target. + _markComplete(); + _teardown(); + return; + } + _ensureOverlay(); + // LP panel sits on the right edge; keep the default tooltip + // placement (lower-right of the ring) so the tooltip extends back + // into the panel area rather than off the right edge of the viewport. + _trackTarget($btn, "left"); + _setStep(4); + Metrics.countEvent(Metrics.EVENT_TYPE.GUIDE, "tour", "step4"); + _setText(Strings.PHOENIX_TOUR_EDIT_MODE); _setActions([ { label: Strings.PHOENIX_TOUR_DISMISS_BTN, @@ -283,8 +387,6 @@ define(function (require, exports, module) { } } ]); - // Intentionally do NOT end on a real click of the target — only the - // Dismiss button ends the tour. } function _shouldRun() { @@ -308,36 +410,19 @@ define(function (require, exports, module) { } /** - * Resolves once the pro trial start dialog has been dismissed, or after - * TRIAL_DIALOG_WAIT_TIMEOUT_MS as a fallback for builds/runs where the - * dialog isn't shown. + * Resolves once the pro trial start dialog has been dismissed. The + * dialog is guaranteed to fire `proTrialStartDialogDismissed` on every + * boot path (including builds where the dialog isn't shown), so we + * just await it without a timeout fallback. */ function _waitForTrialStartDialogDismissed() { - return new Promise(function (resolve) { - const dismissed = _LoginService && _LoginService.proTrialStartDialogDismissed; - if (!dismissed) { - // No pro trial flow exposed — proceed immediately. - resolve(); - return; - } - let settled = false; - const fallback = setTimeout(function () { - if (settled) { - return; - } - settled = true; - resolve(); - }, TRIAL_DIALOG_WAIT_TIMEOUT_MS); - // jQuery deferred or native promise — both implement .then - Promise.resolve(dismissed).then(function () { - if (settled) { - return; - } - settled = true; - clearTimeout(fallback); - resolve(); - }); - }); + const dismissed = _LoginService && _LoginService.proTrialStartDialogDismissed; + // Community-edition builds expose no login service at all — skip + // the wait so the tour still works there. + if (!dismissed) { + return Promise.resolve(); + } + return Promise.resolve(dismissed); } function startTour() { diff --git a/src/htmlContent/about-dialog.html b/src/htmlContent/about-dialog.html index 021c3ade52..2a98fd61a4 100644 --- a/src/htmlContent/about-dialog.html +++ b/src/htmlContent/about-dialog.html @@ -19,6 +19,7 @@

{{APP_NAME_ABOUT_BOX}}

Arun BoseCharly P AbrahamDevansh Agarwal,  + Ansu Ann KoshyKrrish Parmar

diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index bd448c75b2..86ae96e612 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1672,6 +1672,7 @@ define({ "PHOENIX_TOUR_DESIGN_MODE": "Click here to enter Design Mode. Go full-screen and edit your page visually.", "PHOENIX_TOUR_AI_PANEL": "Design layouts, fix bugs, and build faster with AI. Click here to open the AI panel", "PHOENIX_TOUR_NEW_PROJECT": "Open or create a new project from here", + "PHOENIX_TOUR_EDIT_MODE": "Edit Mode lets you tweak your page directly in the live preview — click elements to inspect and edit them inline.", "PHOENIX_TOUR_STEP_OF": "{0} of {1}", "PHOENIX_TOUR_NEXT_BTN": "Next", "PHOENIX_TOUR_DISMISS_BTN": "Dismiss", @@ -2083,12 +2084,12 @@ define({ "PROMO_UPGRADE_MESSAGE": "Enjoy free access to these premium features for the next {0} days:", "PROMO_CARD_1": "Edit In Live Preview", "PROMO_CARD_1_MESSAGE": "Edit text, update images, change links, drag elements, and more. Your code updates as you go.", - "PROMO_CARD_2": "AI in your editor", - "PROMO_CARD_2_MESSAGE": "Ask the AI to write code, fix bugs, or refactor your project. It does the work for you.", - "PROMO_CARD_3": "Preview Any Device", - "PROMO_CARD_3_MESSAGE": "Resize the live preview to any phone, tablet, or custom viewport - catch responsive issues before they ship.", - "PROMO_CARD_4": "Rich Markdown, Live", - "PROMO_CARD_4_MESSAGE": "A polished side-by-side markdown preview with diagrams, tables, and code blocks - rendered as you type.", + "PROMO_CARD_2": "AI that sees what you see", + "PROMO_CARD_2_MESSAGE": "Explore UI ideas, build apps, and fix issues with AI — it sees your page the same way you do, not just code.", + "PROMO_CARD_3": "See your page on any device", + "PROMO_CARD_3_MESSAGE": "Switch instantly between phone, tablet, and desktop views to catch responsive issues early.", + "PROMO_CARD_4": "Your Markdown, as a real document", + "PROMO_CARD_4_MESSAGE": "Edit tables, images, and formatting visually — an easier way to work with Markdown.", "PROMO_LEARN_MORE": "Learn More\u2026", "PROMO_OPT_OUT_LINK": "Opt out?", "PROMO_OPT_OUT_NOTE": "You can cancel your trial anytime by selecting `Help > Cancel Phoenix Pro Trial`.", @@ -2150,8 +2151,8 @@ define({ "AI_UPSELL_DIALOG_MESSAGE": "You’ve discovered {0}. To proceed, you’ll need an AI subscription or credits.", // AI CHAT PANEL - "AI_CHAT_TITLE_NO_AI": "Phoenix Code AI", "AI_CHAT_TITLE": "Claude Code", + "AI_CHAT_SURPRISE_ME_USER_MSG": "Surprise me!", "AI_CHAT_NEW_SESSION_TITLE": "Start a new conversation", "AI_CHAT_NEW_BTN": "New", "AI_CHAT_THINKING": "Thinking...", @@ -2159,24 +2160,34 @@ define({ "AI_CHAT_SEND_TITLE": "Send message", "AI_CHAT_STOP_TITLE": "Stop generation (Esc)", "AI_CHAT_CLI_NOT_FOUND": "Claude Code Not Installed", - "AI_CHAT_CLI_INSTALL_MSG": "Claude Code CLI must be installed on your system to use AI features in {APP_NAME}. Learn more", + "AI_CHAT_CLI_INSTALL_MSG": "Claude Code CLI must be installed on your system to use AI features in {APP_NAME}.", "AI_CHAT_CLI_INSTALL_BTN": "Install Claude Code", "AI_CHAT_CLI_INSTALLING": "Installing…", "AI_CHAT_CLI_INSTALLING_MSG": "Installing Claude Code, please wait. This may take a while...", "AI_CHAT_CLI_RESTART_NOTE": "Restart {APP_NAME} after installation completes.", "AI_CHAT_CLAUDE_LOGIN_TITLE": "Setup Claude Code", - "AI_CHAT_CLAUDE_LOGIN_MSG": "Claude Code is installed but needs to be configured. Learn more", + "AI_CHAT_CLAUDE_LOGIN_MSG": "Claude Code is installed but it needs configuration.", "AI_CHAT_CLAUDE_LOGIN_BTN": "Setup Claude Code", "AI_CHAT_ADD_PROVIDER_BTN": "Add Custom Provider", + "AI_CHAT_SETUP_NEED_HELP": "Need Help?", + "AI_CHAT_SETUP_LEARN_MORE": "Learn More", "AI_CHAT_RETRY": "Retry", - "AI_CHAT_DESKTOP_ONLY": "AI features require the Phoenix desktop app. Download it to get started.", + "AI_CHAT_DESKTOP_ONLY": "AI features require the {APP_NAME} desktop app. Download it to get started.", "AI_CHAT_DOWNLOAD_BTN": "Download Desktop App", - "AI_CHAT_LOGIN_TITLE": "Sign In to Use AI", + "AI_CHAT_PLACEHOLDER_TITLE": "Design, build, and fix faster with AI", + "AI_CHAT_PLACEHOLDER_SUBTITLE": "AI that works with your preview", + "AI_CHAT_PLACEHOLDER_CARD_LAYOUT_TITLE": "Generate Layout", + "AI_CHAT_PLACEHOLDER_CARD_LAYOUT_DESC": "Describe a section", + "AI_CHAT_PLACEHOLDER_CARD_BUGS_TITLE": "Fix Bugs", + "AI_CHAT_PLACEHOLDER_CARD_BUGS_DESC": "Scan for issues", + "AI_CHAT_PLACEHOLDER_CARD_IMPROVE_TITLE": "Suggest improvements", + "AI_CHAT_PLACEHOLDER_CARD_IMPROVE_DESC": "UX & performance tips", + "AI_CHAT_LOGIN_TITLE": "Sign in to get started with AI", "AI_CHAT_LOGIN_MESSAGE": "Sign in to your {APP_NAME} account to access AI features.", "AI_CHAT_LOGIN_BTN": "Sign In", "AI_CHAT_INTRO_GUEST_TITLE": "Design layouts, fix bugs, and build faster with AI", "AI_CHAT_INTRO_CONFIGURE_TITLE": "Getting started with Claude Code", - "AI_CHAT_INTRO_CONFIGURE_DESC": "See this short video on how to set up AI", + "AI_CHAT_INTRO_CONFIGURE_DESC": "Watch this short video to set up AI in {APP_NAME}", "AI_CHAT_UPSELL_TITLE": "Phoenix Pro + AI", "AI_CHAT_UPSELL_MESSAGE": "AI features are available with Phoenix Pro.", "AI_CHAT_UPSELL_BTN": "Get Phoenix Pro", diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index b1c26c4716..af7b30112f 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -2840,6 +2840,221 @@ } } +/* Browser-build placeholder layout (designed by the team — left-aligned + header, video, three info cards, body copy, full-width CTA). Used only + when the browser app can't run AI features, so the panel doubles as a + marketing surface for the desktop app. */ + +.ai-placeholder { + display: flex; + flex-direction: column; + gap: 1.1rem; + padding: 1.25rem 1.25rem 1.5rem; + box-sizing: border-box; + /* Allow the column to shrink below its intrinsic content size when the + sidebar is narrow, otherwise long words like "improvements" force a + horizontal scroll. min-width:0 alone isn't enough on a flex column — + the inner cards' default min-width:auto prevents shrink. */ + min-width: 0; + width: 100%; + + /* The chat panel sets `white-space: nowrap` higher up so streaming + chat lines don't reflow mid-token. Reset it here so all text in the + placeholder wraps normally when the sidebar is narrow. */ + &, * { + white-space: normal; + } +} + +.ai-placeholder-header { + display: flex; + flex-direction: column; + gap: 4px; +} + +.ai-placeholder-title { + font-size: 22px; + font-weight: 700; + line-height: 1.2; + margin: 0; + color: @project-panel-text-1; + letter-spacing: -0.01em; +} + +.ai-placeholder-subtitle { + font-size: 13px; + color: @project-panel-text-2; + opacity: 0.85; + line-height: 1.4; +} + +/* Video slot in the placeholder: full-width, no max cap (the surrounding + `.ai-placeholder` panel padding controls effective width). The shared + `.ai-intro-video-slot` rule above is centered and capped at 320px, + which would look orphaned in the new left-aligned layout. */ +.ai-placeholder .ai-intro-video-slot { + max-width: none; + margin: 0; + text-align: left; + gap: 0; +} + +.ai-placeholder-cards { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.ai-placeholder-card { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 8px; + background: rgba(255, 255, 255, 0.025); + /* Allow the card to shrink at narrow sidebar widths instead of forcing + a horizontal scroll. */ + min-width: 0; +} + +.ai-placeholder-card-icon { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 6px; + background: rgba(255, 255, 255, 0.04); + color: @project-panel-text-1; + font-size: 14px; +} + +.ai-placeholder-card-text { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + flex: 1 1 auto; +} + +.ai-placeholder-card-title, +.ai-placeholder-card-desc { + /* Wrap long localized strings instead of overflowing the card. */ + overflow-wrap: anywhere; + word-break: break-word; +} + +.ai-placeholder-card-title { + font-size: 13.5px; + font-weight: 600; + color: @project-panel-text-1; + line-height: 1.25; +} + +.ai-placeholder-card-desc { + font-size: 12px; + color: @project-panel-text-2; + opacity: 0.8; + line-height: 1.3; +} + +.ai-placeholder-body { + font-size: 12.5px; + color: @project-panel-text-2; + opacity: 0.75; + line-height: 1.5; + margin: 4px 0 0; +} + +/* The shared `.btn.btn-primary` override above sets padding: 6px 18px + for the older Sign-In CTA. `!important` here locks in the designer's + taller spec for this placeholder CTA without having to thread a more + specific selector through every inherited declaration. */ +.ai-chat-panel .ai-placeholder .ai-placeholder-cta { + width: 100%; + padding: 14px 18px !important; + font-size: 14px; + font-weight: 600; + line-height: 1.3; + /* Match the 8px corner radius of the cards above so the column reads + as one coherent stack. The shared .btn.btn-primary rule sets 4px, + hence !important. */ + border-radius: 8px !important; + /* Reset horizontal margins — a higher-up `.btn` rule adds + `margin-left: 3px` which would shift the CTA right of the cards. */ + margin: 4px 0 0; +} + +/* Setup variant of the primary CTA: white pill on the dark panel, used + on the "Install Claude Code" / "Setup Claude Code" screens. The + placeholder/login CTA stays blue — only these setup-on-dark surfaces + flip to white per the designer spec. The selector chains both .btn + and .btn-primary classes (specificity 0,6,0) to beat the + `.dark .ai-chat-panel .ai-placeholder .btn.btn-primary` rule that + would otherwise force blue in the default dark theme. */ +.ai-chat-panel .ai-placeholder .btn.btn-primary.ai-placeholder-cta.ai-placeholder-cta-on-dark { + background-color: #ffffff !important; + background-image: none !important; + color: #1a1a1a !important; + border: 1px solid rgba(0, 0, 0, 0.08) !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25) !important; + + &:hover { + background-color: #f4f4f4 !important; + } + + &:active { + background-color: #e8e8e8 !important; + } +} + +/* Outlined / muted secondary CTA — sits directly under the white + primary on the setup screen for the "Add Custom Provider" alternative. */ +.ai-chat-panel .ai-placeholder .ai-placeholder-cta-secondary { + width: 100%; + padding: 12px 18px; + font-size: 13.5px; + font-weight: 500; + line-height: 1.3; + border-radius: 8px; + margin: 8px 0 0; + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.12); + color: @project-panel-text-1; + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease; + + &:hover { + background-color: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.2); + } +} + +/* Centered "Need Help? Learn More" footer below the setup CTAs. */ +.ai-chat-panel .ai-placeholder-help { + margin-top: 14px; + text-align: center; + font-size: 12.5px; + color: @project-panel-text-2; + opacity: 0.75; +} + +.ai-chat-panel .ai-placeholder-help .ai-learn-more-link { + color: @bc-primary-btn-bg; + text-decoration: none; + margin-left: 4px; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} + /* Re-apply Phoenix's standard `.btn .btn-primary` look inside the .ai-unavailable intro states. The blanket reset below (`.ai-chat-panel .btn-primary`) blanks bootstrap's button styling so chat-area buttons @@ -2847,7 +3062,8 @@ button so the Sign In CTA reads as the standard Phoenix action. The selector is more specific than the reset, and `!important` belt-and- braces against any later reset rule that might still apply. */ -.ai-chat-panel .ai-unavailable .btn.btn-primary { +.ai-chat-panel .ai-unavailable .btn.btn-primary, +.ai-chat-panel .ai-placeholder .btn.btn-primary { display: inline-block; background-color: @bc-primary-btn-bg !important; background-image: none !important; @@ -2876,7 +3092,8 @@ } } -.dark .ai-chat-panel .ai-unavailable .btn.btn-primary { +.dark .ai-chat-panel .ai-unavailable .btn.btn-primary, +.dark .ai-chat-panel .ai-placeholder .btn.btn-primary { background-color: @dark-bc-primary-btn-bg !important; border-color: darken(@dark-bc-primary-btn-bg, 8%) !important; diff --git a/src/styles/phoenix-pro.less b/src/styles/phoenix-pro.less index 4bc90dcf68..b29661f786 100644 --- a/src/styles/phoenix-pro.less +++ b/src/styles/phoenix-pro.less @@ -85,17 +85,31 @@ background: #1b1b1b; } + /* Slide-over animation: every slide is positioned absolute and lives + at one of three transforms — off-screen right (default), active (0), + or off-screen left (.is-left). JS toggles is-active / is-left so the + transitioning slides cross-fade and translate at the same time, in + the direction matching the user's nav. */ .feature-slide { position: absolute; inset: 0; opacity: 0; pointer-events: none; - transition: opacity 0.35s ease; + transform: translateX(100%); + transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.35s ease; + will-change: transform, opacity; } + .feature-slide.is-left { + transform: translateX(-100%); + } + + /* Defined after .is-left so a slide flipping from is-left to is-active + animates straight from -100% to 0, no intermediate jump to +100%. */ .feature-slide.is-active { opacity: 1; pointer-events: auto; + transform: translateX(0); } .feature-thumb { @@ -228,21 +242,30 @@ } } + /* Mirror the .feature-slide transform/cross-fade so the text below the + video animates in the same direction as the video stage. */ .feature-info-slide { position: absolute; inset: 0; padding: 10px 28px; opacity: 0; pointer-events: none; - transition: opacity 0.35s ease; + transform: translateX(100%); + transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.35s ease; + will-change: transform, opacity; font-feature-settings: "kern" 1, "liga" 1, "calt" 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + .feature-info-slide.is-left { + transform: translateX(-100%); + } + .feature-info-slide.is-active { opacity: 1; pointer-events: auto; + transform: translateX(0); } .feature-info-slide h2 { diff --git a/tracking-repos.json b/tracking-repos.json index 64b439c59e..ef2c9b6463 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "85b05bfa13b79712d6854be27d2b02b9d1fa065f" + "commitID": "f1efe02545e90721e29b7c3acd409c5e7cce8df1" } }