Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions e2e/critical-workflows.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,12 @@ test.describe.serial("critical local prototype workflows", () => {
await expect(
page.getByRole("button", { name: "Create Geometric collage background" })
).toBeVisible()
await expect(
page.getByRole("button", { name: "Create Sunset arches background" })
).toBeVisible()
await expect(
page.getByRole("button", { name: "Create Postcard frame background" })
).toBeVisible()

await page.getByRole("button", { name: "Create Geometric collage background" }).click()
await expect(page.getByRole("heading", { name: "Choose a background" })).not.toBeVisible()
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ function BackgroundPicker({ onCreate }: { onCreate: (preset: BackgroundPreset) =
Decorative elements start locked and can be unlocked in the editor.
</DialogDescription>
</DialogHeader>
<div className="grid gap-3 sm:grid-cols-3">
<div className="grid gap-3 sm:grid-cols-2">
{BACKGROUND_PRESETS.map((preset) => (
<Button
key={preset.id}
Expand Down
28 changes: 26 additions & 2 deletions src/domain/layout-backgrounds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import { layoutSchemaValidator } from "./layout.ts"
import { BACKGROUND_PRESETS, backgroundSchema } from "./layout-backgrounds.ts"

describe("layout background presets", () => {
it("provides a blank page and a visual-only geometric collage", () => {
expect(BACKGROUND_PRESETS.map(({ id }) => id)).toEqual(["blank", "geometric-collage"])
it("provides a blank page and visual-only decorated pages", () => {
expect(BACKGROUND_PRESETS.map(({ id }) => id)).toEqual([
"blank",
"geometric-collage",
"sunset-arches",
"postcard-frame",
])

for (const preset of BACKGROUND_PRESETS) {
expect(layoutSchemaValidator.safeParse(preset.schema).success).toBe(true)
Expand Down Expand Up @@ -33,4 +38,23 @@ describe("layout background presets", () => {
})
expect(first.elements.map(({ id }) => id)).not.toEqual(second.elements.map(({ id }) => id))
})

// Rotation is intentionally avoided in the newer presets: the browser rotates around an
// element's top-left corner and the PDF export around its bottom-left, so a rotated preset
// would not print the way it previews.
it.each(["sunset-arches", "postcard-frame"] as const)(
"keeps %s axis-aligned and inside the printable media area",
(presetId) => {
const { elements } = backgroundSchema(presetId)

expect(elements.length).toBeGreaterThan(0)
for (const { geometry } of elements) {
expect(geometry.rotation).toBe(0)
expect(geometry.x).toBeGreaterThanOrEqual(-3)
expect(geometry.y).toBeGreaterThanOrEqual(-3)
expect(geometry.x + geometry.width).toBeLessThanOrEqual(213)
expect(geometry.y + geometry.height).toBeLessThanOrEqual(151)
}
}
)
})
188 changes: 187 additions & 1 deletion src/domain/layout-backgrounds.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { emptyLayoutSchema } from "./layout.ts"
import { type LayoutSchema, type ShapeElement } from "./types.ts"

export const BACKGROUND_PRESET_IDS = ["blank", "geometric-collage"] as const
export const BACKGROUND_PRESET_IDS = [
"blank",
"geometric-collage",
"sunset-arches",
"postcard-frame",
] as const

export type BackgroundPresetId = (typeof BACKGROUND_PRESET_IDS)[number]

Expand Down Expand Up @@ -156,6 +161,177 @@ const geometricCollage: LayoutSchema = {
],
}

// Axis-aligned only: rotated shapes rotate around the top-left corner in the browser and around the
// bottom-left corner in the exported PDF, so a rotation-free composition looks identical everywhere.
const sunsetArches: LayoutSchema = {
...emptyLayoutSchema(),
background: "#fdf4e6",
elements: [
shape({
id: "dusk-band",
type: "rectangle",
geometry: { x: -3, y: 112, width: 216, height: 39, rotation: 0 },
fill: "#f0d3ab",
}),
shape({
id: "horizon-rule",
type: "rectangle",
geometry: { x: -3, y: 109.4, width: 216, height: 0.8, rotation: 0 },
opacity: 0.5,
fill: "#a9713f",
}),
shape({
id: "sun",
type: "circle",
geometry: { x: 118, y: 20, width: 52, height: 52, rotation: 0 },
opacity: 0.9,
fill: "#f2b95c",
}),
shape({
id: "tall-arch-crown",
type: "circle",
geometry: { x: 146, y: 44, width: 48, height: 48, rotation: 0 },
fill: "#c25f45",
}),
shape({
id: "tall-arch-body",
type: "rectangle",
geometry: { x: 146, y: 68, width: 48, height: 46, rotation: 0 },
fill: "#c25f45",
}),
shape({
id: "short-arch-crown",
type: "circle",
geometry: { x: 110, y: 66, width: 32, height: 32, rotation: 0 },
fill: "#4f7f72",
}),
shape({
id: "short-arch-body",
type: "rectangle",
geometry: { x: 110, y: 82, width: 32, height: 32, rotation: 0 },
fill: "#4f7f72",
}),
shape({
id: "band-dot-red",
type: "circle",
geometry: { x: 14, y: 126, width: 8, height: 8, rotation: 0 },
fill: "#c25f45",
}),
shape({
id: "band-dot-teal",
type: "circle",
geometry: { x: 26, y: 126, width: 8, height: 8, rotation: 0 },
fill: "#4f7f72",
}),
shape({
id: "band-dot-cream",
type: "circle",
geometry: { x: 38, y: 126, width: 8, height: 8, rotation: 0 },
fill: "#fdf4e6",
}),
],
}

// Rules are thin filled rectangles rather than stroked outlines: the PDF renderer paints a shape
// fill of "transparent" as black, so an unfilled frame would only look right in the browser.
const postcardFrame: LayoutSchema = {
...emptyLayoutSchema(),
background: "#fffaf2",
elements: [
shape({
id: "frame-top",
type: "rectangle",
geometry: { x: 7, y: 7, width: 196, height: 0.7, rotation: 0 },
fill: "#8a7256",
}),
shape({
id: "frame-bottom",
type: "rectangle",
geometry: { x: 7, y: 140.3, width: 196, height: 0.7, rotation: 0 },
fill: "#8a7256",
}),
shape({
id: "frame-left",
type: "rectangle",
geometry: { x: 7, y: 7, width: 0.7, height: 134, rotation: 0 },
fill: "#8a7256",
}),
shape({
id: "frame-right",
type: "rectangle",
geometry: { x: 202.3, y: 7, width: 0.7, height: 134, rotation: 0 },
fill: "#8a7256",
}),
shape({
id: "inner-frame-top",
type: "rectangle",
geometry: { x: 10, y: 10, width: 190, height: 0.35, rotation: 0 },
fill: "#d8c8ae",
}),
shape({
id: "inner-frame-bottom",
type: "rectangle",
geometry: { x: 10, y: 137.65, width: 190, height: 0.35, rotation: 0 },
fill: "#d8c8ae",
}),
shape({
id: "inner-frame-left",
type: "rectangle",
geometry: { x: 10, y: 10, width: 0.35, height: 128, rotation: 0 },
fill: "#d8c8ae",
}),
shape({
id: "inner-frame-right",
type: "rectangle",
geometry: { x: 199.65, y: 10, width: 0.35, height: 128, rotation: 0 },
fill: "#d8c8ae",
}),
shape({
id: "message-divider",
type: "rectangle",
geometry: { x: 118, y: 20, width: 0.4, height: 108, rotation: 0 },
fill: "#d8c8ae",
}),
shape({
id: "stamp-block",
type: "rectangle",
geometry: { x: 170, y: 16, width: 26, height: 30, rotation: 0 },
fill: "#e8d7bc",
}),
shape({
id: "stamp-inner",
type: "rectangle",
geometry: { x: 173, y: 19, width: 20, height: 24, rotation: 0 },
fill: "#c8836a",
}),
shape({
id: "postmark",
type: "circle",
geometry: { x: 158, y: 14, width: 22, height: 22, rotation: 0 },
opacity: 0.6,
fill: "#7f9d94",
}),
shape({
id: "address-rule-top",
type: "rectangle",
geometry: { x: 132, y: 104, width: 56, height: 0.4, rotation: 0 },
fill: "#d8c8ae",
}),
shape({
id: "address-rule-middle",
type: "rectangle",
geometry: { x: 132, y: 114, width: 56, height: 0.4, rotation: 0 },
fill: "#d8c8ae",
}),
shape({
id: "address-rule-bottom",
type: "rectangle",
geometry: { x: 132, y: 124, width: 56, height: 0.4, rotation: 0 },
fill: "#d8c8ae",
}),
],
}

export const BACKGROUND_PRESETS: BackgroundPreset[] = [
{
id: "blank",
Expand All @@ -167,6 +343,16 @@ export const BACKGROUND_PRESETS: BackgroundPreset[] = [
name: "Geometric collage",
schema: geometricCollage,
},
{
id: "sunset-arches",
name: "Sunset arches",
schema: sunsetArches,
},
{
id: "postcard-frame",
name: "Postcard frame",
schema: postcardFrame,
},
]

export function backgroundSchema(presetId: BackgroundPresetId): LayoutSchema {
Expand Down
3 changes: 2 additions & 1 deletion src/lib/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { PostHog } from "posthog-js"

import type { BackgroundPresetId } from "#/domain/layout-backgrounds.ts"
import type { PageProblem } from "#/domain/types.ts"
import { isDemoMode } from "#/lib/demo-mode.ts"

Expand All @@ -19,7 +20,7 @@ let userKnown = false

interface AnalyticsEvents {
"layout_editor:background_created": {
background_id: "blank" | "geometric-collage"
background_id: BackgroundPresetId
}
"layout_editor:answer_label_edit": {
cleared: boolean
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.