From 7591f7e3133a721618ae31c84baf1c6d4cfc8ce9 Mon Sep 17 00:00:00 2001 From: Bergasha Date: Sat, 22 Aug 2026 17:24:52 +1000 Subject: [PATCH] feat(v2 Containers): scrollable containers (#6629) --- .../container/container-edit-modal.tsx | 5 ++++ .../board/sections/grid/grid-editor.css | 15 ++++++++++ .../sections/grid/grid-preview-layer.tsx | 12 +++++++- .../sections/grid/section-grid.module.css | 22 ++++++++++++++ .../board/sections/grid/section-grid.tsx | 30 +++++++++++++++++-- packages/api/src/router/test/board.spec.ts | 1 + packages/translation/src/lang/en.json | 4 +++ packages/validation/src/board.spec.ts | 2 ++ packages/validation/src/shared.ts | 2 ++ 9 files changed, 89 insertions(+), 4 deletions(-) diff --git a/apps/nextjs/src/components/board/sections/container/container-edit-modal.tsx b/apps/nextjs/src/components/board/sections/container/container-edit-modal.tsx index 579bf1ad5..e4bc0d4cc 100644 --- a/apps/nextjs/src/components/board/sections/container/container-edit-modal.tsx +++ b/apps/nextjs/src/components/board/sections/container/container-edit-modal.tsx @@ -46,6 +46,11 @@ export const ContainerEditModal = createModal(({ actions, innerProps description={tSection("option.showOpenAll.description")} {...form.getInputProps("showOpenAll", { type: "checkbox" })} /> + (null); + const previousItemIdsRef = useRef | null>(null); + useEffect(() => { + const currentIds = new Set([...items, ...innerSections].map((item) => item.id)); + const previousIds = previousItemIdsRef.current; + const hasNewItem = previousIds !== null && [...currentIds].some((id) => !previousIds.has(id)); + previousItemIdsRef.current = currentIds; + if (hasNewItem && isScrollableContainer) { + viewportRef.current?.scrollTo({ top: viewportRef.current.scrollHeight, behavior: "smooth" }); + } + }, [innerSections, isScrollableContainer, items]); + return ( , la if (visiting.has(sectionId)) return { width: 1, height: 1 }; visiting.add(sectionId); + const section = board.sections.find((candidate) => candidate.id === sectionId); + // A scrollable container isn't forced to grow with its content - it scrolls internally instead, + // so it shouldn't have a content-derived height floor imposed by its parent's grid. + const isScrollable = section?.kind === "container" && section.options.scrollable; + const itemBounds = directItemsBySectionId.get(sectionId) ?? []; const sectionBounds = (directSectionsBySectionId.get(sectionId) ?? []).map(({ id, placement }) => { const minimum = resolve(id); @@ -308,7 +332,7 @@ const getContainerMinimumSizes = (board: ReturnType, la const children = [...itemBounds, ...sectionBounds]; const minimum = { width: Math.max(1, ...children.map((child) => child.xOffset + child.width)), - height: Math.max(1, ...children.map((child) => child.yOffset + child.height)), + height: isScrollable ? 1 : Math.max(1, ...children.map((child) => child.yOffset + child.height)), }; visiting.delete(sectionId); minimumBySectionId.set(sectionId, minimum); diff --git a/packages/api/src/router/test/board.spec.ts b/packages/api/src/router/test/board.spec.ts index 536a1361a..c194dd5ca 100644 --- a/packages/api/src/router/test/board.spec.ts +++ b/packages/api/src/router/test/board.spec.ts @@ -1396,6 +1396,7 @@ describe("saveBoard should save full board", () => { showLabel: false, collapsible: false, showOpenAll: false, + scrollable: false, }); expect( await db.query.sectionLayouts.findFirst({ diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 1957636a0..65b423d4f 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -1943,6 +1943,10 @@ "showOpenAll": { "label": "Show open-all action", "description": "Show an action that opens every app in this container in new tabs." + }, + "scrollable": { + "label": "Scrollable", + "description": "Keep this container at its set size and scroll its contents instead of growing to fit them." } }, "action": { diff --git a/packages/validation/src/board.spec.ts b/packages/validation/src/board.spec.ts index 27b973e53..e854e16cd 100644 --- a/packages/validation/src/board.spec.ts +++ b/packages/validation/src/board.spec.ts @@ -75,6 +75,7 @@ describe("section behavior validation", () => { showLabel: false, collapsible: true, showOpenAll: true, + scrollable: true, }, layouts: [], }); @@ -88,6 +89,7 @@ describe("section behavior validation", () => { showLabel: false, collapsible: true, showOpenAll: true, + scrollable: true, }, }); }); diff --git a/packages/validation/src/shared.ts b/packages/validation/src/shared.ts index 6a6a3e92d..3a0d82de3 100644 --- a/packages/validation/src/shared.ts +++ b/packages/validation/src/shared.ts @@ -61,6 +61,7 @@ export const containerSectionOptionsDefaults = { showLabel: true, collapsible: false, showOpenAll: false, + scrollable: false, } as const; export const containerSectionOptionsSchema = z @@ -71,6 +72,7 @@ export const containerSectionOptionsSchema = z showLabel: z.boolean().default(containerSectionOptionsDefaults.showLabel), collapsible: z.boolean().default(containerSectionOptionsDefaults.collapsible), showOpenAll: z.boolean().default(containerSectionOptionsDefaults.showOpenAll), + scrollable: z.boolean().default(containerSectionOptionsDefaults.scrollable), }) .default(containerSectionOptionsDefaults);