feat: masks, properties refactor, shaders, storage migrations, and more

This commit is contained in:
Maze Winther
2026-03-29 15:48:22 +02:00
parent 39ea298a9c
commit 8db3bead13
690 changed files with 35618 additions and 7337 deletions
@@ -0,0 +1,24 @@
import { PlusSignIcon, RulerIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Button } from "@/components/ui/button";
import type { GuideDefinition } from "@/lib/guides/types";
function CustomGuideOptions() {
return (
<div className="flex gap-2">
<Button variant="outline" size="sm" className="flex-1">
<HugeiconsIcon icon={PlusSignIcon} />
Add guide line
</Button>
</div>
);
}
export const customGuide = {
id: "custom",
label: "Custom",
renderPreview: () => <HugeiconsIcon size={16} icon={RulerIcon} />,
renderTriggerIcon: () => <HugeiconsIcon icon={RulerIcon} />,
renderOverlay: () => null,
renderOptions: () => <CustomGuideOptions />,
} as const satisfies GuideDefinition;
@@ -0,0 +1,125 @@
import {
GridTableIcon,
LayoutThreeColumnIcon,
LayoutThreeRowIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { NumberField } from "@/components/ui/number-field";
import {
GRID_MIN,
GRID_MAX,
DEFAULT_GRID_CONFIG,
} from "@/constants/guide-constants";
import { usePreviewStore } from "@/stores/preview-store";
import { clampRound } from "@/utils/math";
import { cn } from "@/utils/ui";
import type { GuideDefinition } from "@/lib/guides/types";
function GridLines({
rows,
cols,
color,
}: {
rows: number;
cols: number;
color: string;
}) {
const verticals = Array.from(
{ length: cols - 1 },
(_, i) => ((i + 1) / cols) * 100,
);
const horizontals = Array.from(
{ length: rows - 1 },
(_, i) => ((i + 1) / rows) * 100,
);
return (
<>
{verticals.map((pct) => (
<div
key={`v-${pct}`}
className={cn("absolute top-0 bottom-0 w-px", color)}
style={{ left: `${pct}%` }}
/>
))}
{horizontals.map((pct) => (
<div
key={`h-${pct}`}
className={cn("absolute left-0 right-0 h-px", color)}
style={{ top: `${pct}%` }}
/>
))}
</>
);
}
function GridGuidePreview() {
return (
<div className="relative aspect-video w-full">
<GridLines rows={3} cols={4} color="bg-foreground/15" />
</div>
);
}
function GridGuideOverlay() {
const { rows, cols } = usePreviewStore((s) => s.gridConfig);
return (
<div className="absolute inset-0">
<GridLines rows={rows} cols={cols} color="bg-white/35" />
</div>
);
}
function GridGuideOptions() {
const rows = usePreviewStore((s) => s.gridConfig.rows);
const cols = usePreviewStore((s) => s.gridConfig.cols);
const setGridConfig = usePreviewStore((s) => s.setGridConfig);
const clampGridValue = (value: number) =>
clampRound({ value, min: GRID_MIN, max: GRID_MAX });
return (
<div className="flex gap-2">
<NumberField
icon={<HugeiconsIcon icon={LayoutThreeRowIcon} />}
value={rows}
min={GRID_MIN}
max={GRID_MAX}
isDefault={rows === DEFAULT_GRID_CONFIG.rows}
onReset={() => setGridConfig({ rows: DEFAULT_GRID_CONFIG.rows })}
onScrub={(value) => setGridConfig({ rows: clampGridValue(value) })}
onChange={(event) => {
const parsed = Number.parseInt(event.target.value, 10);
if (!Number.isNaN(parsed))
setGridConfig({ rows: clampGridValue(parsed) });
}}
className="flex-1"
/>
<NumberField
icon={<HugeiconsIcon icon={LayoutThreeColumnIcon} />}
value={cols}
min={GRID_MIN}
max={GRID_MAX}
isDefault={cols === DEFAULT_GRID_CONFIG.cols}
onReset={() => setGridConfig({ cols: DEFAULT_GRID_CONFIG.cols })}
onScrub={(value) => setGridConfig({ cols: clampGridValue(value) })}
onChange={(event) => {
const parsed = Number.parseInt(event.target.value, 10);
if (!Number.isNaN(parsed))
setGridConfig({ cols: clampGridValue(parsed) });
}}
className="flex-1"
/>
</div>
);
}
export const gridGuide = {
id: "grid",
label: "Grid",
renderPreview: () => <GridGuidePreview />,
renderTriggerIcon: () => <HugeiconsIcon icon={GridTableIcon} />,
renderOverlay: () => <GridGuideOverlay />,
renderOptions: () => <GridGuideOptions />,
} as const satisfies GuideDefinition;
@@ -0,0 +1,65 @@
import Image from "next/image";
import type { GuideDefinition } from "@/lib/guides/types";
import { TikTokLayout } from "./tiktok-layout";
function PlatformLogo({
domain,
className = "size-4",
}: {
domain: string;
className?: string;
}) {
return (
<Image
src={`https://cdn.brandfetch.io/${domain}/w/64/h/64`}
alt=""
width={18}
height={18}
className={className}
draggable={false}
unoptimized
/>
);
}
function PlatformGuidePreview({ domain }: { domain: string }) {
return <PlatformLogo domain={domain} />;
}
function platformGuide({
id,
label,
domain,
}: {
id: string;
label: string;
domain: string;
}): GuideDefinition {
return {
id,
label,
renderPreview: () => <PlatformGuidePreview domain={domain} />,
renderTriggerIcon: () => <PlatformLogo domain={domain} />,
renderOverlay: () => null,
};
}
export const tiktokGuide: GuideDefinition = {
...platformGuide({ id: "tiktok", label: "TikTok", domain: "tiktok.com" }),
renderOverlay: () => <TikTokLayout />,
};
export const igReelsGuide = platformGuide({
id: "ig-reels",
label: "Reels",
domain: "instagram.com",
});
export const ytShortsGuide = platformGuide({
id: "yt-shorts",
label: "Shorts",
domain: "youtube.com",
});
export const spotlightGuide = platformGuide({
id: "spotlight",
label: "Spotlight",
domain: "snapchat.com",
});
File diff suppressed because one or more lines are too long
+13
View File
@@ -0,0 +1,13 @@
import { GUIDE_REGISTRY } from "./registry";
import type { GuideDefinition } from "@/lib/guides/types";
export { GUIDE_REGISTRY, isGuideId } from "./registry";
export type { GuideDefinition, GuideId, GuideRenderProps } from "./registry";
export function getGuideById(guideId: string | null): GuideDefinition | null {
if (!guideId) {
return null;
}
return GUIDE_REGISTRY.find((guide) => guide.id === guideId) ?? null;
}
+28
View File
@@ -0,0 +1,28 @@
import type { GuideDefinition } from "@/lib/guides/types";
import { gridGuide } from "./definitions/grid";
// import { customGuide } from "./definitions/custom";
import {
tiktokGuide,
igReelsGuide,
ytShortsGuide,
spotlightGuide,
} from "./definitions/platforms";
export type { GuideDefinition, GuideRenderProps } from "@/lib/guides/types";
export const GUIDE_REGISTRY = [
gridGuide,
tiktokGuide,
igReelsGuide,
ytShortsGuide,
spotlightGuide,
// todo: wire up custom guide fully, then uncomment this:
// customGuide,
] as const satisfies readonly GuideDefinition[];
export type GuideId = (typeof GUIDE_REGISTRY)[number]["id"];
export function isGuideId(value: string): value is GuideId {
return GUIDE_REGISTRY.some((guide) => guide.id === value);
}
+20
View File
@@ -0,0 +1,20 @@
import type { ReactNode } from "react";
export interface GridConfig {
rows: number;
cols: number;
}
export interface GuideRenderProps {
width: number;
height: number;
}
export interface GuideDefinition {
id: string;
label: string;
renderPreview: () => ReactNode;
renderTriggerIcon: () => ReactNode;
renderOverlay: (props: GuideRenderProps) => ReactNode;
renderOptions?: () => ReactNode;
}