German page, and a section for the image module
/ and /de/ are now two pages from one shared component and one copy file, with hreflang between them and a language button in the header. Separate URLs rather than a runtime switch: each language is linkable and indexable, and the page still works without JavaScript. The sheet playground takes its labels from the page, so its readout says "29,7 cm" in German and "29.7 cm" in English. New section for the `ai` module, because the page so far read as if Klarbild were print-only. It says plainly what the module costs and that the endpoint is configurable, and it carries a preset builder: pick tasks and a target size, see the exact pixels and the recipe that gets saved. It composes; it does not generate. A generator here would need a key on a public page and would show a picture from a model rather than anything about Klarbild — the part worth showing is the recipe. The target pixels come from engine/format.ts, copied verbatim from the application like the layout files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nh4A79bbqT9dcd2inzYUdg
This commit is contained in:
10 files changed
+1126
-372
No files matched your search
+30
-9
@@ -14,6 +14,11 @@
|
||||
import { layout, cutMarks, effectiveGap, recommendedGap, type PlaceSpec, type SheetSpec, type MarkMode } from '../engine/printlayout';
|
||||
import { PAPERS, PHOTO_SIZES, labelMm } from '../engine/paper';
|
||||
|
||||
/** German writes 29,7 cm where English writes 29.7 cm. Only the separator differs. */
|
||||
function localiseNumbers(text: string, lang: string): string {
|
||||
return lang === 'de' ? text.replace(/(\d),(?=\d)/g, '$1\u2009').replace(/(\d)\.(?=\d)/g, '$1,') : text;
|
||||
}
|
||||
|
||||
const MM_PT = 72 / 25.4;
|
||||
|
||||
type State = {
|
||||
@@ -174,7 +179,17 @@ async function buildPdf(): Promise<Blob> {
|
||||
|
||||
/* ---------------------------------------------------------------------- wire */
|
||||
|
||||
export function mountPlayground(root: HTMLElement) {
|
||||
interface PgLabels {
|
||||
downloadBusy: string;
|
||||
noteAllFit: string;
|
||||
noteAllFitMulti: string;
|
||||
notePartial: string;
|
||||
noteNone: string;
|
||||
}
|
||||
|
||||
export function mountPlayground(root: HTMLElement, labels: PgLabels) {
|
||||
const lang = document.documentElement.lang || 'en';
|
||||
const mm = (w: number, h: number) => localiseNumbers(labelMm(w, h), lang);
|
||||
const svg = root.querySelector<SVGSVGElement>('#sheet')!;
|
||||
const out = {
|
||||
fits: root.querySelector<HTMLElement>('[data-out=fits]')!,
|
||||
@@ -193,7 +208,7 @@ export function mountPlayground(root: HTMLElement) {
|
||||
for (const p of PAPERS.filter((x) => x.group === group)) {
|
||||
const o = document.createElement('option');
|
||||
o.value = p.id;
|
||||
o.textContent = `${p.label} · ${labelMm(p.w, p.h)}`;
|
||||
o.textContent = `${p.label} · ${mm(p.w, p.h)}`;
|
||||
og.appendChild(o);
|
||||
}
|
||||
paperSel.appendChild(og);
|
||||
@@ -222,18 +237,24 @@ export function mountPlayground(root: HTMLElement) {
|
||||
|
||||
out.fits.textContent = String(placed);
|
||||
out.pages.textContent = String(result.pages.length);
|
||||
out.cell.textContent = labelMm(s.w, s.h);
|
||||
out.paper.textContent = labelMm(sheet.wMm, sheet.hMm);
|
||||
out.gap.textContent = `${effectiveGap(sheet).toFixed(1)} mm`;
|
||||
out.cell.textContent = mm(s.w, s.h);
|
||||
out.paper.textContent = mm(sheet.wMm, sheet.hMm);
|
||||
out.gap.textContent = localiseNumbers(`${effectiveGap(sheet).toFixed(1)} mm`, lang);
|
||||
|
||||
const fill = (t: string) => t
|
||||
.replace('{n}', String(wanted))
|
||||
.replace('{placed}', String(placed))
|
||||
.replace('{pages}', String(result.pages.length))
|
||||
.replace('{size}', s.label);
|
||||
|
||||
if (placed === 0) {
|
||||
out.note.textContent = `${s.label} does not fit on this sheet at 100 %. In the application this is where you choose overflow or tiling.`;
|
||||
out.note.textContent = fill(labels.noteNone);
|
||||
out.note.dataset.tone = 'warn';
|
||||
} else if (placed < wanted) {
|
||||
out.note.textContent = `${placed} of ${wanted} fit on ${result.pages.length} sheet${result.pages.length > 1 ? 's' : ''}.`;
|
||||
out.note.textContent = fill(labels.notePartial);
|
||||
out.note.dataset.tone = 'warn';
|
||||
} else {
|
||||
out.note.textContent = `All ${placed} fit${result.pages.length > 1 ? ` across ${result.pages.length} sheets` : ' on one sheet'}. Nothing is scaled.`;
|
||||
out.note.textContent = fill(result.pages.length > 1 ? labels.noteAllFitMulti : labels.noteAllFit);
|
||||
out.note.dataset.tone = 'ok';
|
||||
}
|
||||
}
|
||||
@@ -269,7 +290,7 @@ export function mountPlayground(root: HTMLElement) {
|
||||
dl.addEventListener('click', async () => {
|
||||
const label = dl.textContent;
|
||||
dl.disabled = true;
|
||||
dl.textContent = 'Building the PDF …';
|
||||
dl.textContent = labels.downloadBusy;
|
||||
try {
|
||||
const blob = await buildPdf();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
Reference in new issue
Block a user