fix(board): refine container spacing and collapse layout (#6833)
This commit is contained in:
1 parent
9bc3802b3b
commit
ff2b589380
5 files changed
+82
-57
No files matched your search
@@ -11,6 +11,7 @@ import type { ContainerSectionItem } from "~/app/[locale]/boards/_types";
|
||||
import { COLLAPSED_SECTION_ROW_COUNT } from "~/components/board/layout";
|
||||
import { SectionGrid } from "./grid/section-grid";
|
||||
import { useSectionCollapse } from "./section-collapse";
|
||||
import { useSectionContext } from "./section-context";
|
||||
import { useOpenSectionApps } from "./use-open-section-apps";
|
||||
import classes from "./item.module.css";
|
||||
|
||||
@@ -25,6 +26,7 @@ interface Props {
|
||||
|
||||
export const BoardContainerSection = ({ section }: Props) => {
|
||||
const board = useRequiredBoard();
|
||||
const { section: parentSection } = useSectionContext();
|
||||
const [isEditMode] = useEditMode();
|
||||
const t = useI18n("section.container");
|
||||
const tSection = useI18n("section");
|
||||
@@ -39,8 +41,26 @@ export const BoardContainerSection = ({ section }: Props) => {
|
||||
});
|
||||
const label = options.title.trim() || t("untitled");
|
||||
const contentId = `board-container-${section.id}-content`;
|
||||
const labelLeft = 8;
|
||||
const labelRight = isEditMode ? 48 : options.showOpenAll ? 40 : 8;
|
||||
let labelLeft = 8;
|
||||
let labelRight = 8;
|
||||
if (isEditMode) {
|
||||
// Match the root/nested menu offsets in BoardContainerMenu, plus its width and gap.
|
||||
labelLeft = 36;
|
||||
if (parentSection.kind === "container") labelLeft = 68;
|
||||
} else if (options.showOpenAll) {
|
||||
labelRight = 40;
|
||||
}
|
||||
// Expanded controls sit on the border like the ordinary label, without reserving a header row.
|
||||
const toggleLayout = isVisuallyCollapsed
|
||||
? { top: 0, left: 0, w: "100%", h: "100%", maw: "100%" }
|
||||
: {
|
||||
top: "calc(var(--mantine-spacing-xs) * -1)",
|
||||
left: labelLeft,
|
||||
w: "auto",
|
||||
h: 20,
|
||||
maw: `calc(100% - ${labelLeft + labelRight}px)`,
|
||||
};
|
||||
const toggleIcon = isVisuallyCollapsed ? <IconChevronDown size={16} /> : <IconChevronUp size={16} />;
|
||||
|
||||
return (
|
||||
<Box className="board-grid-item-content" data-grid-item-content w="100%" h="100%" style={{ overflow: "visible" }}>
|
||||
@@ -68,24 +88,14 @@ export const BoardContainerSection = ({ section }: Props) => {
|
||||
<Button
|
||||
className={classes.containerToggle}
|
||||
pos="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
w="100%"
|
||||
h={isVisuallyCollapsed ? "100%" : 24}
|
||||
px={12}
|
||||
pe={isEditMode ? 48 : options.showOpenAll ? 40 : 12}
|
||||
{...toggleLayout}
|
||||
px={6}
|
||||
ps={isVisuallyCollapsed ? labelLeft : 6}
|
||||
pe={isVisuallyCollapsed ? labelRight : 6}
|
||||
radius="sm"
|
||||
variant="default"
|
||||
justify={options.showLabel ? "flex-start" : "center"}
|
||||
leftSection={
|
||||
options.showLabel ? (
|
||||
isVisuallyCollapsed ? (
|
||||
<IconChevronDown size={16} />
|
||||
) : (
|
||||
<IconChevronUp size={16} />
|
||||
)
|
||||
) : undefined
|
||||
}
|
||||
leftSection={options.showLabel && toggleIcon}
|
||||
onClick={toggle}
|
||||
aria-expanded={!isVisuallyCollapsed}
|
||||
aria-controls={contentId}
|
||||
@@ -94,13 +104,7 @@ export const BoardContainerSection = ({ section }: Props) => {
|
||||
data-board-container-label
|
||||
title={options.showLabel ? label : undefined}
|
||||
>
|
||||
{options.showLabel ? (
|
||||
label
|
||||
) : isVisuallyCollapsed ? (
|
||||
<IconChevronDown size={16} />
|
||||
) : (
|
||||
<IconChevronUp size={16} />
|
||||
)}
|
||||
{options.showLabel ? label : toggleIcon}
|
||||
</Button>
|
||||
)}
|
||||
{!isVisuallyCollapsed && !options.collapsible && options.showLabel && options.title && (
|
||||
|
||||
@@ -133,6 +133,21 @@ body[data-board-grid-interacting="resize"] .board-grid-entry[data-dnd-active="tr
|
||||
background: color-mix(in srgb, var(--mantine-primary-color-filled) 10%, transparent);
|
||||
}
|
||||
|
||||
.board-grid-placeholder[data-grid-placeholder-type="section"] {
|
||||
clip-path: none;
|
||||
background: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.board-grid-placeholder[data-grid-placeholder-type="section"]::after {
|
||||
position: absolute;
|
||||
inset: var(--board-container-inset, 0px);
|
||||
content: "";
|
||||
background: color-mix(in srgb, var(--mantine-primary-color-filled) 16%, transparent);
|
||||
border: 2px dashed var(--mantine-primary-color-filled);
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.board-grid-resize-outline {
|
||||
z-index: 1002;
|
||||
clip-path: inset(var(--board-grid-item-inset, 0px));
|
||||
|
||||
@@ -136,6 +136,7 @@ export const GridPreviewLayer = ({
|
||||
style={getLogicalItemStyle(targetPlacement)}
|
||||
data-grid-placeholder-for={interaction?.activeId}
|
||||
data-grid-placeholder-mode={interaction?.mode}
|
||||
data-grid-placeholder-type={targetPlacement.type}
|
||||
data-grid-x={targetPlacement.x}
|
||||
data-grid-y={targetPlacement.y}
|
||||
data-grid-w={targetPlacement.w}
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
while growing during a drag happening anywhere else on the board - any container (not just
|
||||
a scrollable one - a non-scrollable, e.g. collapsible, container is just as capped) sits
|
||||
inside that section's DOM tree, so the stray value cascades straight through and silently
|
||||
defeats the `var(--board-grid-drag-height, ...px)` fallback in section-grid.tsx (including
|
||||
its collapsibleHeaderInset adjustment), which only kicks in when the property is entirely
|
||||
unset, not merely inherited. Reset it here so every container can never inherit a stray
|
||||
defeats the `var(--board-grid-drag-height, ...px)` fallback in section-grid.tsx, which only
|
||||
kicks in when the property is entirely unset, not merely inherited. Reset it here so every
|
||||
container can never inherit a stray
|
||||
value from anywhere above it, no matter what any ancestor's own drag state is doing.
|
||||
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables#custom_properties_and_inheritance */
|
||||
.viewport[data-section-kind="container"] {
|
||||
@@ -62,9 +62,9 @@
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Containers provide their own inner item gap, so their card must keep the full grid aspect ratio. */
|
||||
/* SectionGrid fits its content to the same inset in both view and edit mode. */
|
||||
.staticItem[data-type="section"] > .contentMount {
|
||||
inset: 0;
|
||||
inset: var(--board-container-inset, 0px);
|
||||
}
|
||||
|
||||
.staticItem[data-type="item"] > .contentMount {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties, KeyboardEvent, PointerEvent } from "react";
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { Box } from "@mantine/core";
|
||||
import combineClasses from "clsx";
|
||||
|
||||
@@ -29,6 +29,9 @@ import { useSectionItems } from "../use-section-items";
|
||||
import { useBoardGridPortalHost } from "./grid-portal-host";
|
||||
import classes from "./section-grid.module.css";
|
||||
|
||||
const GridContentScaleContext = createContext(1);
|
||||
const CONTAINER_CARD_INSET = 5;
|
||||
|
||||
interface SectionGridProps {
|
||||
section: Exclude<Section, { kind: "container" }> | ContainerSectionItem;
|
||||
columnCount: number;
|
||||
@@ -50,6 +53,7 @@ export const SectionGrid = ({
|
||||
}: SectionGridProps) => {
|
||||
const [isEditMode] = useEditMode();
|
||||
const canvasScale = useBoardCanvasScale();
|
||||
const parentContentScale = useContext(GridContentScaleContext);
|
||||
const editorRuntimeStatus = useGridEditorRuntimeStatus();
|
||||
const editorRegistry = useGridEditorRegistry();
|
||||
const editorHostRef = useRef<HTMLDivElement>(null);
|
||||
@@ -143,36 +147,37 @@ export const SectionGrid = ({
|
||||
const isScrollableContainer = section.kind === "container" && section.options.scrollable;
|
||||
const viewportRowCount =
|
||||
viewportRowCountOverride ?? (isScrollableContainer ? Math.max(requestedRowCount, 1) : rowCount);
|
||||
const effectiveCanvasScale = Number.isFinite(canvasScale) && canvasScale > 0 ? canvasScale : 1;
|
||||
// The collapsible label keeps a fixed physical height while the grid uses logical canvas pixels.
|
||||
// Convert that height so the label reserves the same space at every board scale.
|
||||
const collapsibleHeaderInset =
|
||||
section.kind === "container" && section.options.collapsible ? CONTAINER_HEADER_HEIGHT / effectiveCanvasScale : 0;
|
||||
const parentScale = canvasScale * parentContentScale;
|
||||
const effectiveCanvasScale = Number.isFinite(parentScale) && parentScale > 0 ? parentScale : 1;
|
||||
// Match the card's inset without changing its persisted grid footprint. Include ancestor
|
||||
// container zoom so equally sized nested containers keep distinct borders at every depth.
|
||||
let outerCardInset = 0;
|
||||
if (section.kind === "container") {
|
||||
outerCardInset = (2 * CONTAINER_CARD_INSET) / effectiveCanvasScale;
|
||||
}
|
||||
const fullGridWidth = getLogicalGridSize(columnCount);
|
||||
const fullGridHeight = getLogicalGridSize(rowCount);
|
||||
const logicalWidth = fullGridWidth;
|
||||
const viewportHeight = Math.max(1, getLogicalGridSize(viewportRowCount) - collapsibleHeaderInset);
|
||||
// For a scrollable container, rowCount covers *all* content rows, not just the visible card -
|
||||
// fullGridHeight grows right along with it, so a ratio built from it drifts toward 1 as content
|
||||
// grows regardless of the (fixed) inset, under-scaling relative to what the actually-visible
|
||||
// viewport needs and clipping the bottom of the visible rows. viewportHeight/viewportRowCount
|
||||
// describe the real visible card size in both cases (they equal the non-scrollable values when
|
||||
// rowCount and viewportRowCount are the same), so use those instead.
|
||||
const fullViewportHeight = getLogicalGridSize(viewportRowCount);
|
||||
// A collapsible container reserves a fixed-height label above the square-cell grid. Keep the
|
||||
// nested content uniformly scaled within the remaining height so labels and icons retain their
|
||||
// proportions; ordinary containers use the original unscaled, edge-aligned grid geometry.
|
||||
const containerContentScale =
|
||||
section.kind === "container" && fullGridWidth > 0 && fullViewportHeight > 0
|
||||
? Math.max(0.01, Math.min(logicalWidth / fullGridWidth, viewportHeight / fullViewportHeight, 1))
|
||||
: 1;
|
||||
const logicalWidth = Math.max(1, fullGridWidth - outerCardInset);
|
||||
const viewportHeight = Math.max(1, fullViewportHeight - outerCardInset);
|
||||
// Fit square cells into the inset card. Scrollable grids use the visible row count,
|
||||
// not their full content height. Collapse controls never change this geometry.
|
||||
let containerContentScale = 1;
|
||||
if (section.kind === "container" && fullGridWidth > 0 && fullViewportHeight > 0) {
|
||||
containerContentScale = Math.max(
|
||||
0.01,
|
||||
Math.min(logicalWidth / fullGridWidth, viewportHeight / fullViewportHeight, 1),
|
||||
);
|
||||
}
|
||||
const contentScale = parentContentScale * containerContentScale;
|
||||
const effectiveContentScale = effectiveCanvasScale * containerContentScale;
|
||||
// Compute the combined value in JS rather than a CSS calc() referencing the existing
|
||||
// --board-canvas-ui-scale: custom properties declared on the *same* element don't have a
|
||||
// sequential/temporal order the way normal variables do, so any calc() on this element that
|
||||
// both reads and writes --board-canvas-ui-scale (even indirectly, through another property)
|
||||
// is a circular reference - CSS invalidates the whole group rather than using "the old value",
|
||||
// silently breaking every icon/text/custom-CSS size that compensates off it for descendants.
|
||||
const combinedUiScale = calculateBoardUiScale(canvasScale) / containerContentScale;
|
||||
const combinedUiScale = calculateBoardUiScale(canvasScale) / contentScale;
|
||||
// A collapsed container's compact coordinates are display-only. Its own
|
||||
// nested grid stays inactive until an explicit edit interaction expands it.
|
||||
const isInteractionDisabled = section.kind === "container" && collapsedSectionIds.has(section.id);
|
||||
@@ -268,9 +273,9 @@ export const SectionGrid = ({
|
||||
{
|
||||
width: logicalWidth,
|
||||
height: `var(--board-grid-drag-height, ${viewportHeight}px)`,
|
||||
marginTop: collapsibleHeaderInset || undefined,
|
||||
"--board-item-radius": `var(--mantine-radius-${board.itemRadius})`,
|
||||
"--board-grid-content-scale": containerContentScale,
|
||||
"--board-container-inset": `${CONTAINER_CARD_INSET / effectiveContentScale}px`,
|
||||
} as CSSProperties
|
||||
}
|
||||
data-section-id={section.id}
|
||||
@@ -289,14 +294,17 @@ export const SectionGrid = ({
|
||||
height: fullGridHeight,
|
||||
zoom: containerContentScale,
|
||||
margin: containerContentScale < 1 ? "0 auto" : undefined,
|
||||
...(containerContentScale < 1 ? { "--board-canvas-ui-scale": combinedUiScale } : {}),
|
||||
"--board-canvas-inverse-scale": 1 / effectiveContentScale,
|
||||
"--board-canvas-ui-scale": combinedUiScale,
|
||||
} as CSSProperties
|
||||
}
|
||||
data-grid-section-id={section.id}
|
||||
data-kind={section.kind}
|
||||
data-grid-editor-error={isEditMode && editorRuntimeStatus === "error" ? "true" : undefined}
|
||||
>
|
||||
<SectionContent />
|
||||
<GridContentScaleContext.Provider value={contentScale}>
|
||||
<SectionContent />
|
||||
</GridContentScaleContext.Provider>
|
||||
</Box>
|
||||
<div ref={editorHostRef} className={classes.editorPortalHost} />
|
||||
</Box>
|
||||
@@ -304,9 +312,6 @@ export const SectionGrid = ({
|
||||
);
|
||||
};
|
||||
|
||||
// Matches the collapsible container toggle's h={24} in container-section.tsx.
|
||||
const CONTAINER_HEADER_HEIGHT = 24;
|
||||
|
||||
const INTERACTIVE_GRID_SELECTOR =
|
||||
'a,button,input,textarea,select,option,[contenteditable="true"],[role="button"],[data-grid-no-drag]';
|
||||
const EDIT_ACTIVATION_KEYS = new Set(["Enter", " ", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"]);
|
||||
|
||||
Reference in new issue
Block a user