From 8db3bead13441a5882d8689dfc6a9f09c82375d9 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Mar 2026 16:47:22 +0100 Subject: feat: masks, properties refactor, shaders, storage migrations, and more --- apps/web/src/lib/guides/definitions/custom.tsx | 24 ++ apps/web/src/lib/guides/definitions/grid.tsx | 125 ++++++++ apps/web/src/lib/guides/definitions/platforms.tsx | 65 ++++ .../src/lib/guides/definitions/tiktok-layout.tsx | 335 +++++++++++++++++++++ apps/web/src/lib/guides/index.ts | 13 + apps/web/src/lib/guides/registry.tsx | 28 ++ apps/web/src/lib/guides/types.ts | 20 ++ 7 files changed, 610 insertions(+) create mode 100644 apps/web/src/lib/guides/definitions/custom.tsx create mode 100644 apps/web/src/lib/guides/definitions/grid.tsx create mode 100644 apps/web/src/lib/guides/definitions/platforms.tsx create mode 100644 apps/web/src/lib/guides/definitions/tiktok-layout.tsx create mode 100644 apps/web/src/lib/guides/index.ts create mode 100644 apps/web/src/lib/guides/registry.tsx create mode 100644 apps/web/src/lib/guides/types.ts (limited to 'apps/web/src/lib/guides') diff --git a/apps/web/src/lib/guides/definitions/custom.tsx b/apps/web/src/lib/guides/definitions/custom.tsx new file mode 100644 index 00000000..bc92cb84 --- /dev/null +++ b/apps/web/src/lib/guides/definitions/custom.tsx @@ -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 ( +
+ +
+ ); +} + +export const customGuide = { + id: "custom", + label: "Custom", + renderPreview: () => , + renderTriggerIcon: () => , + renderOverlay: () => null, + renderOptions: () => , +} as const satisfies GuideDefinition; diff --git a/apps/web/src/lib/guides/definitions/grid.tsx b/apps/web/src/lib/guides/definitions/grid.tsx new file mode 100644 index 00000000..0e7a2004 --- /dev/null +++ b/apps/web/src/lib/guides/definitions/grid.tsx @@ -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) => ( +
+ ))} + {horizontals.map((pct) => ( +
+ ))} + + ); +} + +function GridGuidePreview() { + return ( +
+ +
+ ); +} + +function GridGuideOverlay() { + const { rows, cols } = usePreviewStore((s) => s.gridConfig); + + return ( +
+ +
+ ); +} + +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 ( +
+ } + 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" + /> + } + 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" + /> +
+ ); +} + +export const gridGuide = { + id: "grid", + label: "Grid", + renderPreview: () => , + renderTriggerIcon: () => , + renderOverlay: () => , + renderOptions: () => , +} as const satisfies GuideDefinition; diff --git a/apps/web/src/lib/guides/definitions/platforms.tsx b/apps/web/src/lib/guides/definitions/platforms.tsx new file mode 100644 index 00000000..ffeec1c0 --- /dev/null +++ b/apps/web/src/lib/guides/definitions/platforms.tsx @@ -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 ( + + ); +} + +function PlatformGuidePreview({ domain }: { domain: string }) { + return ; +} + +function platformGuide({ + id, + label, + domain, +}: { + id: string; + label: string; + domain: string; +}): GuideDefinition { + return { + id, + label, + renderPreview: () => , + renderTriggerIcon: () => , + renderOverlay: () => null, + }; +} + +export const tiktokGuide: GuideDefinition = { + ...platformGuide({ id: "tiktok", label: "TikTok", domain: "tiktok.com" }), + renderOverlay: () => , +}; +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", +}); diff --git a/apps/web/src/lib/guides/definitions/tiktok-layout.tsx b/apps/web/src/lib/guides/definitions/tiktok-layout.tsx new file mode 100644 index 00000000..794d1379 --- /dev/null +++ b/apps/web/src/lib/guides/definitions/tiktok-layout.tsx @@ -0,0 +1,335 @@ +export function TikTokLayout() { + return ( + + TikTok Layout + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/web/src/lib/guides/index.ts b/apps/web/src/lib/guides/index.ts new file mode 100644 index 00000000..007e6fda --- /dev/null +++ b/apps/web/src/lib/guides/index.ts @@ -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; +} diff --git a/apps/web/src/lib/guides/registry.tsx b/apps/web/src/lib/guides/registry.tsx new file mode 100644 index 00000000..60dec428 --- /dev/null +++ b/apps/web/src/lib/guides/registry.tsx @@ -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); +} diff --git a/apps/web/src/lib/guides/types.ts b/apps/web/src/lib/guides/types.ts new file mode 100644 index 00000000..340101ac --- /dev/null +++ b/apps/web/src/lib/guides/types.ts @@ -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; +} -- cgit v1.3.1