feat: shared tile look for cards and pages, plus new page pieces in SDK 1.1.0

This commit is contained in:
LukeGus committed 2026-11-04 18:57:18 -06:00
1 parent 1867be3f58
commit 19bdd589f3
41 files changed
+1497 -207

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
/* eslint-disable react-refresh/only-export-components */
import {
createContext,
useContext,
useLayoutEffect,
useRef,
useState,
type ReactNode,
} from "react";
import { cn } from "@/lib/utils";
export function scrollParent(element: HTMLElement | null) {
for (let node = element?.parentElement; node; node = node.parentElement) {
const { overflowY } = getComputedStyle(node);
if (overflowY === "auto" || overflowY === "scroll") return node;
}
return null;
}
/** True inside a TileStack or TileColumn section, which draws its own line. */
const EdgeOwnedContext = createContext(false);
export function EdgeOwned({ children }: { children: ReactNode }) {
return (
<EdgeOwnedContext.Provider value={true}>
{children}
</EdgeOwnedContext.Provider>
);
}
/**
* Whether the element's end reaches the bottom of the visible scroll area,
* where a closing line would sit on the screen edge and only add noise.
*/
function useReachesBottom(ref: React.RefObject<HTMLElement | null>) {
const [reaches, setReaches] = useState(false);
const reachesRef = useRef(reaches);
reachesRef.current = reaches;
useLayoutEffect(() => {
const element = ref.current;
const scroller = scrollParent(element);
if (!element || !scroller) return;
// Measured without the line itself, so showing it never flips the answer.
const update = () => {
const end =
element.getBoundingClientRect().bottom -
scroller.getBoundingClientRect().top +
scroller.scrollTop -
(reachesRef.current ? 0 : 1);
setReaches(end >= scroller.clientHeight - 0.5);
};
update();
const observer = new ResizeObserver(update);
observer.observe(scroller);
observer.observe(element);
return () => observer.disconnect();
}, [ref]);
return reaches;
}
/**
* Wraps tiles whose last row draws a line under itself. The line stays when
* there is room below and is clipped when the tiles reach the bottom of the
* screen, the same rule the tile grid uses. Inside a stack section the
* section owns the line, so it is always clipped.
*/
export function BottomEdge({
className,
innerClassName,
children,
}: {
className?: string;
innerClassName?: string;
children: ReactNode;
}) {
const owned = useContext(EdgeOwnedContext);
const ref = useRef<HTMLDivElement>(null);
const reaches = useReachesBottom(ref);
return (
<div ref={ref} className={cn("min-w-0 overflow-clip", className)}>
<div className={cn((owned || reaches) && "-mb-px", innerClassName)}>
{children}
</div>
</div>
);
}