Skip to content

Commit 185ca50

Browse files
committed
fix(quoteforge[studio]): make custom size usable and match deck preview to active slide
1 parent a1ac012 commit 185ca50

7 files changed

Lines changed: 100 additions & 7 deletions

File tree

src/__tests__/preview-route.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ describe("previewRoute", () => {
3131
expect(res.status).toBe(400);
3232
});
3333

34+
test("returns 400 for a custom-size card with width 0", async () => {
35+
const req = makeRequest({
36+
card: makeCard({ width: 0, height: 600 }),
37+
theme: "brutal-white",
38+
size: "custom",
39+
});
40+
const res = await previewRoute(req, new URL("http://localhost/api/preview"));
41+
expect(res.status).toBe(400);
42+
});
43+
3444
test("resolves custom width/height instead of the 0x0 sentinel", async () => {
3545
const req = makeRequest({
3646
card: makeCard({ width: 800, height: 600 }),

src/renderer/dimensions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ export function resolveDimensions(input: SizedContent): Dimensions {
1717
if (input.width === undefined || input.height === undefined) {
1818
throw new Error('size "custom" requires both "width" and "height"');
1919
}
20+
if (!Number.isInteger(input.width) || input.width < 1 || input.width > 8000) {
21+
throw new Error('size "custom" requires "width" to be an integer between 1 and 8000');
22+
}
23+
if (!Number.isInteger(input.height) || input.height < 1 || input.height > 8000) {
24+
throw new Error('size "custom" requires "height" to be an integer between 1 and 8000');
25+
}
2026
return { w: input.width, h: input.height };
2127
}
2228

studio/src/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ function StudioApp() {
167167
fitContent={fitContent}
168168
onThemeChange={mode === "card" ? cardStore.setTheme : deckStore.setDeckTheme}
169169
onSizeChange={mode === "card" ? cardStore.setSize : deckStore.setDeckSize}
170+
width={mode === "card" ? cardStore.card.width : deckStore.deck.defaults.width}
171+
height={mode === "card" ? cardStore.card.height : deckStore.deck.defaults.height}
172+
onDimensionsChange={mode === "card" ? cardStore.setDimensions : deckStore.setDeckDimensions}
170173
onAlignChange={mode === "card" ? cardStore.setAlign : deckStore.setDeckAlign}
171174
onFitContentChange={setFitContent}
172175
onExportPng={handleExportPng}
@@ -212,7 +215,7 @@ function StudioApp() {
212215
<PreviewPane
213216
card={previewCard}
214217
theme={currentTheme}
215-
size={currentSize}
218+
size={previewCard.size}
216219
slideIndex={mode === "deck" ? deckStore.activeSlideIndex : 0}
217220
slideTotal={mode === "deck" ? deckStore.deck.slides.length : 1}
218221
showCounter={mode === "deck" ? deckStore.showCounter : false}

studio/src/components/Editor/Toolbar.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ interface ToolbarProps {
99
mode: ContentMode;
1010
theme: string;
1111
size: SizeName;
12+
width?: number;
13+
height?: number;
1214
align: Align | undefined;
1315
fitContent: boolean;
1416
onThemeChange: (name: string) => void;
1517
onSizeChange: (size: SizeName) => void;
18+
onDimensionsChange: (width: number, height: number) => void;
1619
onAlignChange: (align: Align) => void;
1720
onFitContentChange: (fitContent: boolean) => void;
1821
onExportPng: () => void;
@@ -25,8 +28,8 @@ interface ToolbarProps {
2528
}
2629

2730
export function Toolbar({
28-
mode, theme, size, align, fitContent,
29-
onThemeChange, onSizeChange, onAlignChange, onFitContentChange,
31+
mode, theme, size, width, height, align, fitContent,
32+
onThemeChange, onSizeChange, onDimensionsChange, onAlignChange, onFitContentChange,
3033
onExportPng, onExportDeck,
3134
onUndo, onRedo, canUndo, canRedo,
3235
isDirty,
@@ -40,6 +43,31 @@ export function Toolbar({
4043

4144
<ThemePicker current={theme} onChange={onThemeChange} />
4245
<SizePicker current={size} onChange={onSizeChange} mode={mode} />
46+
{size === "custom" && (
47+
<div className="flex items-center gap-1">
48+
<input
49+
type="number"
50+
className="w-16 px-1.5 py-1 text-xs bg-neutral-800 border border-neutral-700 rounded text-neutral-200"
51+
value={width ?? ""}
52+
onChange={(e) => {
53+
const w = Number(e.target.value);
54+
if (e.target.value !== "" && Number.isFinite(w) && w > 0) onDimensionsChange(w, height ?? 0);
55+
}}
56+
title="Width"
57+
/>
58+
<span className="text-neutral-600 text-xs">×</span>
59+
<input
60+
type="number"
61+
className="w-16 px-1.5 py-1 text-xs bg-neutral-800 border border-neutral-700 rounded text-neutral-200"
62+
value={height ?? ""}
63+
onChange={(e) => {
64+
const h = Number(e.target.value);
65+
if (e.target.value !== "" && Number.isFinite(h) && h > 0) onDimensionsChange(width ?? 0, h);
66+
}}
67+
title="Height"
68+
/>
69+
</div>
70+
)}
4371
<AlignPicker current={align} onChange={onAlignChange} />
4472

4573
<div className="w-px h-5 bg-neutral-700" />

studio/src/components/Preview/PreviewPane.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ export function PreviewPane({ card, theme, size, slideIndex = 0, slideTotal = 1,
2020
const [scale, setScale] = useState(0.5);
2121

2222
const sizeInfo = SIZES[size];
23-
const cardW = sizeInfo?.w || 1200;
24-
const cardH = sizeInfo?.h || 675;
23+
const cardW = size === "custom" ? card.width || 1200 : sizeInfo?.w || 1200;
24+
const cardH = size === "custom" ? card.height || 675 : sizeInfo?.h || 675;
2525

2626
useEffect(() => {
2727
function updateScale() {

studio/src/store/cardStore.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { create } from "zustand";
22
import type { CardContent, Block, BlockType, SizeName, Align } from "../types";
3+
import { SIZES } from "../types";
34

45
interface CardStore {
56
card: CardContent;
@@ -19,6 +20,7 @@ interface CardStore {
1920
selectBlock: (id: string | null) => void;
2021
setTheme: (name: string) => void;
2122
setSize: (size: SizeName) => void;
23+
setDimensions: (width: number, height: number) => void;
2224
setAlign: (align: Align) => void;
2325
setZoom: (zoom: number) => void;
2426
undo: () => void;
@@ -125,8 +127,29 @@ export const useCardStore = create<CardStore>((set) => ({
125127
})),
126128

127129
setSize: (size) =>
130+
set((s) => {
131+
let card: CardContent = { ...s.card, size };
132+
if (size === "custom") {
133+
if (card.width === undefined || card.height === undefined) {
134+
const dims = SIZES[s.card.size];
135+
card.width = dims.w || 1200;
136+
card.height = dims.h || 675;
137+
}
138+
} else {
139+
delete card.width;
140+
delete card.height;
141+
}
142+
return {
143+
card,
144+
isDirty: true,
145+
past: [...s.past.slice(-49), s.card],
146+
future: [],
147+
};
148+
}),
149+
150+
setDimensions: (width, height) =>
128151
set((s) => ({
129-
card: { ...s.card, size },
152+
card: { ...s.card, width, height },
130153
isDirty: true,
131154
past: [...s.past.slice(-49), s.card],
132155
future: [],

studio/src/store/deckStore.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { create } from "zustand";
22
import type { DeckContent, Block, BlockType, SizeName, Slide, Align } from "../types";
3+
import { SIZES } from "../types";
34

45
interface DeckStore {
56
deck: DeckContent;
@@ -27,6 +28,7 @@ interface DeckStore {
2728
selectBlock: (id: string | null) => void;
2829
setDeckTheme: (name: string) => void;
2930
setDeckSize: (size: SizeName) => void;
31+
setDeckDimensions: (width: number, height: number) => void;
3032
setDeckAlign: (align: Align) => void;
3133
toggleCounter: () => void;
3234
setZoom: (zoom: number) => void;
@@ -241,8 +243,29 @@ export const useDeckStore = create<DeckStore>((set) => ({
241243
})),
242244

243245
setDeckSize: (size) =>
246+
set((s) => {
247+
const defaults = { ...s.deck.defaults, size };
248+
if (size === "custom") {
249+
if (defaults.width === undefined || defaults.height === undefined) {
250+
const dims = SIZES[s.deck.defaults.size];
251+
defaults.width = dims.w || 1200;
252+
defaults.height = dims.h || 675;
253+
}
254+
} else {
255+
delete defaults.width;
256+
delete defaults.height;
257+
}
258+
return {
259+
deck: { ...s.deck, defaults },
260+
isDirty: true,
261+
past: snapshot(s.past, s.deck),
262+
future: [],
263+
};
264+
}),
265+
266+
setDeckDimensions: (width, height) =>
244267
set((s) => ({
245-
deck: { ...s.deck, defaults: { ...s.deck.defaults, size } },
268+
deck: { ...s.deck, defaults: { ...s.deck.defaults, width, height } },
246269
isDirty: true,
247270
past: snapshot(s.past, s.deck),
248271
future: [],

0 commit comments

Comments
 (0)