Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
777b59359c | ||
|
|
9284949b9a | ||
|
|
7591f7e313 | ||
|
|
011ce8e5a3 |
No files matched your search
@@ -46,6 +46,11 @@ export const ContainerEditModal = createModal<ModalProps>(({ actions, innerProps
|
||||
description={tSection("option.showOpenAll.description")}
|
||||
{...form.getInputProps("showOpenAll", { type: "checkbox" })}
|
||||
/>
|
||||
<Switch
|
||||
label={tSection("option.scrollable.label")}
|
||||
description={tSection("option.scrollable.description")}
|
||||
{...form.getInputProps("scrollable", { type: "checkbox" })}
|
||||
/>
|
||||
<TextMultiSelect
|
||||
label={tSection("container.option.customCssClasses.label")}
|
||||
{...form.getInputProps("customCssClasses")}
|
||||
|
||||
@@ -296,6 +296,21 @@ body[data-board-grid-interacting="resize"] .board-grid-entry[data-dnd-active="tr
|
||||
top: -0.625rem;
|
||||
left: -0.625rem;
|
||||
cursor: nwse-resize;
|
||||
/* Leaves a thin draggable border around the corner instead of a solid 44x44 hit area,
|
||||
so it doesn't sit on top of the container menu button anchored in this same corner. */
|
||||
clip-path: polygon(
|
||||
evenodd,
|
||||
0 0,
|
||||
100% 0,
|
||||
100% 100%,
|
||||
0 100%,
|
||||
0 0,
|
||||
4px 4px,
|
||||
4px 40px,
|
||||
40px 40px,
|
||||
40px 4px,
|
||||
4px 4px
|
||||
);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -91,7 +91,17 @@ export const GridPreviewLayer = ({
|
||||
|
||||
const viewport = grid.parentElement;
|
||||
if (viewport?.hasAttribute("data-section-id")) {
|
||||
viewport.style.setProperty("--board-grid-drag-height", `${getLogicalTrackSize(renderedRowCount)}px`);
|
||||
// This grows the viewport to match the inner grid while a drag needs extra room to show a
|
||||
// drop target (maxRowCount === null - a "main"/rail section without a fixed cap). A capped
|
||||
// section (e.g. a scrollable container, maxRowCount !== null) must never have this set: it
|
||||
// isn't just a temporary default here, this effect reruns on every board mutation - so a
|
||||
// stray value would silently blow the cap open for the rest of the session, well after any
|
||||
// drag ended, since nothing else would ever clear it back out.
|
||||
if (maxRowCount === null) {
|
||||
viewport.style.setProperty("--board-grid-drag-height", `${getLogicalTrackSize(renderedRowCount)}px`);
|
||||
} else {
|
||||
viewport.style.removeProperty("--board-grid-drag-height");
|
||||
}
|
||||
}
|
||||
}, [
|
||||
entryElements,
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Chained with .viewport (rather than a standalone .scrollableViewport rule) so this always
|
||||
wins on specificity, not source order. GridEditor loads as a separate async chunk in edit
|
||||
mode, and a board mutation can trigger its stylesheet to re-register - at equal specificity,
|
||||
that flips which of these two rules the cascade picks, silently un-capping the container even
|
||||
though its height (set inline, see section-grid.tsx) stays correct throughout. */
|
||||
.viewport.scrollableViewport {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
/* --board-grid-drag-height inherits down the DOM tree like any custom property. A capped
|
||||
section's own GridPreviewLayer effect correctly avoids setting it on itself (see
|
||||
grid-preview-layer.tsx), but that does nothing to stop it inheriting the value an
|
||||
*uncapped* ancestor section (e.g. the board's root section) legitimately sets on itself
|
||||
while growing during a drag happening anywhere else on the board - a nested scrollable
|
||||
container 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, which only kicks in when the property is entirely unset, not merely
|
||||
inherited. Reset it here so this element 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 */
|
||||
--board-grid-drag-height: initial;
|
||||
}
|
||||
|
||||
.staticGrid,
|
||||
.editorGrid {
|
||||
position: relative;
|
||||
|
||||
@@ -130,8 +130,13 @@ export const SectionGrid = ({
|
||||
const contentRowCount = Math.max(1, getLayoutRowCount(displayPlacements));
|
||||
const rowCount = Math.max(contentRowCount, requestedRowCount, minimumViewportRowCount);
|
||||
const maxRowCount = section.kind === "container" || railPlacement !== "main" ? rowCount : null;
|
||||
// A scrollable container isn't forced to grow with its content - it scrolls internally instead
|
||||
// of expanding to fit every widget, so its viewport height is capped independently of rowCount.
|
||||
const isScrollableContainer = section.kind === "container" && section.options.scrollable;
|
||||
const viewportRowCount = isScrollableContainer ? Math.max(requestedRowCount, 1) : rowCount;
|
||||
const logicalWidth = getLogicalTrackSize(columnCount);
|
||||
const logicalHeight = getLogicalTrackSize(rowCount);
|
||||
const viewportHeight = getLogicalTrackSize(viewportRowCount);
|
||||
// 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);
|
||||
@@ -190,6 +195,18 @@ export const SectionGrid = ({
|
||||
section,
|
||||
]);
|
||||
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
const previousItemIdsRef = useRef<Set<string> | 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 (
|
||||
<SectionProvider
|
||||
value={{
|
||||
@@ -206,18 +223,20 @@ export const SectionGrid = ({
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={viewportRef}
|
||||
{...canvasAttributes}
|
||||
className={combineClasses(classes.viewport, className)}
|
||||
className={combineClasses(classes.viewport, isScrollableContainer && classes.scrollableViewport, className)}
|
||||
style={
|
||||
{
|
||||
width: logicalWidth,
|
||||
height: `var(--board-grid-drag-height, ${logicalHeight}px)`,
|
||||
height: `var(--board-grid-drag-height, ${viewportHeight}px)`,
|
||||
"--board-item-radius": `var(--mantine-radius-${board.itemRadius})`,
|
||||
} as CSSProperties
|
||||
}
|
||||
data-section-id={section.id}
|
||||
data-section-kind={section.kind}
|
||||
data-rail-placement={railPlacement}
|
||||
data-scrollable={isScrollableContainer ? "true" : undefined}
|
||||
data-grid-interaction-disabled={isEditMode && isInteractionDisabled ? "true" : undefined}
|
||||
onPointerDownCapture={expandCollapsedSectionsForPointerEdit}
|
||||
onKeyDownCapture={expandCollapsedSectionsForKeyboardEdit}
|
||||
@@ -296,6 +315,11 @@ const getContainerMinimumSizes = (board: ReturnType<typeof useRequiredBoard>, 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<typeof useRequiredBoard>, 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);
|
||||
|
||||
@@ -1396,6 +1396,7 @@ describe("saveBoard should save full board", () => {
|
||||
showLabel: false,
|
||||
collapsible: false,
|
||||
showOpenAll: false,
|
||||
scrollable: false,
|
||||
});
|
||||
expect(
|
||||
await db.query.sectionLayouts.findFirst({
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -23,3 +23,15 @@
|
||||
.bookmarkButton:focus-visible .bookmarkIcon {
|
||||
background-color: var(--mantine-color-iconColor-filled-hover);
|
||||
}
|
||||
|
||||
.compactScrollViewport > div {
|
||||
display: flex !important;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.compactScrollContent {
|
||||
flex: 1;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
@@ -2,19 +2,7 @@
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
Anchor,
|
||||
Box,
|
||||
Card,
|
||||
Flex,
|
||||
Group,
|
||||
ScrollArea,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import { Anchor, Card, Flex, Group, ScrollArea, SimpleGrid, Stack, Text, Title, UnstyledButton } from "@mantine/core";
|
||||
import { IconLink } from "@tabler/icons-react";
|
||||
import combineClasses from "clsx";
|
||||
|
||||
@@ -42,6 +30,7 @@ export interface CompactBookmarkLayout {
|
||||
columns: number;
|
||||
hideHostname: boolean;
|
||||
hideTitle: boolean;
|
||||
minimumItemWidth: number;
|
||||
minimumItemSize: number;
|
||||
}
|
||||
|
||||
@@ -64,6 +53,7 @@ export function getCompactBookmarkLayout(
|
||||
columns,
|
||||
hideHostname: cellWidth < 120 || cellHeight < (usesHorizontalItems ? 52 : 96),
|
||||
hideTitle: cellWidth < 64 || cellHeight < 40,
|
||||
minimumItemWidth: targetWidth,
|
||||
minimumItemSize: usesHorizontalItems ? 48 : 96,
|
||||
};
|
||||
}
|
||||
@@ -139,12 +129,17 @@ export default function BookmarksWidget({
|
||||
/>
|
||||
)}
|
||||
{displayMode !== "advanced" && (
|
||||
<ScrollArea type="auto" scrollbarSize={6} offsetScrollbars style={{ flex: 1, minHeight: 0 }}>
|
||||
<Box mih="100%">
|
||||
<ScrollArea
|
||||
classNames={{ viewport: classes.compactScrollViewport }}
|
||||
type="auto"
|
||||
scrollbarSize={6}
|
||||
style={{ flex: 1, minHeight: 0 }}
|
||||
>
|
||||
<div className={classes.compactScrollContent}>
|
||||
{options.layout === "grid" || options.layout === "gridHorizontal" ? (
|
||||
<GridLayout
|
||||
data={data}
|
||||
columns={compactLayout.columns}
|
||||
minimumItemWidth={compactLayout.minimumItemWidth}
|
||||
minimumItemHeight={compactLayout.minimumItemSize}
|
||||
itemDirection={options.layout === "gridHorizontal" ? "horizontal" : "vertical"}
|
||||
hideTitle={compactHideTitle}
|
||||
@@ -167,7 +162,7 @@ export default function BookmarksWidget({
|
||||
hasIconColor={board.iconColor !== null}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -340,7 +335,7 @@ interface GridLayoutProps {
|
||||
withBorder: boolean;
|
||||
itemDirection: "horizontal" | "vertical";
|
||||
hasIconColor: boolean;
|
||||
columns: number;
|
||||
minimumItemWidth: number;
|
||||
minimumItemHeight: number;
|
||||
}
|
||||
|
||||
@@ -353,20 +348,13 @@ const GridLayout = ({
|
||||
withBorder,
|
||||
itemDirection,
|
||||
hasIconColor,
|
||||
columns,
|
||||
minimumItemWidth,
|
||||
minimumItemHeight,
|
||||
}: GridLayoutProps) => {
|
||||
const board = useRequiredBoard();
|
||||
|
||||
return (
|
||||
<SimpleGrid
|
||||
cols={columns}
|
||||
spacing={4}
|
||||
verticalSpacing={4}
|
||||
miw="100%"
|
||||
mih="100%"
|
||||
style={{ gridAutoRows: `minmax(${minimumItemHeight}px, 1fr)` }}
|
||||
>
|
||||
<Flex gap={4} wrap="wrap" miw="100%" mih="100%" style={{ alignContent: "stretch" }}>
|
||||
{data.map((app) => {
|
||||
const href = getSafeAppHref(app.href);
|
||||
return (
|
||||
@@ -377,7 +365,11 @@ const GridLayout = ({
|
||||
target={href ? (openNewTab ? "_blank" : "_self") : undefined}
|
||||
rel={href && openNewTab ? SAFE_NEW_TAB_REL : undefined}
|
||||
key={app.id}
|
||||
h="100%"
|
||||
style={{
|
||||
flex: `1 0 calc(${minimumItemWidth}px * var(--mantine-scale, 1))`,
|
||||
minWidth: 0,
|
||||
minHeight: `calc(${minimumItemHeight}px * var(--mantine-scale, 1))`,
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
h="100%"
|
||||
@@ -407,7 +399,7 @@ const GridLayout = ({
|
||||
</UnstyledButton>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user