@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.
- package/README.md +2 -3
- package/dist/assets/index-DDeEGeHD.js +317 -0
- package/dist/index.html +1 -1
- package/dist/lib/adapter.js +82 -234
- package/dist/lib/index.js +82 -234
- package/package.json +2 -5
- package/src/adapter.ts +85 -267
- package/src/core/canvas-draw.ts +9 -9
- package/src/core/engine.ts +14 -22
- package/src/core/pretext.ts +0 -8
- package/src/core/template.ts +1 -1
- package/src/core/typography.ts +0 -14
- package/src/index.ts +2 -2
- package/src/server/index.ts +1 -1
- package/src/server/routes/fonts.ts +1 -1
- package/src/server/routes/templates.ts +4 -14
- package/src/web/App.tsx +0 -2
- package/src/web/components/EditorSidebar.tsx +24 -48
- package/src/web/components/Header.tsx +1 -1
- package/src/web/components/TemplateModal.tsx +3 -14
- package/src/web/hooks/useTypeset.ts +21 -69
- package/dist/assets/index-Dpnz-F2g.js +0 -317
|
@@ -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
|
-
//
|
|
480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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((
|
|
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((
|
|
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
|
}
|