diff --git a/apps/web/components/share-modal.tsx b/apps/web/components/share-modal.tsx index 091a9446..f17ee4c7 100644 --- a/apps/web/components/share-modal.tsx +++ b/apps/web/components/share-modal.tsx @@ -18,6 +18,12 @@ import { useLocalStorageUsername } from "@hooks/use-local-storage-username" import { useHasCompanyBrain } from "@/hooks/use-company-brain" import { toast } from "sonner" import * as htmlToImage from "html-to-image" +import { + getShareAspectPreset, + SHARE_ASPECT_PRESETS, + type ShareAspect, + shareAspectMaxHeightDvh, +} from "@/lib/share-aspect" type BackgroundTheme = "gradient" | "dark-gradient" | "black" @@ -281,9 +287,11 @@ export function ShareModal({ const isCompanyBrain = useHasCompanyBrain() const [selectedTheme, setSelectedTheme] = useState("gradient") + const [selectedAspect, setSelectedAspect] = useState("post") const [isCopying, setIsCopying] = useState(false) const [copied, setCopied] = useState(false) const previewRef = useRef(null) + const aspectPreset = getShareAspectPreset(selectedAspect) const localStorageUsername = useLocalStorageUsername() const displayName = @@ -438,8 +446,10 @@ export function ShareModal({ {/* Preview area */}
@@ -493,6 +503,26 @@ export function ShareModal({ isSelected={selectedTheme === "black"} onClick={() => setSelectedTheme("black")} /> +
+
+ {SHARE_ASPECT_PRESETS.map((preset) => ( + + ))} +
{/* Action buttons */} diff --git a/apps/web/lib/share-aspect.test.ts b/apps/web/lib/share-aspect.test.ts new file mode 100644 index 00000000..548934be --- /dev/null +++ b/apps/web/lib/share-aspect.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "bun:test" +import { + getShareAspectPreset, + SHARE_ASPECT_PRESETS, + shareAspectMaxHeightDvh, + type ShareAspect, +} from "./share-aspect" + +describe("share-aspect presets", () => { + it("exposes post, square and story", () => { + expect(SHARE_ASPECT_PRESETS.map((p) => p.id)).toEqual([ + "post", + "square", + "story", + ]) + }) + + it("resolves each preset by id", () => { + for (const preset of SHARE_ASPECT_PRESETS) { + expect(getShareAspectPreset(preset.id)).toBe(preset) + } + }) + + it("falls back to post for an unknown aspect", () => { + expect(getShareAspectPreset("nope" as ShareAspect).id).toBe("post") + }) + + it("has coherent ratio strings and numeric values", () => { + expect(getShareAspectPreset("square").value).toBe(1) + expect(getShareAspectPreset("post").value).toBeGreaterThan(1) // landscape + expect(getShareAspectPreset("story").value).toBeLessThan(1) // portrait + }) + + it("lets the narrow portrait story use more vertical space than the wide post", () => { + expect(shareAspectMaxHeightDvh("story")).toBeGreaterThan( + shareAspectMaxHeightDvh("post"), + ) + }) +}) diff --git a/apps/web/lib/share-aspect.ts b/apps/web/lib/share-aspect.ts new file mode 100644 index 00000000..9e231f4e --- /dev/null +++ b/apps/web/lib/share-aspect.ts @@ -0,0 +1,40 @@ +/** + * Aspect-ratio presets for the graph share card, sized for where people post + * screenshots. Pure so the mapping can be unit tested. + */ + +export type ShareAspect = "post" | "square" | "story" + +export interface ShareAspectPreset { + id: ShareAspect + label: string + /** CSS `aspect-ratio` value (width / height). */ + ratio: string + /** Width / height as a number, for any numeric layout math. */ + value: number +} + +const POST_PRESET: ShareAspectPreset = { + id: "post", + label: "Post", + ratio: "674 / 505", + value: 674 / 505, +} + +export const SHARE_ASPECT_PRESETS: readonly ShareAspectPreset[] = [ + POST_PRESET, + { id: "square", label: "Square", ratio: "1 / 1", value: 1 }, + { id: "story", label: "Story", ratio: "9 / 16", value: 9 / 16 }, +] + +export function getShareAspectPreset(aspect: ShareAspect): ShareAspectPreset { + return SHARE_ASPECT_PRESETS.find((p) => p.id === aspect) ?? POST_PRESET +} + +/** + * A max-height (in dvh) that keeps the preview inside the dialog. Tall (story) + * ratios need to be shorter than wide ones so they don't overflow. + */ +export function shareAspectMaxHeightDvh(aspect: ShareAspect): number { + return aspect === "story" ? 60 : 48 +}