From ac17a0f91102e99d6b48edb99bc3696d532d859b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 23:10:45 +0000 Subject: [PATCH 1/3] feat(app-shell): Publish opens the pending-changes sheet to confirm, Changes to review DraftChangesPanel takes a `posture` prop. `confirm` expands every row once the list arrives, puts a per-type summary line above it and moves focus to Publish all; `review` (the default) keeps rows collapsed and focus on the list. The Studio's Publish passes `confirm`, its Changes button `review`; every other host passes nothing and keeps reviewing. Claude-Session: https://claude.ai/code/session_01MxLBRTq32u4PebqJCjFXW7 Co-authored-by: Claude --- .changeset/12517-publish-confirm-posture.md | 13 ++ .../src/preview/DraftChangesPanel.tsx | 68 ++++++- .../DraftChangesPanel.posture-12517.test.tsx | 142 +++++++++++++ ...esignSurface.publishPosture-12517.test.tsx | 186 ++++++++++++++++++ .../studio-design/StudioDesignSurface.tsx | 19 +- packages/i18n/src/locales/ar.ts | 8 + packages/i18n/src/locales/de.ts | 4 + packages/i18n/src/locales/en.ts | 6 + packages/i18n/src/locales/es.ts | 5 + packages/i18n/src/locales/fr.ts | 5 + packages/i18n/src/locales/ja.ts | 4 + packages/i18n/src/locales/ko.ts | 4 + packages/i18n/src/locales/pt.ts | 5 + packages/i18n/src/locales/ru.ts | 6 + packages/i18n/src/locales/zh.ts | 4 + 15 files changed, 474 insertions(+), 5 deletions(-) create mode 100644 .changeset/12517-publish-confirm-posture.md create mode 100644 packages/app-shell/src/preview/__tests__/DraftChangesPanel.posture-12517.test.tsx create mode 100644 packages/app-shell/src/views/studio-design/StudioDesignSurface.publishPosture-12517.test.tsx diff --git a/.changeset/12517-publish-confirm-posture.md b/.changeset/12517-publish-confirm-posture.md new file mode 100644 index 0000000000..08de00b47c --- /dev/null +++ b/.changeset/12517-publish-confirm-posture.md @@ -0,0 +1,13 @@ +--- +'@object-ui/app-shell': minor +'@object-ui/i18n': minor +--- + +The Studio's Publish and Changes buttons no longer open the pending-changes sheet the same way. + +- **Publish opens it to confirm.** Every pending item is expanded, so the fields and keys each one adds, changes or removes are on screen. A summary line above the list counts what the publish carries, by type (for example "3 items · 1 object · 1 app · 1 page"). Focus lands on Publish all. The header button still publishes nothing by itself; only Publish all in the sheet does. +- **Changes opens it to review.** Items stay collapsed and focus lands on the list. Publish all is still in the footer, unfocused. + +Every other place that opens the sheet (the Home page, the draft preview bar and the history timeline) keeps the review behaviour. With nothing pending, Publish stays disabled and says why, as before. + +New language-pack keys under `preview.changes`: `summaryItems` (with its plural forms) and `summaryType`, in every shipped locale. diff --git a/packages/app-shell/src/preview/DraftChangesPanel.tsx b/packages/app-shell/src/preview/DraftChangesPanel.tsx index 889646a653..f6af034fc7 100644 --- a/packages/app-shell/src/preview/DraftChangesPanel.tsx +++ b/packages/app-shell/src/preview/DraftChangesPanel.tsx @@ -471,8 +471,20 @@ export interface DraftChangesPanelProps { onPublish?: () => void | Promise; /** Disables the confirm button and shows a spinner while the caller publishes. */ publishing?: boolean; + /** + * How the sheet opens (objectui#12517). `review` — the default, and what + * every host but the Studio's Publish gets: rows collapsed, focus on the + * list. `confirm` — the host's Publish opened it: every row expanded to + * show what changes, a per-type summary line above the list, and focus on + * the footer's Publish all once it renders. Read on each open; a posture + * never changes what the footer button does (objectui#2261: only it + * publishes). + */ + posture?: DraftChangesPosture; } +export type DraftChangesPosture = 'review' | 'confirm'; + export function DraftChangesPanel({ open, onOpenChange, @@ -480,6 +492,7 @@ export function DraftChangesPanel({ include, onPublish, publishing = false, + posture = 'review', }: DraftChangesPanelProps) { const { t, language } = useObjectTranslation(); const [entries, setEntries] = useState(null); @@ -514,6 +527,36 @@ export function DraftChangesPanel({ }); }, []); + /** + * objectui#12517 — the posture's two openings. Each open starts collapsed; + * the confirm posture expands every row ONCE, when the list first arrives, + * so a row the author then collapses stays collapsed while the type reads + * below re-map the entries. Focus lands on the list at open (the Publish + * all button does not exist until the list loads) and moves to Publish all + * on that same arrival — only if the author has not moved it meanwhile. + */ + const listRef = useRef(null); + const publishRef = useRef(null); + const settledRef = useRef(false); + // The list as it stood when the sheet opened: the previous open's, until + // this open's load replaces it. Only a list newer than that one settles. + const entriesAtOpenRef = useRef(null); + useEffect(() => { + if (!open) return; + settledRef.current = false; + entriesAtOpenRef.current = entries; + setExpanded(new Set()); + // Keyed on the open transition alone: `entries` is read, not watched. + }, [open]); + useEffect(() => { + if (!open || settledRef.current || entries === null || entries === entriesAtOpenRef.current) return; + settledRef.current = true; + if (posture !== 'confirm') return; + setExpanded(new Set(entries.map(entryKey))); + const button = publishRef.current; + if (button && document.activeElement === listRef.current) button.focus(); + }, [open, posture, entries]); + const markCompared = (key: string, isUnchanged: boolean) => { setUnchanged((prev) => { if (prev.has(key) === isUnchanged) return prev; @@ -652,6 +695,13 @@ export function DraftChangesPanel({ side="right" className="flex w-[420px] flex-col sm:max-w-[420px]" data-testid="draft-changes-panel" + data-posture={posture} + onOpenAutoFocus={(event) => { + // objectui#12517 — the list, not the first tabbable (the close + // button): see the posture effect above for where focus goes next. + event.preventDefault(); + listRef.current?.focus(); + }} > @@ -663,9 +713,24 @@ export function DraftChangesPanel({ })} + {posture === 'confirm' && entries && entries.length > 0 && !error ? ( + // objectui#12517 — what the publish carries, counted off the same + // per-type grouping the headings below read. +

+ {[ + t('preview.changes.summaryItems', { count: entries.length, defaultValue: '{{count}} items' }), + ...[...byType.entries()].map(([type, items]) => + t('preview.changes.summaryType', { n: items.length, type, defaultValue: '{{n}} {{type}}' }), + ), + ].join(' · ')} +

+ ) : null}
{error ? (

@@ -854,6 +919,7 @@ export function DraftChangesPanel({ })}