Files
Saxon FletcherandClaude Opus 5 7880c2f079 fix(studio): explorer chat and notebook layout refinements (#50453)
Five layout fixes across Explorer chat, notebooks, and the sidebar.

### Chat

- **Conversation fade overlapped the scrollbar.** The top and bottom
gradients are positioned against the conversation's padding box, which
includes the scroll container's scrollbar gutter, so `inset-x-0` painted
them over the scrollbar. They now stop at the conversation's content
gutter, which `Conversation` owns for both the content and the fades.
- **Composer background bled past the input's radius.** The form paints
the surface behind the textarea but had no radius of its own, so its
square corners showed outside the `rounded-lg` input. It now shares the
radius.
- **Message parts used two different widths.** Wide parts come down to
`max-w-3xl` so every part shares a column, matching `AssistantQueryCell`
and `AssistantNotebookPreview`. `isWide` / `isWideMessagePart` stay in
place with both widths equal, so a part can diverge again later without
rebuilding the mechanism.

### Notebooks

- **Cell controls sat at the container edge.** Each cell centred itself
at its own max width while the grip and add-cell button stayed at the
far left of the full-width row, leaving a large gap. `SortableSection`
takes a `sectionWidth` and carries its control gutter twice — once as
the controls, once as padding on the other side — so the section stays
centred with its controls immediately beside it. Cell widths are
unchanged (prose `48rem`, query `72rem`); set them equal and the two
cell types' controls line up on their own.

The controls stay in flow rather than floating in an outside gutter, so
on a viewport narrower than the cap the row just fills the space instead
of clipping the controls into the padding.

### Sidebar

- **Search icon didn't line up with the menu row icons.** The row box
already sits flush with the search input's box, so rows moved from
`pl-3` to `pl-2` to put their icons on the same 8px offset the search
icon uses. Spacing between the input and the list now matches the 12px
side padding.

### Testing

`pnpm --filter studio run typecheck`, Prettier, and 378 tests across
`Explorer`, `ProjectHome`, `AIAssistantPanel`, and `ExplorerLayout`
pass. ESLint warning counts are unchanged from master.

These were reasoned from layout rather than checked in a browser, so
they're worth a look on a preview before merge.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **UI Improvements**
* Updated Explorer layouts with flexible, configurable widths for
notebook and query sections.
  * Refined navigation spacing and padding across Explorer views.
* Centered and standardized AI Assistant preview, query, and message
content widths.
  * Improved chat form styling with rounded corners.
* Adjusted conversation spacing and fade overlays to avoid overlapping
the scrollbar.
* Preserved full-width behavior where appropriate while keeping controls
aligned.

* **Tests**
* Updated layout tests to reflect revised width and alignment behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 11:39:30 +08:00

144 lines
5.1 KiB
TypeScript

import { useParams } from 'common'
import { NotebookText } from 'lucide-react'
import { useMemo, useState } from 'react'
import { Button, cn } from 'ui'
import {
formatNotebookDiffSummary,
getEntryKey,
notebookEntriesNeedDatabaseLookup,
summarizeNotebookDiff,
type NotebookDatabaseContext,
} from './AssistantNotebookPreview.utils'
import { AssistantNotebookPreviewCell } from './AssistantNotebookPreviewCell'
import {
ExplorerToolbar,
ExplorerToolbarActions,
ExplorerToolbarIcon,
ExplorerToolbarTitle,
} from '@/components/interfaces/Explorer/ExplorerToolbar'
import type { QueryResult } from '@/components/interfaces/Explorer/types'
import type { NotebookCellDiffEntry } from '@/data/content/notebooks/notebook-operations'
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
export interface AssistantNotebookPreviewProps {
entries: NotebookCellDiffEntry[]
mode: 'create' | 'update' | 'run'
/** The notebook's name, shown in the toolbar. Falls back to a generic label. */
title?: string
/** Query results keyed by persisted cell id. */
results?: Record<string, QueryResult>
className?: string
}
const VISIBLE_ENTRY_LIMIT = 5
const FALLBACK_TITLE = {
create: 'New notebook',
update: 'Notebook changes',
run: 'Notebook run',
} as const
/**
* Read-only minified notebook for assistant create/update proposals and notebook runs.
* Composes the same Explorer toolbar and query-result surfaces as notebook tabs; the
* surrounding `Confirm` card owns the frame. Pure presentational: no data fetching,
* approval, or notebook-editor state — callers adapt those concerns into entries/results.
*/
export const AssistantNotebookPreview = ({
entries,
mode,
title,
results,
className,
}: AssistantNotebookPreviewProps) => {
const { ref: projectRef } = useParams()
const [isShowingAllEntries, setIsShowingAllEntries] = useState(false)
const [expandedOverrides, setExpandedOverrides] = useState<Record<string, boolean>>({})
const needsDatabaseLookup = notebookEntriesNeedDatabaseLookup(entries, projectRef)
const {
data: databases,
isPending: isLoadingDatabases,
isError: isDatabaseError,
} = useReadReplicasQuery({ projectRef }, { enabled: needsDatabaseLookup })
const databasesByIdentifier = useMemo(
() => new Map((databases ?? []).map((database) => [database.identifier, database])),
[databases]
)
const databaseContext: NotebookDatabaseContext = !needsDatabaseLookup
? { status: 'success', projectRef, databasesByIdentifier }
: isLoadingDatabases
? { status: 'loading', projectRef }
: isDatabaseError
? { status: 'error', projectRef }
: { status: 'success', projectRef, databasesByIdentifier }
const summary = summarizeNotebookDiff(entries, mode)
const visibleEntries = isShowingAllEntries ? entries : entries.slice(0, VISIBLE_ENTRY_LIMIT)
const hiddenCount = entries.length - visibleEntries.length
const hasResults = results !== undefined
const isExpanded = (entry: NotebookCellDiffEntry) =>
expandedOverrides[getEntryKey(entry)] === true
return (
<div className={cn('flex w-full min-w-0 max-w-3xl mx-auto flex-col', className)}>
<ExplorerToolbar aria-label="Notebook toolbar">
<ExplorerToolbarIcon>
<NotebookText size={16} strokeWidth={2} />
</ExplorerToolbarIcon>
<ExplorerToolbarTitle>{title ?? FALLBACK_TITLE[mode]}</ExplorerToolbarTitle>
<ExplorerToolbarActions>
<span className="shrink-0 text-sm text-muted-foreground">
{formatNotebookDiffSummary(summary)}
</span>
</ExplorerToolbarActions>
</ExplorerToolbar>
<div className="p-2">
<div
className={cn(
hasResults ? 'flex flex-col gap-2' : 'overflow-hidden rounded-md border bg-surface-100'
)}
>
<div className={cn(hasResults ? 'contents' : 'divide-y divide-border')}>
{visibleEntries.map((entry) => {
const key = getEntryKey(entry)
return (
<div
key={key}
className={cn(hasResults && 'overflow-hidden rounded-md border bg-surface-100')}
>
<AssistantNotebookPreviewCell
entry={entry}
mode={mode}
result={results?.[key]}
isExpanded={isExpanded(entry)}
databaseContext={databaseContext}
onExpandedChange={(open) =>
setExpandedOverrides((prev) => ({ ...prev, [key]: open }))
}
/>
</div>
)
})}
</div>
{hiddenCount > 0 && (
<Button
variant="text"
size="tiny"
className={cn(
'w-full text-foreground-light',
!hasResults && 'rounded-none border-0 border-t border-default'
)}
onClick={() => setIsShowingAllEntries(true)}
>
Show {hiddenCount} more cell{hiddenCount === 1 ? '' : 's'}
</Button>
)}
</div>
</div>
</div>
)
}