lazy load editor + static build + update lucide

main
Kyle Zheng 2025-01-24 19:54:43 -05:00
rodzic 541674b72b
commit 21fb3b4040
9 zmienionych plików z 186 dodań i 181 usunięć

Wyświetl plik

@ -1,29 +1,18 @@
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "@solidjs/start/config";
import UnoCSS from "unocss/vite";
import wasmpack from "vite-plugin-wasm-pack";
export default defineConfig({
server: {
static: true,
preset: "cloudflare-pages",
rollupConfig: {
external: ["node:async_hooks"]
}
external: ["node:async_hooks"],
},
},
ssr: true,
ssr: false,
vite: {
plugins: [UnoCSS(), wasmpack([], ["fuqr"]), blobRewriter()],
resolve: {
alias: {
// https://christopher.engineering/en/blog/lucide-icons-with-vite-dev-server/
"lucide-solid/icons": fileURLToPath(
new URL(
"./node_modules/lucide-solid/dist/source/icons",
import.meta.url
)
),
},
},
},
});

Wyświetl plik

@ -24,7 +24,7 @@
"@unocss/reset": "^0.59.4",
"codemirror": "^6.0.1",
"fuqr": "^1.0.0",
"lucide-solid": "^0.378.0",
"lucide-solid": "^0.474.0",
"solid-js": "^1.9.2",
"unocss": "^0.59.4",
"vinxi": "^0.3.11"

Wyświetl plik

@ -58,8 +58,8 @@ importers:
specifier: ^1.0.0
version: 1.0.0
lucide-solid:
specifier: ^0.378.0
version: 0.378.0(solid-js@1.9.2)
specifier: ^0.474.0
version: 0.474.0(solid-js@1.9.2)
solid-js:
specifier: ^1.9.2
version: 1.9.2
@ -2340,8 +2340,8 @@ packages:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
lucide-solid@0.378.0:
resolution: {integrity: sha512-flJIh53qIVnHMsq4WipY+5dlclhnAIrVIIatK3TjUD+9aNNwvb8dSOYw5+Gyxdy69k0A4fWKmtU3k6O9AUdgLA==}
lucide-solid@0.474.0:
resolution: {integrity: sha512-sSEzUW2TVHpbYRehLfPuWtL+mGeBUQRZWPE0020aealCIxaNSuZOWts3NdS+R+itWr5JenTcwi1+Q3OcLTIZXg==}
peerDependencies:
solid-js: ^1.4.7
@ -5639,7 +5639,7 @@ snapshots:
dependencies:
yallist: 4.0.0
lucide-solid@0.378.0(solid-js@1.9.2):
lucide-solid@0.474.0(solid-js@1.9.2):
dependencies:
solid-js: 1.9.2
@ -5737,7 +5737,7 @@ snapshots:
acorn: 8.11.3
pathe: 1.1.2
pkg-types: 1.1.0
ufo: 1.5.3
ufo: 1.5.4
mri@1.2.0: {}
@ -5894,7 +5894,7 @@ snapshots:
dependencies:
destr: 2.0.3
node-fetch-native: 1.6.4
ufo: 1.5.3
ufo: 1.5.4
ohash@1.1.3: {}

Wyświetl plik

@ -1,7 +0,0 @@
declare module "lucide-solid/icons/*" {
import { LucideProps } from "lucide-solid/dist/types/types";
import { Component } from "solid-js";
const cmp: Component<LucideProps>;
export = cmp;
}

Wyświetl plik

@ -18,7 +18,7 @@ export function Switch(props: Props) {
)}
<KSwitch.Input class="peer" />
<KSwitch.Control class="inline-flex items-center w-11 h-6 px-0.5 bg-back-distinct data-[checked]:bg-fore-base transition-colors border rounded-3 peer-focus:(ring-2 ring-fore-base ring-offset-2 ring-offset-back-base)">
<KSwitch.Thumb class="h-5 w-5 rounded-2.5 bg-back-base data-[checked]:translate-x-[calc(100%-1px)] transition-transform" />
<KSwitch.Thumb class="h-5 w-5 rounded-2.5 bg-back-base data-[checked]:translate-x-[calc(100%-.125rem)] transition-transform" />
</KSwitch.Control>
</KSwitch>
);

Wyświetl plik

@ -1,4 +1,4 @@
import { createEffect, createSignal, onMount, Show, untrack } from "solid-js";
import { createEffect, createSignal, onMount, untrack } from "solid-js";
import { basicSetup } from "codemirror";
import { historyKeymap, indentWithTab } from "@codemirror/commands";
@ -14,7 +14,6 @@ import { vim } from "@replit/codemirror-vim";
import { Button } from "@kobalte/core/button";
import { debounce } from "~/lib/util";
import { Switch } from "../Switch";
import { AllowPasteDialog } from "./AllowPasteDialog";
type Props = {
@ -26,7 +25,7 @@ const VIM_MODE_KEY = "vimMode";
const ALLOW_PASTE_KEY = "allowPaste";
export function CodeEditor(props: Props) {
let parent: HTMLDivElement;
let parent!: HTMLDivElement;
let view: EditorView;
let modeComp = new Compartment();
let allowPaste;
@ -140,58 +139,50 @@ export function CodeEditor(props: Props) {
view.contentDOM.blur();
});
const [showCode, setShowCode] = createSignal(false);
const [showDialog, setShowDialog] = createSignal(false);
return (
<>
<div class="py-2">
<Switch label="Code editor" value={showCode()} setValue={setShowCode} />
</div>
<div>
<AllowPasteDialog
open={showDialog()}
setClosed={() => {
setShowDialog(false);
}}
onAllow={() => {
allowPaste = true;
localStorage.setItem(ALLOW_PASTE_KEY, "true");
}}
/>
<div class="flex justify-end gap-4 pb-2 h-11">
<Show when={showCode()}>
<label class="flex items-center gap-1 text-sm">
Vim mode
<input
class="h-4 w-4"
type="checkbox"
checked={vimMode()}
onChange={(e) => setVimMode(e.target.checked)}
/>
</label>
<label class="flex items-center gap-1 text-sm">
Update thumbnail
<input
class="h-4 w-4"
type="checkbox"
checked={updateThumbnail()}
onChange={(e) => setUpdateThumbnail(e.target.checked)}
/>
</label>
<Button
disabled={!dirty()}
onMouseDown={() =>
props.onSave(view.state.doc.toString(), updateThumbnail())
}
class="bg-green-700 border rounded-md hover:bg-green-700/90 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) disabled:(bg-transparent text-fore-base pointer-events-none opacity-50) transition-colors px-3 min-w-150px"
>
{dirty() ? "Save" : "No changes"}
</Button>
</Show>
</div>
<div ref={parent!} classList={{ hidden: !showCode() }}></div>
<AllowPasteDialog
open={showDialog()}
setClosed={() => {
setShowDialog(false);
}}
onAllow={() => {
allowPaste = true;
localStorage.setItem(ALLOW_PASTE_KEY, "true");
}}
/>
<div class="flex justify-end gap-4 py-2">
<label class="flex items-center gap-1 text-sm">
Vim mode
<input
class="h-4 w-4"
type="checkbox"
checked={vimMode()}
onChange={(e) => setVimMode(e.target.checked)}
/>
</label>
<label class="flex items-center gap-1 text-sm">
Update thumbnail
<input
class="h-4 w-4"
type="checkbox"
checked={updateThumbnail()}
onChange={(e) => setUpdateThumbnail(e.target.checked)}
/>
</label>
<Button
disabled={!dirty()}
onMouseDown={() =>
props.onSave(view.state.doc.toString(), updateThumbnail())
}
class="bg-green-700 border rounded-md hover:bg-green-700/90 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) disabled:(bg-transparent text-fore-base pointer-events-none opacity-50) transition-colors px-3 py-1 min-w-150px"
>
{dirty() ? "Save" : "No changes"}
</Button>
</div>
<div ref={parent!}></div>
</>
);
}

Wyświetl plik

@ -1,8 +1,12 @@
import {
closestCenter, createSortable, DragDropProvider,
closestCenter,
createSortable,
DragDropProvider,
DragDropSensors,
DragOverlay,
SortableProvider, transformStyle, useDragDropContext
SortableProvider,
transformStyle,
useDragDropContext,
} from "@thisbeyond/solid-dnd";
import GripVertical from "lucide-solid/icons/grip-vertical";
import Minus from "lucide-solid/icons/minus";
@ -16,7 +20,7 @@ import { FlatButton } from "../Button";
export function ParamsEditor() {
const { paramsSchema, params, setParams } = useRenderContext();
return (
<div class="flex flex-col gap-2 mb-4">
<div class="flex flex-col gap-2">
<For each={Object.entries(paramsSchema())}>
{([label, { type, ...other }]) => {
if (type === "array") {
@ -125,7 +129,10 @@ function ArrayParam({ label, other }) {
value={v()}
setValue={(v: any) => setParams(label, i, v)}
/>
<div class="px-1 cursor-move touch-none" {...sortable.dragActivators}>
<div
class="px-1 cursor-move touch-none"
{...sortable.dragActivators}
>
<GripVertical />
</div>
</div>

Wyświetl plik

@ -1,6 +1,15 @@
import Pencil from "lucide-solid/icons/pencil";
import Trash2 from "lucide-solid/icons/trash-2";
import { For, Show, batch, createSignal, onMount, type JSX } from "solid-js";
import {
For,
Show,
Suspense,
batch,
createSignal,
lazy,
onMount,
type JSX,
} from "solid-js";
import { createStore } from "solid-js/store";
import {
deepEqualObj,
@ -17,9 +26,15 @@ import { Collapsible } from "../Collapsible";
import { ContentMenuTrigger, ContextMenuProvider } from "../ContextMenu";
import { ControlledDialog, DialogButton } from "../Dialog";
import { TextInput, TextareaInput } from "../TextInput";
import { CodeEditor } from "./CodeEditor";
import { ParamsEditor } from "./ParamsEditor";
import { Settings } from "./Settings";
import { Switch } from "../Switch";
const CodeEditor = lazy(() => {
return import("./CodeEditor").then((module) => ({
default: module.CodeEditor,
}));
});
import "virtual:blob-rewriter";
@ -286,6 +301,8 @@ export function Editor(props: Props) {
saveAndRun(code, true, true);
};
const [showCode, setShowCode] = createSignal(false);
return (
<div class={props.class}>
<TextareaInput
@ -310,7 +327,7 @@ export function Editor(props: Props) {
const [rename, setRename] = createSignal(key);
const [duplicate, setDuplicate] = createSignal(false);
let ref: HTMLInputElement;
let ref!: HTMLInputElement;
onMount(() => ref.focus());
const onSubmit = () => {
@ -340,7 +357,7 @@ export function Editor(props: Props) {
<>
<TextInput
class="mt-2"
ref={ref!}
ref={ref}
defaultValue={rename()}
onInput={(s) => {
if (duplicate()) setDuplicate(false);
@ -397,8 +414,8 @@ export function Editor(props: Props) {
);
}}
</ControlledDialog>
<div class="py-4">
<div class="mb-4 h-[180px] md:(h-unset)">
<div class="py-4 flex flex-col gap-4">
<div class="h-[180px] md:(h-unset)">
<div class="flex justify-between">
<div class="text-sm py-2 border border-transparent">Presets</div>
<div class="flex gap-2">
@ -429,7 +446,7 @@ export function Editor(props: Props) {
</Show>
</div>
</div>
<div class="flex gap-3 pt-2 pb-4 md:(flex-wrap static ml-0 px-0 overflow-x-visible) absolute max-w-full overflow-x-auto -ml-6 px-6">
<div class="flex gap-3 py-2 md:(flex-wrap static ml-0 px-0 overflow-x-visible) absolute max-w-full overflow-x-auto -ml-6 px-6">
<ContextMenuProvider
disabled={isPreset(dialogKey())}
onRename={() => setRenameOpen(true)}
@ -466,16 +483,27 @@ export function Editor(props: Props) {
</div>
</div>
<ParamsEditor />
<CodeEditor
initialValue={code()}
onSave={(code, updateThumbnail) => {
if (presetKeys.includes(renderKey())) {
createAndSelectFunc(renderKey(), code);
} else {
saveAndRun(code, true, updateThumbnail);
}
}}
/>
<div>
<Switch
label="Code editor"
value={showCode()}
setValue={setShowCode}
/>
<Show when={showCode()}>
<Suspense fallback={<p>Loading...</p>}>
<CodeEditor
initialValue={code()}
onSave={(code, updateThumbnail) => {
if (presetKeys.includes(renderKey())) {
createAndSelectFunc(renderKey(), code);
} else {
saveAndRun(code, true, updateThumbnail);
}
}}
/>
</Suspense>
</Show>
</div>
</div>
</Collapsible>
</div>

Wyświetl plik

@ -220,90 +220,87 @@ function DownloadButtons() {
};
return (
<div>
<div class="font-bold text-sm pb-2 md:hidden">Downloads</div>
<div class="flex gap-2 md:(grid grid-cols-2)">
<SplitButton
disabled={disabled()}
onPng={async (resizeWidth, resizeHeight) => {
try {
const blob = await pngBlob(resizeWidth, resizeHeight);
if (blob == null) throw "toBlob returned null";
<div class="flex gap-2 md:(grid grid-cols-2)">
<SplitButton
disabled={disabled()}
onPng={async (resizeWidth, resizeHeight) => {
try {
const blob = await pngBlob(resizeWidth, resizeHeight);
if (blob == null) throw "toBlob returned null";
const url = URL.createObjectURL(blob);
download(url, `${filename()}.png`);
URL.revokeObjectURL(url);
} catch (e) {
toastError("Failed to create image", e as string);
return;
}
}}
onSvg={downloadSvg}
/>
<Show when={render()?.type === "svg"}>
<FlatButton
class="hidden md:inline-flex flex-1 justify-center items-center gap-1 px-3 py-2"
disabled={disabled()}
onClick={downloadSvg}
>
<Download size={20} />
SVG
</FlatButton>
</Show>
const url = URL.createObjectURL(blob);
download(url, `${filename()}.png`);
URL.revokeObjectURL(url);
} catch (e) {
toastError("Failed to create image", e as string);
return;
}
}}
onSvg={downloadSvg}
/>
<Show when={render()?.type === "svg"}>
<FlatButton
class="md:hidden inline-flex justify-center items-center gap-1 px-6 py-2"
class="hidden md:inline-flex flex-1 justify-center items-center gap-1 px-3 py-2"
disabled={disabled()}
title="Share"
onClick={async () => {
let blob;
try {
blob = await pngBlob(0, 0);
if (blob == null) throw "toBlob returned null";
} catch (e) {
toastError(
"Failed to create image",
typeof e === "string" ? e : "pngBlob failed"
);
return;
}
try {
const shareData = {
files: [
new File([blob], `${filename()}.png`, {
type: "image/png",
}),
],
};
if (!navigator.canShare(shareData)) {
throw new Error();
}
navigator.share(shareData);
} catch (e) {
console.log(e);
toastError(
"Native sharing failed",
"File sharing not supported by browser"
);
}
}}
onClick={downloadSvg}
>
<Share2 size={20} />
<Download size={20} />
SVG
</FlatButton>
<Popover gutter={4}>
<Popover.Trigger
class="md:hidden border rounded-md hover:bg-fore-base/5 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) p-2 disabled:(pointer-events-none opacity-50)"
disabled={disabled()}
title="QR Metadata"
>
<Info size={20} />
</Popover.Trigger>
<Popover.Portal>
<Popover.Content class="z-50 bg-back-base rounded-md border p-2 outline-none min-w-150px leading-tight">
<Metadata />
</Popover.Content>
</Popover.Portal>
</Popover>
</div>
</Show>
<FlatButton
class="md:hidden inline-flex justify-center items-center gap-1 px-6 py-2"
disabled={disabled()}
title="Share"
onClick={async () => {
let blob;
try {
blob = await pngBlob(0, 0);
if (blob == null) throw "toBlob returned null";
} catch (e) {
toastError(
"Failed to create image",
typeof e === "string" ? e : "pngBlob failed"
);
return;
}
try {
const shareData = {
files: [
new File([blob], `${filename()}.png`, {
type: "image/png",
}),
],
};
if (!navigator.canShare(shareData)) {
throw new Error();
}
navigator.share(shareData);
} catch (e) {
console.log(e);
toastError(
"Native sharing failed",
"File sharing not supported by browser"
);
}
}}
>
<Share2 size={20} />
</FlatButton>
<Popover gutter={4}>
<Popover.Trigger
class="md:hidden border rounded-md hover:bg-fore-base/5 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) p-2 disabled:(pointer-events-none opacity-50)"
disabled={disabled()}
title="QR Metadata"
>
<Info size={20} />
</Popover.Trigger>
<Popover.Portal>
<Popover.Content class="z-50 bg-back-base rounded-md border p-2 outline-none min-w-150px leading-tight">
<Metadata />
</Popover.Content>
</Popover.Portal>
</Popover>
</div>
);
}