In embed mode the "+ New row" button rendered to the right of the
data columns instead of at the page-content edge (where it lives in
standalone). Two compounding causes:
- position: sticky with inset-inline-start:
var(--embed-grid-pad-left, 0). Sticky offsets are measured from
the scroll-port, not the bodyGrid's outer edge — and the scroll-
port already starts at the page-content edge (bodyGrid's negative
margins and equal padding cancel out). With the variable set to
~200px in embed, sticky shifted the button 200px *into* the
scroll-port. inset-inline-start: 0 keeps it at the scroll-port's
start in both modes.
- max-content grid item with default justify-self: stretch on a
grid-column: 1 / -1 area has surprising placement; `justify-self:
start` makes the inline-start anchoring explicit.
Standalone behavior is unchanged (the variable was 0 there anyway).
Split the unified .gridWrapper into a sticky band (containing column
headers, plus banner + toolbar in inline) and a body grid that owns
horizontal scroll. The band's vertical sticky anchor is automatic CSS:
the table's scrollport in standalone, the page in inline. A small
useHorizontalScrollSync hook mirrors body scrollLeft onto the header
and turns wheel-on-header into pan-on-body.
Mirror the leftward padding with --embed-grid-pad-right = extendRight,
applied as padding-right on .grid. The user can now pan past the last
column into empty space — same shape as Notion's behavior. Standalone
full-page bases are unaffected (var unset → 0).
As a child of .grid with grid-column: 1/-1, the button took the full
row width and the sticky-left offset got swallowed by the grid layout
— it scrolled along with the cells instead of anchoring to the left.
Move it to a sibling of .grid inside .gridWrapper, restyle as a
small inline-flex chip with width: max-content. Sticky-left now
keeps it pinned to page-content-left while the user scrolls
horizontally, matching the toolbar.
Add padding-bottom: 6px on .gridWrapper and a small vertical margin
on the button so the horizontal scrollbar no longer overlaps it.
Add position: sticky + inset-inline-start to .addRowButton so the
'New row' affordance stays at the page-content edge while the user
scrolls horizontally — same shape as the toolbar staying put. The
sticky offset uses the existing --embed-grid-pad-left var: in embed
mode that's the leftward extension distance (so it sticks at
page-content-left); in standalone mode the var is 0 (sticks at the
grid's natural left edge, no visible effect when there's no scroll).
Add a body-color background and z-index: 4 so cells don't bleed
through during scroll.
The .grid had a 1px outer border + small radius drawing a rounded
rectangle around the whole table. In standalone full-page mode that
reads as a panel and is fine. In an inline embed it looked like a
floating box, especially with the leftward padded area inside the
border.
Make the outer border and radius CSS vars; the embed wrapper sets
them to none/0 so inline databases get only cell-level gridlines,
matching Notion's document-style rendering. Standalone bases get
the default 1px panel frame as before.
Bring back the leftward extension via negative margin-left so the
scroll viewport reaches AppShell.Main's left edge, then offset the
.grid with padding-left = extendLeft so the first cell still lines
up with page-content on load. The extended area becomes scrollable
empty space the user can pan into — same shape as Notion's inline
databases.
Done with one new CSS var (--embed-grid-pad-left) consumed by the
.grid in grid.module.css. Standalone full-page bases never set the
var, so it's a no-op there.
Pins the header row to the top of the gridWrapper scroll viewport, with
the pinned-left corner cell stacking above pinned body cells at the
top-left intersection. The trailing add-column button gets the same
sticky treatment so it doesn't drift away from the row.
Subtle bit: `position: sticky` confines an element to its containing
block. The previous subgrid `.headerRow` wrapper was 34px tall, so
sticky cells could only travel 34px before scrolling out with the
wrapper. Switching the wrapper to `display: contents` lets the cells
become direct grid children of `.grid` (full table height) so sticky
travel matches the scroll range. role="row" survives display:contents
in modern browsers.
Browser overflow-anchor silently bumped scrollTop by one page's worth
of pixels every time a new page of rows committed — anchoring on the
AddRowButton that sits below paddingBottom. This kept the near-bottom
threshold satisfied and re-fired onFetchNextPage indefinitely, even
after the user released the scrollbar. Disabling scroll anchoring on
the grid scroll container stops the browser from adjusting scrollTop
in response to content growth.