From 26ebb64458859bfebd7564c12ef61ed3297063d3 Mon Sep 17 00:00:00 2001 From: CRT Date: Fri, 23 Oct 2026 10:44:45 +0800 Subject: [PATCH] Desktop: tab row measures the room inside the sticky bar's padding (the More button was clipped); editor saves skip the in-app preview (their Review step already showed the dry run) --- apps/desktop/src/store.tsx | 11 +++++++---- apps/desktop/src/ui/Editor.tsx | 2 +- apps/desktop/src/ui/ds.tsx | 6 +++++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/store.tsx b/apps/desktop/src/store.tsx index ee77473..d203870 100644 --- a/apps/desktop/src/store.tsx +++ b/apps/desktop/src/store.tsx @@ -557,7 +557,8 @@ export interface ReadState { refresh: () => Promise; } -export interface WriteRun { method: string; args: Record; title: string; done: string; after?: () => void } +/** preview: false when the screen already showed this dry run (the editors' Review step); the native dialog still follows. */ +export interface WriteRun { method: string; args: Record; title: string; done: string; after?: () => void; preview?: false } /** * Writes of one app folder: dry run, the native dialog that shows it (Apply / Cancel, or the owner action's own @@ -575,9 +576,11 @@ export function useWrite(projectId: string | null) { const tid = d.toast({ id: job, kind: 'job', title: w.title, detail: t('write.dryRunning'), progress: null, job }); try { // the in-app preview of the dry run first (readable, styled); the host's native dialog stays the real gate - const dry = await api.call(w.method, projectId, { args: w.args }, job); - if (!dry.ok) { d.updateToast(tid, { kind: 'bad', title: t('write.dryFailed'), detail: withHint(failureText(dry)), progress: undefined, job: undefined }); return null; } - if (!(await askPreview(w.title, dry))) { d.updateToast(tid, { kind: 'info', title: t('write.notApplied'), detail: t('write.notAppliedDetail'), progress: undefined, job: undefined }); return null; } + if (w.preview !== false) { + const dry = await api.call(w.method, projectId, { args: w.args }, job); + if (!dry.ok) { d.updateToast(tid, { kind: 'bad', title: t('write.dryFailed'), detail: withHint(failureText(dry)), progress: undefined, job: undefined }); return null; } + if (!(await askPreview(w.title, dry))) { d.updateToast(tid, { kind: 'info', title: t('write.notApplied'), detail: t('write.notAppliedDetail'), progress: undefined, job: undefined }); return null; } + } const o = await api.writeWithConfirm(w.method, projectId, w.args, job); if (o.stage === 'dry-run') d.updateToast(tid, { kind: 'bad', title: t('write.dryFailed'), detail: withHint(failureText(o.dryRun)), progress: undefined, job: undefined }); else if (o.stage === 'dialog') d.updateToast(tid, { kind: 'info', title: t('write.notApplied'), detail: t('write.notAppliedDetail'), progress: undefined, job: undefined }); diff --git a/apps/desktop/src/ui/Editor.tsx b/apps/desktop/src/ui/Editor.tsx index 84d5029..cc079c3 100644 --- a/apps/desktop/src/ui/Editor.tsx +++ b/apps/desktop/src/ui/Editor.tsx @@ -260,7 +260,7 @@ export function EditBar({ project, area, data, files, onSaved }: { project: Proj 0 || !review.r.files.length} hint={review.r.errors ? t('ed.fixFirst') : undefined} - w={{ method: 'project.edit', args: { ops: opsJson }, title: t('ed.save'), done: t('ed.saved'), after: () => { e.clear(project.id, area); setReview(null); onSaved?.(); } }}>{t('ed.save')} + w={{ method: 'project.edit', args: { ops: opsJson }, preview: false, title: t('ed.save'), done: t('ed.saved'), after: () => { e.clear(project.id, area); setReview(null); onSaved?.(); } }}>{t('ed.save')} )} diff --git a/apps/desktop/src/ui/ds.tsx b/apps/desktop/src/ui/ds.tsx index ad97ce1..b38aa8b 100644 --- a/apps/desktop/src/ui/ds.tsx +++ b/apps/desktop/src/ui/ds.tsx @@ -295,7 +295,11 @@ export function Tabs({ items, value, onChange, label, panelId refs.current.forEach((b, i) => { if (b && b.offsetParent !== null) widths.current[i] = b.getBoundingClientRect().width; }); if (widths.current.length < items.length || widths.current.some(w => w === undefined)) { setShown(null); return; } const cs = getComputedStyle(el); - const avail = (el.parentElement?.clientWidth ?? el.clientWidth) - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight) - parseFloat(cs.borderLeftWidth) - parseFloat(cs.borderRightWidth); + // the row's room: the parent's content box (the sticky bar has its own padding) minus the row's padding and border + const par = el.parentElement; + const ps = par ? getComputedStyle(par) : null; + const box = par && ps ? par.clientWidth - parseFloat(ps.paddingLeft) - parseFloat(ps.paddingRight) : el.clientWidth; + const avail = box - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight) - parseFloat(cs.borderLeftWidth) - parseFloat(cs.borderRightWidth) - 2; const next = fitTabs(widths.current, avail, active, parseFloat(cs.columnGap) || 4, 150); setShown(prev => (prev && prev.join() === next.join() ? prev : next.length === items.length ? null : next)); };