mirror of
https://github.com/docmost/docmost.git
synced 2026-07-25 07:14:43 +10:00
feat(base): use negative margin to extend inline grid past parent
The previous approach (position: relative + explicit width + inset-inline-start) didn't physically grow the box in some flex contexts, so the grid stayed at the parent's width. Switch to negative margin-left / margin-right on the grid wrapper only — with width: auto, the rendered width becomes parent + |margin|, extending the box past the parent without any positioning hacks. The toolbar keeps its natural parent-constrained width (no extension) so it stays aligned with the page text above. Two CSS vars, --embed-extend-l / --embed-extend-r, are computed on mount + on ResizeObserver from the wrapper and the closest wider ancestor.
This commit is contained in:
@@ -314,27 +314,16 @@ export function BaseTable({ pageId, embedded }: BaseTableProps) {
|
|||||||
|
|
||||||
if (!base) return null;
|
if (!base) return null;
|
||||||
|
|
||||||
// When the table is embedded inline in a doc page, the parent
|
// When embedded inline in a doc page, the parent <NodeViewWrapper>
|
||||||
// <NodeViewWrapper> measures the available area and exposes
|
// exposes --embed-extend-l / --embed-extend-r (positive px values).
|
||||||
// --embed-width / --embed-shift / --embed-pad. We extend the
|
// We pull the grid's left/right edges outward via negative margin —
|
||||||
// toolbar and grid sections out to the full available width via
|
// box-model: width: auto becomes parent_width + extend, so the box
|
||||||
// those vars, then re-pad the inner content so it visually aligns
|
// physically grows past its parent's bounds. The toolbar keeps the
|
||||||
// with page text. Sticky inset-inline-start keeps the toolbar
|
// parent-constrained width so it stays in line with the page text.
|
||||||
// pinned to the page-content edge during horizontal scroll.
|
const gridExtendStyle = embedded
|
||||||
const extendStyle = embedded
|
|
||||||
? ({
|
? ({
|
||||||
position: "relative",
|
marginLeft: "calc(-1 * var(--embed-extend-l, 0px))",
|
||||||
width: "var(--embed-width, 100%)",
|
marginRight: "calc(-1 * var(--embed-extend-r, 0px))",
|
||||||
insetInlineStart: "var(--embed-shift, 0px)",
|
|
||||||
paddingInline: "var(--embed-pad, 0px)",
|
|
||||||
} as const)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const stickyToolbarStyle = embedded
|
|
||||||
? ({
|
|
||||||
position: "sticky",
|
|
||||||
insetInlineStart: 0,
|
|
||||||
zIndex: 4,
|
|
||||||
} as const)
|
} as const)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@@ -346,29 +335,25 @@ export function BaseTable({ pageId, embedded }: BaseTableProps) {
|
|||||||
height: embedded ? "auto" : "100%",
|
height: embedded ? "auto" : "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={extendStyle}>
|
<BaseViewDraftBanner
|
||||||
<BaseViewDraftBanner
|
isDirty={isDirty}
|
||||||
isDirty={isDirty}
|
canSave={canSave}
|
||||||
canSave={canSave}
|
onReset={resetDraft}
|
||||||
onReset={resetDraft}
|
onSave={handleSaveDraft}
|
||||||
onSave={handleSaveDraft}
|
saving={updateViewMutation.isPending}
|
||||||
saving={updateViewMutation.isPending}
|
/>
|
||||||
/>
|
<BaseToolbar
|
||||||
<div style={stickyToolbarStyle}>
|
base={base}
|
||||||
<BaseToolbar
|
activeView={effectiveView}
|
||||||
base={base}
|
views={views}
|
||||||
activeView={effectiveView}
|
table={table}
|
||||||
views={views}
|
onViewChange={handleViewChange}
|
||||||
table={table}
|
onAddView={handleAddView}
|
||||||
onViewChange={handleViewChange}
|
onPersistViewConfig={persistViewConfig}
|
||||||
onAddView={handleAddView}
|
onDraftSortsChange={handleDraftSortsChange}
|
||||||
onPersistViewConfig={persistViewConfig}
|
onDraftFiltersChange={handleDraftFiltersChange}
|
||||||
onDraftSortsChange={handleDraftSortsChange}
|
/>
|
||||||
onDraftFiltersChange={handleDraftFiltersChange}
|
<div style={gridExtendStyle}>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={extendStyle}>
|
|
||||||
<GridContainer
|
<GridContainer
|
||||||
table={table}
|
table={table}
|
||||||
properties={base.properties}
|
properties={base.properties}
|
||||||
|
|||||||
@@ -4,43 +4,40 @@ import { useEffect, useRef } from "react";
|
|||||||
import { BaseTable } from "@/features/base/components/base-table";
|
import { BaseTable } from "@/features/base/components/base-table";
|
||||||
import { useBaseQuery } from "@/features/base/queries/base-query";
|
import { useBaseQuery } from "@/features/base/queries/base-query";
|
||||||
|
|
||||||
const SIDE_GUTTER = 8;
|
const RIGHT_GUTTER = 16;
|
||||||
|
const LEFT_GUTTER = 8;
|
||||||
|
|
||||||
// Walk up from `el` to find the closest ancestor that's meaningfully
|
// Measure how far we can extend the grid past the wrapper's natural
|
||||||
// wider than `el` itself. That ancestor is the available drawing area
|
// (parent-constrained) bounds, and write the values as CSS vars on the
|
||||||
// our embed should expand into (e.g. AppShell.Main when the page sits
|
// wrapper so the descendant grid can consume them via margin.
|
||||||
// inside a 900px Mantine Container). Returns null if no such ancestor
|
function applyExtension(wrapper: HTMLDivElement) {
|
||||||
// exists — the embed then renders without extension.
|
const rect = wrapper.getBoundingClientRect();
|
||||||
function findWiderAncestor(el: HTMLElement): HTMLElement | null {
|
if (rect.width === 0) return;
|
||||||
const baseWidth = el.getBoundingClientRect().width;
|
|
||||||
let cur: HTMLElement | null = el.parentElement;
|
const extendRight = Math.max(
|
||||||
|
0,
|
||||||
|
window.innerWidth - rect.right - RIGHT_GUTTER,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the leftmost the grid can reach: walk up the ancestor chain
|
||||||
|
// for the closest element wider than the wrapper. That ancestor's
|
||||||
|
// left edge (plus a small gutter) is our left target. This handles
|
||||||
|
// the sidebar-collapsed case naturally — the wider ancestor is
|
||||||
|
// AppShell.Main, whose left edge moves when the sidebar toggles.
|
||||||
|
let targetLeft = rect.left;
|
||||||
|
let cur: HTMLElement | null = wrapper.parentElement;
|
||||||
while (cur && cur !== document.body) {
|
while (cur && cur !== document.body) {
|
||||||
const w = cur.getBoundingClientRect().width;
|
const r = cur.getBoundingClientRect();
|
||||||
if (w > baseWidth + 32) return cur;
|
if (r.width > rect.width + 32) {
|
||||||
|
targetLeft = r.left + LEFT_GUTTER;
|
||||||
|
break;
|
||||||
|
}
|
||||||
cur = cur.parentElement;
|
cur = cur.parentElement;
|
||||||
}
|
}
|
||||||
return null;
|
const extendLeft = Math.max(0, rect.left - targetLeft);
|
||||||
}
|
|
||||||
|
|
||||||
function applyExtension(wrapper: HTMLDivElement) {
|
wrapper.style.setProperty("--embed-extend-r", `${extendRight}px`);
|
||||||
const wrapperRect = wrapper.getBoundingClientRect();
|
wrapper.style.setProperty("--embed-extend-l", `${extendLeft}px`);
|
||||||
if (wrapperRect.width === 0) return;
|
|
||||||
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) {
|
export function BaseEmbedView({ node }: NodeViewProps) {
|
||||||
@@ -57,8 +54,17 @@ export function BaseEmbedView({ node }: NodeViewProps) {
|
|||||||
|
|
||||||
const ro = new ResizeObserver(update);
|
const ro = new ResizeObserver(update);
|
||||||
ro.observe(wrapper);
|
ro.observe(wrapper);
|
||||||
const wider = findWiderAncestor(wrapper);
|
// Also observe an ancestor so sidebar collapse / window changes
|
||||||
if (wider) ro.observe(wider);
|
// propagate even when the wrapper itself doesn't resize.
|
||||||
|
let cur: HTMLElement | null = wrapper.parentElement;
|
||||||
|
while (cur && cur !== document.body) {
|
||||||
|
const r = cur.getBoundingClientRect();
|
||||||
|
if (r.width > wrapper.getBoundingClientRect().width + 32) {
|
||||||
|
ro.observe(cur);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cur = cur.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", update);
|
window.addEventListener("resize", update);
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user