@akairos/typeset-studio 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -464,6 +464,7 @@ export function findTemplateByKeyOrName(key: string): TypesetConfig | null {
464
464
  // Scan all templates on disk
465
465
  const files = readdirSync(TEMPLATES_DIR).filter((f) => f.endsWith(".json"));
466
466
 
467
+ const templates: TypesetConfig[] = [];
467
468
  for (const file of files) {
468
469
  try {
469
470
  const content = readFileSync(join(TEMPLATES_DIR, file), "utf-8");
@@ -471,25 +472,14 @@ export function findTemplateByKeyOrName(key: string): TypesetConfig | null {
471
472
  if (tpl.id?.toLowerCase() === lowerKey || tpl.name.toLowerCase() === lowerKey) {
472
473
  return tpl;
473
474
  }
475
+ templates.push(tpl);
474
476
  } catch {
475
477
  // ignore
476
478
  }
477
479
  }
478
480
 
479
- // Partial match by name
480
- for (const file of files) {
481
- try {
482
- const content = readFileSync(join(TEMPLATES_DIR, file), "utf-8");
483
- const tpl = JSON.parse(content) as TypesetConfig;
484
- if (tpl.name.toLowerCase().includes(lowerKey)) {
485
- return tpl;
486
- }
487
- } catch {
488
- // ignore
489
- }
490
- }
491
-
492
- return null;
481
+ // 精确匹配优先,名称模糊匹配复用已读取的模板。
482
+ return templates.find((tpl) => tpl.name.toLowerCase().includes(lowerKey)) ?? null;
493
483
  }
494
484
 
495
485
  export const templatesRouter = new Hono();
package/src/web/App.tsx CHANGED
@@ -1,5 +1,4 @@
1
1
  import React, { useEffect, useRef, useState } from "react";
2
- import { toPng } from "html-to-image";
3
2
  import { AuthModal } from "./components/AuthModal";
4
3
  import { CanvasViewport } from "./components/CanvasViewport";
5
4
  import { EditorSidebar } from "./components/EditorSidebar";
@@ -197,7 +196,6 @@ export const App: React.FC = () => {
197
196
  }
198
197
  }
199
198
  if (exportSucceeded) {
200
- setIsExporting(false);
201
199
  return;
202
200
  }
203
201
  }
@@ -56,6 +56,26 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = ({
56
56
  const file = e.target.files?.[0];
57
57
  if (!file) return;
58
58
 
59
+ // 上传失败时仍可用本地图片继续编辑。
60
+ const addUploadedImage = (src: string) => onAddImage({
61
+ src,
62
+ alt: file.name,
63
+ caption: "",
64
+ x: 40,
65
+ y: 400,
66
+ width: 440,
67
+ height: 300,
68
+ captionHeight: 36,
69
+ layoutPreset: "auto",
70
+ });
71
+ const readLocalImage = () => {
72
+ const reader = new FileReader();
73
+ reader.onload = () => {
74
+ if (typeof reader.result === "string") addUploadedImage(reader.result);
75
+ };
76
+ reader.readAsDataURL(file);
77
+ };
78
+
59
79
  setIsUploading(true);
60
80
  try {
61
81
  const formData = new FormData();
@@ -69,56 +89,12 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = ({
69
89
 
70
90
  const data = await res.json();
71
91
  if (data.ok && data.url) {
72
- onAddImage({
73
- src: data.url,
74
- alt: file.name,
75
- caption: "",
76
- x: 40,
77
- y: 400,
78
- width: 440,
79
- height: 300,
80
- captionHeight: 36,
81
- layoutPreset: "auto",
82
- });
92
+ addUploadedImage(data.url);
83
93
  } else {
84
- // Fallback to FileReader base64
85
- const reader = new FileReader();
86
- reader.onload = (event) => {
87
- if (event.target?.result) {
88
- onAddImage({
89
- src: event.target.result as string,
90
- alt: file.name,
91
- caption: "",
92
- x: 40,
93
- y: 400,
94
- width: 440,
95
- height: 300,
96
- captionHeight: 36,
97
- layoutPreset: "auto",
98
- });
99
- }
100
- };
101
- reader.readAsDataURL(file);
94
+ readLocalImage();
102
95
  }
103
96
  } catch {
104
- // Fallback to base64
105
- const reader = new FileReader();
106
- reader.onload = (event) => {
107
- if (event.target?.result) {
108
- onAddImage({
109
- src: event.target.result as string,
110
- alt: file.name,
111
- caption: "",
112
- x: 40,
113
- y: 400,
114
- width: 440,
115
- height: 300,
116
- captionHeight: 36,
117
- layoutPreset: "auto",
118
- });
119
- }
120
- };
121
- reader.readAsDataURL(file);
97
+ readLocalImage();
122
98
  } finally {
123
99
  setIsUploading(false);
124
100
  if (fileInputRef.current) fileInputRef.current.value = "";
@@ -375,7 +351,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = ({
375
351
  <select
376
352
  value={img.layoutPreset || "auto"}
377
353
  onChange={(e) =>
378
- onUpdateImage(img.id, { layoutPreset: e.target.value as any })
354
+ onUpdateImage(img.id, { layoutPreset: e.target.value as FlowImage["layoutPreset"] })
379
355
  }
380
356
  className="w-full bg-white dark:bg-[#181818] border border-[#d4d4d4] dark:border-[#262626] rounded px-1.5 py-1 text-black dark:text-white text-[11px] focus:outline-none"
381
357
  >
@@ -52,7 +52,7 @@ export const Header: React.FC<HeaderProps> = ({
52
52
  </span>
53
53
  <span>Typeset Studio</span>
54
54
  </div>
55
- <span className="text-xs text-[#888888] dark:text-[#555555] font-mono hidden md:inline">v0.1.0</span>
55
+ <span className="text-xs text-[#888888] dark:text-[#555555] font-mono hidden md:inline">v0.2.2</span>
56
56
  <div className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-[#f5f5f5] dark:bg-[#111111] border border-[#e5e5e5] dark:border-[#222222] text-[11px] text-emerald-600 dark:text-[#00e676]">
57
57
  <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 dark:bg-[#00e676] animate-pulse" />
58
58
  <span className="font-mono">Ready</span>
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useState } from "react";
2
- import { Check, FolderPlus, Trash2, X, Sparkles, Star, Tag, ChevronDown } from "lucide-react";
2
+ import { Check, FolderPlus, Trash2, X, Sparkles, Star } from "lucide-react";
3
3
  import { TemplateSlot, TypesetConfig } from "../../core/types";
4
4
 
5
5
  interface TemplateModalProps {
@@ -20,12 +20,6 @@ interface SlotsResponse {
20
20
  };
21
21
  }
22
22
 
23
- const SLOT_LABELS: Record<TemplateSlot, string> = {
24
- article_cover: "文章封面 (2.35:1)",
25
- newspic_cover: "贴图封面 (3:4)",
26
- newspic_content: "贴图内容 (3:4)",
27
- };
28
-
29
23
  export const TemplateModal: React.FC<TemplateModalProps> = ({
30
24
  isOpen,
31
25
  onClose,
@@ -114,7 +108,6 @@ export const TemplateModal: React.FC<TemplateModalProps> = ({
114
108
  id: undefined, // auto generate
115
109
  name: templateName.trim(),
116
110
  slot: saveSlot !== "none" ? saveSlot : undefined,
117
- images: currentConfig.images ? [...currentConfig.images] : undefined,
118
111
  };
119
112
 
120
113
  const res = await fetch("/api/templates", {
@@ -192,7 +185,7 @@ export const TemplateModal: React.FC<TemplateModalProps> = ({
192
185
  />
193
186
  <select
194
187
  value={saveSlot}
195
- onChange={(e) => setSaveSlot(e.target.value as any)}
188
+ onChange={(e) => setSaveSlot(e.target.value as TemplateSlot | "none")}
196
189
  className="bg-white dark:bg-[#0a0a0a] border border-[#d4d4d4] dark:border-[#2a2a2a] rounded px-2 py-1.5 text-xs text-black dark:text-white focus:outline-none focus:border-black dark:focus:border-[#444444]"
197
190
  >
198
191
  <option value="article_cover">槽位: 文章封面 (2.35:1)</option>
@@ -224,11 +217,7 @@ export const TemplateModal: React.FC<TemplateModalProps> = ({
224
217
  <span>默认槽位配置账号:</span>
225
218
  <select
226
219
  value={account}
227
- onChange={(e) => {
228
- const acc = e.target.value;
229
- setAccount(acc);
230
- fetchSlots(acc);
231
- }}
220
+ onChange={(e) => setAccount(e.target.value)}
232
221
  className="bg-white dark:bg-[#141414] border border-[#d4d4d4] dark:border-[#2a2a2a] rounded px-2 py-0.5 text-xs text-black dark:text-white focus:outline-none font-mono"
233
222
  >
234
223
  <option value="default">default (主账号)</option>
@@ -27,9 +27,23 @@ const INITIAL_MARKDOWN = `# 大模型纯算法排版深度剖析
27
27
  - 任意画板比例智能自适应
28
28
  `;
29
29
 
30
+ // 统一预设配图的页内序号计算,手动坐标和自由图层保持原样。
31
+ function resolvePresetImage(
32
+ image: FlowImage,
33
+ images: FlowImage[],
34
+ stageWidth: number,
35
+ stageHeight: number,
36
+ ): FlowImage {
37
+ if (image.isAbsolute || !image.layoutPreset || image.layoutPreset === "custom") return image;
38
+ const page = image.pageIndex || 1;
39
+ const indexOnPage = images.filter((item) => (item.pageIndex || 1) === page)
40
+ .findIndex((item) => item.id === image.id);
41
+ return resolveImagePlacement(image, indexOnPage, stageWidth, stageHeight);
42
+ }
43
+
30
44
  export function useTypeset() {
31
- const [aspectRatio, setAspectRatio] = useState<AspectRatioType>("longform-3-4");
32
45
  const [config, setConfig] = useState<TypesetConfig>(() => getDefaultConfig("longform-3-4"));
46
+ const aspectRatio = config.aspectRatio;
33
47
  const [markdown, setMarkdown] = useState<string>(INITIAL_MARKDOWN);
34
48
  const [images, setImages] = useState<FlowImage[]>([
35
49
  {
@@ -117,7 +131,6 @@ export function useTypeset() {
117
131
  }, []);
118
132
 
119
133
  const switchAspectRatio = (ratio: AspectRatioType) => {
120
- setAspectRatio(ratio);
121
134
  const spec = getCanvasSpec(ratio);
122
135
  const nextDefault = getDefaultConfig(ratio);
123
136
  const { stageWidth, stageHeight } = computeStageDimensions(
@@ -125,24 +138,7 @@ export function useTypeset() {
125
138
  nextDefault.algorithm.safeArea,
126
139
  nextDefault.algorithm.contentBottomGap,
127
140
  );
128
- setImages((prev) =>
129
- prev.map((img) => {
130
- if (!img.isAbsolute && img.layoutPreset && img.layoutPreset !== "custom") {
131
- const page = img.pageIndex || 1;
132
- const samePageList = prev.filter((im) => (im.pageIndex || 1) === page);
133
- const indexOnPage = Math.max(0, samePageList.findIndex((im) => im.id === img.id));
134
- const resolved = resolveImagePlacement(img, indexOnPage, stageWidth, stageHeight);
135
- return {
136
- ...img,
137
- x: resolved.x,
138
- y: resolved.y,
139
- width: resolved.width,
140
- height: resolved.height,
141
- };
142
- }
143
- return img;
144
- }),
145
- );
141
+ setImages((prev) => prev.map((image) => resolvePresetImage(image, prev, stageWidth, stageHeight)));
146
142
  setConfig((prev) => {
147
143
  return {
148
144
  ...prev,
@@ -235,28 +231,15 @@ export function useTypeset() {
235
231
 
236
232
  // If non-custom flow preset, recalculate clean coordinates based on its page index
237
233
  if (!updatedImg.isAbsolute && updatedImg.layoutPreset && updatedImg.layoutPreset !== "custom") {
238
- const page = updatedImg.pageIndex || 1;
239
- const samePageList = nextImages.filter((im) => (im.pageIndex || 1) === page);
240
- const indexOnPage = Math.max(0, samePageList.findIndex((im) => im.id === id));
241
-
242
234
  const spec = getCanvasSpec(aspectRatio);
243
235
  const { stageWidth, stageHeight } = computeStageDimensions(
244
236
  spec,
245
237
  config.algorithm.safeArea,
246
238
  config.algorithm.contentBottomGap,
247
239
  );
248
- const resolved = resolveImagePlacement(updatedImg, indexOnPage, stageWidth, stageHeight);
249
- return nextImages.map((im) =>
250
- im.id === id
251
- ? {
252
- ...im,
253
- x: resolved.x,
254
- y: resolved.y,
255
- width: resolved.width,
256
- height: resolved.height,
257
- }
258
- : im,
259
- );
240
+ return nextImages.map((image) => image.id === id
241
+ ? resolvePresetImage(image, nextImages, stageWidth, stageHeight)
242
+ : image);
260
243
  }
261
244
 
262
245
  return nextImages;
@@ -272,22 +255,7 @@ export function useTypeset() {
272
255
  config.algorithm.safeArea,
273
256
  config.algorithm.contentBottomGap,
274
257
  );
275
- return remaining.map((img) => {
276
- if (!img.isAbsolute && img.layoutPreset && img.layoutPreset !== "custom") {
277
- const page = img.pageIndex || 1;
278
- const samePageList = remaining.filter((im) => (im.pageIndex || 1) === page);
279
- const indexOnPage = Math.max(0, samePageList.findIndex((im) => im.id === img.id));
280
- const resolved = resolveImagePlacement(img, indexOnPage, stageWidth, stageHeight);
281
- return {
282
- ...img,
283
- x: resolved.x,
284
- y: resolved.y,
285
- width: resolved.width,
286
- height: resolved.height,
287
- };
288
- }
289
- return img;
290
- });
258
+ return remaining.map((image) => resolvePresetImage(image, remaining, stageWidth, stageHeight));
291
259
  });
292
260
  };
293
261
 
@@ -309,28 +277,12 @@ export function useTypeset() {
309
277
  config.algorithm.contentBottomGap,
310
278
  );
311
279
 
312
- return next.map((img) => {
313
- if (!img.isAbsolute && img.layoutPreset && img.layoutPreset !== "custom") {
314
- const page = img.pageIndex || 1;
315
- const samePageList = next.filter((im) => (im.pageIndex || 1) === page);
316
- const indexOnPage = Math.max(0, samePageList.findIndex((im) => im.id === img.id));
317
- const resolved = resolveImagePlacement(img, indexOnPage, stageWidth, stageHeight);
318
- return {
319
- ...img,
320
- x: resolved.x,
321
- y: resolved.y,
322
- width: resolved.width,
323
- height: resolved.height,
324
- };
325
- }
326
- return img;
327
- });
280
+ return next.map((image) => resolvePresetImage(image, next, stageWidth, stageHeight));
328
281
  });
329
282
  };
330
283
 
331
284
  const loadTemplate = (tpl: TypesetConfig) => {
332
285
  setConfig(tpl);
333
- setAspectRatio(tpl.aspectRatio);
334
286
  if (tpl.images) {
335
287
  setImages((prev) => applyTemplateImages(prev, tpl.images));
336
288
  }