kopia lustrzana https://github.com/zhengkyl/qrframe
lazy load editor + static build + update lucide
rodzic
541674b72b
commit
21fb3b4040
|
@ -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
|
||||
)
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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: {}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue