diff --git a/apps/client/src/features/base/components/base-table.tsx b/apps/client/src/features/base/components/base-table.tsx index 17a8b5250..2bcdb529a 100644 --- a/apps/client/src/features/base/components/base-table.tsx +++ b/apps/client/src/features/base/components/base-table.tsx @@ -43,9 +43,10 @@ import classes from "@/features/base/styles/grid.module.css"; type BaseTableProps = { pageId: string; + embedded?: boolean; }; -export function BaseTable({ pageId }: BaseTableProps) { +export function BaseTable({ pageId, embedded }: BaseTableProps) { const { t } = useTranslation(); // Subscribe to the base's realtime room so other clients' edits, // schema changes, and async-job completions reconcile into our cache. @@ -313,39 +314,75 @@ export function BaseTable({ pageId }: BaseTableProps) { if (!base) return null; + // When the table is embedded inline in a doc page, the parent + // measures the available area and exposes + // --embed-width / --embed-shift / --embed-pad. We extend the + // toolbar and grid sections out to the full available width via + // those vars, then re-pad the inner content so it visually aligns + // with page text. Sticky inset-inline-start keeps the toolbar + // pinned to the page-content edge during horizontal scroll. + const extendStyle = embedded + ? ({ + position: "relative", + width: "var(--embed-width, 100%)", + insetInlineStart: "var(--embed-shift, 0px)", + paddingInline: "var(--embed-pad, 0px)", + } as const) + : undefined; + + const stickyToolbarStyle = embedded + ? ({ + position: "sticky", + insetInlineStart: 0, + zIndex: 4, + } as const) + : undefined; + return ( -
- - - +
+
+ +
+ +
+
+
+ +
); } diff --git a/apps/client/src/features/editor/components/base-embed/base-embed-view.tsx b/apps/client/src/features/editor/components/base-embed/base-embed-view.tsx index c2f5e6359..f371dfb2c 100644 --- a/apps/client/src/features/editor/components/base-embed/base-embed-view.tsx +++ b/apps/client/src/features/editor/components/base-embed/base-embed-view.tsx @@ -1,10 +1,69 @@ -import { NodeViewWrapper, NodeViewProps } from '@tiptap/react'; -import { Box, Text } from '@mantine/core'; -import { BaseTable } from '@/features/base/components/base-table'; -import { useBaseQuery } from '@/features/base/queries/base-query'; +import { NodeViewWrapper, NodeViewProps } from "@tiptap/react"; +import { Box, Text } from "@mantine/core"; +import { useEffect, useRef } from "react"; +import { BaseTable } from "@/features/base/components/base-table"; +import { useBaseQuery } from "@/features/base/queries/base-query"; + +const SIDE_GUTTER = 8; + +// Walk up from `el` to find the closest ancestor that's meaningfully +// wider than `el` itself. That ancestor is the available drawing area +// our embed should expand into (e.g. AppShell.Main when the page sits +// inside a 900px Mantine Container). Returns null if no such ancestor +// exists — the embed then renders without extension. +function findWiderAncestor(el: HTMLElement): HTMLElement | null { + const baseWidth = el.getBoundingClientRect().width; + let cur: HTMLElement | null = el.parentElement; + while (cur && cur !== document.body) { + const w = cur.getBoundingClientRect().width; + if (w > baseWidth + 32) return cur; + cur = cur.parentElement; + } + return null; +} + +function applyExtension(wrapper: HTMLDivElement) { + const wrapperRect = wrapper.getBoundingClientRect(); + const wider = findWiderAncestor(wrapper); + if (!wider) { + wrapper.style.setProperty("--embed-shift", "0px"); + wrapper.style.setProperty("--embed-width", "100%"); + wrapper.style.setProperty("--embed-pad", "0px"); + return; + } + const widerRect = wider.getBoundingClientRect(); + const targetLeft = widerRect.left + SIDE_GUTTER; + const targetWidth = widerRect.width - SIDE_GUTTER * 2; + const shift = targetLeft - wrapperRect.left; + wrapper.style.setProperty("--embed-shift", `${shift}px`); + wrapper.style.setProperty("--embed-width", `${targetWidth}px`); + // Re-pad inner content back to the original wrapper bounds so + // toolbar buttons / first column visually align with page text. + wrapper.style.setProperty("--embed-pad", `${-shift}px`); +} export function BaseEmbedView({ node }: NodeViewProps) { const pageId = node.attrs.pageId as string | null; + const wrapperRef = useRef(null); + + useEffect(() => { + const wrapper = wrapperRef.current; + if (!wrapper) return; + + const update = () => applyExtension(wrapper); + update(); + + const ro = new ResizeObserver(update); + ro.observe(wrapper); + const wider = findWiderAncestor(wrapper); + if (wider) ro.observe(wider); + + window.addEventListener("resize", update); + return () => { + ro.disconnect(); + window.removeEventListener("resize", update); + }; + }, []); if (!pageId) { return ( @@ -40,9 +99,9 @@ export function BaseEmbedView({ node }: NodeViewProps) { return ( - - - +
+ +
); }