From 61a196619731e0926a490bd561b127b9ce0e31e8 Mon Sep 17 00:00:00 2001 From: Vianney MORAIN Date: Tue, 8 Sep 2026 21:38:51 +0200 Subject: [PATCH] feat(publish): move renderMode / host selectors into an Advanced settings toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two self-hosting selectors (Rendering, Hosting) are now inside a collapsible "Advanced settings" disclosure in the Publish panel, closed by default. It opens automatically on mount when a non-default target is selected (renderMode !== "ssr" or host !== "local") so a persisted or seeded choice stays visible. Open/closed is remembered globally in localStorage (publish:advancedOpen) — a UI preference, not per-project. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017rF3HMB1Z7V3LLsYaUrELs --- .../app/builder/features/publish/publish.tsx | 129 +++++++++++++----- 1 file changed, 93 insertions(+), 36 deletions(-) diff --git a/apps/builder/app/builder/features/publish/publish.tsx b/apps/builder/app/builder/features/publish/publish.tsx index 63b582bc6282..7f404d35e081 100644 --- a/apps/builder/app/builder/features/publish/publish.tsx +++ b/apps/builder/app/builder/features/publish/publish.tsx @@ -40,6 +40,7 @@ import { css, textVariants, SmallIconButton, + Collapsible, } from "@webstudio-is/design-system"; import { validateProjectDomain, type Project } from "@webstudio-is/project"; import { @@ -82,6 +83,8 @@ import { UpgradeIcon, HelpIcon, InfoCircleIcon, + ChevronDownIcon, + ChevronRightIcon, } from "@webstudio-is/icons"; import { AddDomain } from "./add-domain"; import { humanizeString } from "~/shared/string-utils"; @@ -549,6 +552,36 @@ export const usePublishTarget = (projectId: string) => { return { renderMode, setRenderMode, host, setHost }; }; +const advancedPublishOpenStorageKey = "publish:advancedOpen"; + +/** + * Open/closed state of the "Advanced settings" disclosure holding the + * renderMode / host selectors. Persisted globally — it's a UI preference, not a + * per-project setting. Forced open on mount when a non-default target is + * selected so the user can see (and change) what they'll publish with. + */ +const useAdvancedPublishOpen = (hasNonDefaultTarget: boolean) => { + const [isOpen, setIsOpen] = useState(() => { + if (hasNonDefaultTarget) { + return true; + } + try { + return localStorage.getItem(advancedPublishOpenStorageKey) === "true"; + } catch { + return false; + } + }); + const handleOpenChange = (open: boolean) => { + try { + localStorage.setItem(advancedPublishOpenStorageKey, String(open)); + } catch { + // localStorage unavailable — keep the in-memory state only + } + setIsOpen(open); + }; + return [isOpen, handleOpenChange] as const; +}; + const Publish = ({ project, timesLeft, @@ -585,6 +618,9 @@ const Publish = ({ useState(false); const countdown = usePublishCountdown(isPublishing); const publisherHost = useStore($publisherHost); + const [advancedOpen, setAdvancedOpen] = useAdvancedPublishOpen( + renderMode !== "ssr" || host !== "local" + ); const { load: loadCapabilities, data: capabilities } = trpcClient.domain.publisherCapabilities.useQuery(); @@ -811,42 +847,63 @@ const Publish = ({ )} {publisherHost && ( - <> - publishHostLabels[value]} - getDescription={(value: PublishHost) => - publishHostDescriptions[value] - } - getItemProps={(value: PublishHost) => { - const reason = publishHostUnavailableReason( - capabilities, - renderMode, - value - ); - return reason === undefined - ? {} - : { disabled: true, title: reason }; - }} - onChange={onHostChange} - /> - + + + + + + + + publishHostLabels[value]} + getDescription={(value: PublishHost) => + publishHostDescriptions[value] + } + getItemProps={(value: PublishHost) => { + const reason = publishHostUnavailableReason( + capabilities, + renderMode, + value + ); + return reason === undefined + ? {} + : { disabled: true, title: reason }; + }} + onChange={onHostChange} + /> + + + + )}