dev: apply review suggestions and make auto-preview toggleable

This commit is contained in:
QuadnucYard 2025-11-15 21:24:20 +08:00
parent fcaf7a7e9a
commit 25b8a77ae8
9 changed files with 61 additions and 63 deletions

View file

@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.1/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.5/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",

View file

@ -282,7 +282,8 @@ export const messageHandlers: Record<string, MessageHandler> = {
}
}
} catch (error) {
await vscode.window.showErrorMessage(`Export failed: ${error}`);
const message = error instanceof Error ? error.message : String(error);
await vscode.window.showErrorMessage(`Export failed: ${message}`);
}
},
@ -298,17 +299,14 @@ export const messageHandlers: Record<string, MessageHandler> = {
});
};
console.log(`Generating preview for format=${format}, extraArgs=`, extraArgs);
try {
const ops = exportOps(extraArgs);
const formatProvider = provideFormats(extraArgs);
// await vscode.window.showInformationMessage(`Active `)
// Get the active document
const uri = ops.resolveInputPath();
if (!uri) {
// sendError("No active document found");
// Just ignore if no active document
return;
}
@ -332,7 +330,6 @@ export const messageHandlers: Record<string, MessageHandler> = {
return;
}
console.log(`Generating preview for format=${format}, extraArgs=${extraArgs}, uri=${uri}`);
// For visual formats, generate PNG/SVG previews
if (format === "pdf" || format === "png" || format === "svg") {
// Extract base64 data from the response
@ -362,7 +359,6 @@ export const messageHandlers: Record<string, MessageHandler> = {
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.log("Preview generation failed:", error);
sendError(`Preview generation failed: ${message}`);
}
},

View file

@ -12,34 +12,6 @@ export const ActionButtons = ({ onExport }: ActionButtonsProps) => {
h3("Export Actions"),
// Task Label Input
/* div(
{ class: "card flex flex-col gap-xs" },
label(
{
class: "text-sm font-medium",
for: "task-label",
style: "margin-bottom: 0.5rem; display: block;",
},
"Custom Task Label (Optional)",
),
input({
class: "input",
type: "text",
id: "task-label",
placeholder: `Export to ${exportConfig.val.format.label}`,
value: customTaskLabel,
oninput: (e: Event) => {
const target = e.target as HTMLInputElement;
customTaskLabel.val = target.value;
},
}),
div(
{ class: "text-xs text-desc" },
"This will be used as the task name in tasks.json. Leave empty for default naming.",
),
), */
// Action Buttons
div(
{ class: "action-buttons flex items-center gap-md" },

View file

@ -23,20 +23,8 @@ export const Header =
{ class: "flex flex-col sm:flex-row sm:justify-between sm:items-center gap-sm" },
div(
{ class: "flex flex-col gap-xs" },
h1(
{
class: "text-xl font-semibold text-base-content",
style: "margin: 0; font-size: 1.25rem; font-weight: 600;",
},
title,
),
p(
{
class: "text-desc",
style: "margin: 0; font-size: 0.875rem;",
},
description,
),
h1({ class: "text-xl font-semibold text-base-content" }, title),
p({ class: "text-desc font-sm" }, description),
),
actions ? div({ class: "flex gap-xs" }, actions) : null,
),

View file

@ -67,7 +67,7 @@ const OptionField = (schema: OptionSchema, valueState: State<Scalar>) => {
const renderInput = (
schema: OptionSchema,
valueState: State<Scalar>,
valueState: State<Scalar | undefined>,
validationError: State<string | undefined>,
) => {
const { type, key, options: selectOptions, min, max } = schema;
@ -102,7 +102,7 @@ const renderInput = (
const target = e.target as HTMLInputElement;
// Call custom validation function if provided
validationError.val = schema.validate?.(target.value);
valueState.val = parseFloat(target.value);
valueState.val = target.value === "" ? undefined : parseFloat(target.value);
},
});

View file

@ -1,17 +1,18 @@
import van, { type State } from "vanjs-core";
import type { ExportFormat, PreviewData, PreviewPage } from "../types";
const { div, h3, img, span, button } = van.tags;
const { div, h3, img, span, button, label, input } = van.tags;
interface PreviewGridProps {
format: State<ExportFormat>;
previewData: State<PreviewData>;
previewGenerating: State<boolean>;
autoPreview: State<boolean>;
onPreview: () => void;
}
export const PreviewGrid = (props: PreviewGridProps) => {
const { format, previewData, previewGenerating, onPreview } = props;
const { format, previewData, previewGenerating, autoPreview, onPreview } = props;
const thumbnailZoom = van.state<number>(100); // Percentage for thumbnail sizing
@ -19,14 +20,13 @@ export const PreviewGrid = (props: PreviewGridProps) => {
// Preview Header
div(
{ class: "flex justify-between items-center mb-md" },
h3({ class: "text-lg font-semibold" }, () => `Preview (${format.val.label})`),
() =>
div(
{ class: "flex items-center gap-sm", style: "min-height: 2rem;" },
// Only show zoom controls for image content (thumbnails)
!previewGenerating.val && previewData.val.pages && previewData.val.pages.length > 0
? ZoomControls(thumbnailZoom)
: null,
{ class: "flex items-center gap-sm" },
h3({ class: "text-lg font-semibold" }, () => `Preview (${format.val.label})`),
// Generate Preview Button
() =>
button(
{
class: "btn btn-secondary",
@ -35,6 +35,31 @@ export const PreviewGrid = (props: PreviewGridProps) => {
},
previewGenerating.val ? "Generating..." : "Generate Preview",
),
// Auto-preview toggle
label(
{ class: "flex items-center gap-xs cursor-pointer", style: "user-select: none;" },
input({
type: "checkbox",
class: "toggle",
checked: autoPreview,
onchange: (e: Event) => {
const target = e.target as HTMLInputElement;
autoPreview.val = target.checked;
},
}),
span({ class: "text-sm" }, "Auto"),
),
),
() =>
div(
{ class: "flex items-center gap-sm", style: "min-height: 2rem;" },
// Only show zoom controls for image content (thumbnails)
!previewGenerating.val && previewData.val.pages && previewData.val.pages.length > 0
? ZoomControls(thumbnailZoom)
: null,
),
),

View file

@ -14,6 +14,7 @@ export function useExporter() {
let previewVersion = 0;
const previewGenerating = van.state(false);
const previewData = van.state<PreviewData>({});
const autoPreview = van.state(true);
const buildOptions = () => {
const extraOpts = Object.fromEntries(
@ -52,6 +53,8 @@ export function useExporter() {
// Regenerate preview automatically when format or options change
van.derive(() => {
if (!autoPreview.val) return;
if (format.oldVal !== format.val) {
// Clear previous preview data when format changes
previewData.val = {};
@ -71,6 +74,10 @@ export function useExporter() {
};
window?.addEventListener("message", handleMessage);
const cleanup = () => {
window?.removeEventListener("message", handleMessage);
};
return {
inputPath,
outputPath,
@ -78,8 +85,10 @@ export function useExporter() {
optionStates,
previewGenerating,
previewData,
autoPreview,
exportDocument,
generatePreview,
cleanup,
};
}

View file

@ -45,9 +45,11 @@ export const EXPORT_FORMATS: ExportFormat[] = [
if (value.trim() === "") {
return; // Allow empty input
}
if (!/^\d+$/.test(value)) {
return "Creation timestamp must be a valid non-negative integer UNIX timestamp";
}
const num = Number(value);
if (Number.isNaN(num) || !Number.isInteger(num) || num < 0) {
// fixme: it still accepts floating point numbers like "1e5"
return "Creation timestamp must be a valid non-negative integer UNIX timestamp";
}
},

View file

@ -23,10 +23,15 @@ const ExportTool = () => {
optionStates,
previewGenerating,
previewData,
autoPreview,
exportDocument,
generatePreview,
} = useExporter();
// Note: cleanup() should be called when the component is unmounted
// In the current single-page architecture, this might not be needed,
// but it's available for future use if the tool becomes part of a larger app
return div(
{ class: "export-tool-container flex flex-col gap-lg text-base-content" },
@ -49,6 +54,7 @@ const ExportTool = () => {
format: format,
previewData,
previewGenerating,
autoPreview,
onPreview: generatePreview,
}),