mobile friendly scroll thumbnails + hide code

main
Kyle Zheng 2024-07-30 04:26:53 -04:00
rodzic d6249d1ab3
commit dadb2cb282
7 zmienionych plików z 276 dodań i 235 usunięć

Wyświetl plik

@ -10,10 +10,11 @@ type Props = {
export function Collapsible(props: Props) {
return (
<KCollapsible class="" defaultOpen={props.defaultOpen}>
<KCollapsible.Trigger class="group font-bold w-full inline-flex justify-between leading-tight focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) disabled:(pointer-events-none opacity-50) px-2 py-4 mb-2 border-b">
<KCollapsible.Trigger class="group font-bold w-full inline-flex justify-between leading-tight focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) disabled:(pointer-events-none opacity-50) px-2 py-3 border-b">
{props.trigger}
<ChevronDown class="w-5 h-5 group-data-[expanded]:rotate-180 transition-transform duration-300" />
</KCollapsible.Trigger>
{/* Content cannot have y padding b/c it breaks animation */}
<KCollapsible.Content class="overflow-hidden animate-collapsible-exit data-[expanded]:animate-collapsible-enter px-2">
{props.children}
</KCollapsible.Content>

Wyświetl plik

@ -101,24 +101,37 @@ export function CodeEditor(props: Props) {
}
});
const [showCode, setShowCode] = createSignal(false);
return (
<div>
<div class="flex justify-between pb-2">
<Switch label="Vim mode" value={vimMode()} setValue={setVimMode} />
<Button
disabled={!dirty()}
onMouseDown={() => props.onSave(view.state.doc.toString())}
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 class="flex justify-between pb-2 h-11">
<Switch label="Show code" value={showCode()} setValue={setShowCode} />
<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>
<Button
disabled={!dirty()}
onMouseDown={() => props.onSave(view.state.doc.toString())}
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>
<Show when={props.error}>
<div class="text-red-100 bg-red-950 px-2 py-1 rounded-md mb-1">
{props.error}
</div>
</Show>
<div ref={parent!}></div>
<div ref={parent!} classList={{ hidden: !showCode() }}></div>
</div>
);
}

Wyświetl plik

@ -1,6 +1,6 @@
import Pencil from "lucide-solid/icons/pencil";
import Trash2 from "lucide-solid/icons/trash-2";
import { For, Show, batch, createSignal, onMount } from "solid-js";
import { For, Show, batch, createSignal, onMount, type JSX } from "solid-js";
import { createStore } from "solid-js/store";
import { Dynamic } from "solid-js/web";
import {
@ -70,15 +70,15 @@ export function Editor(props: Props) {
onMount(async () => {
const storedFuncKeys = localStorage.getItem(USER_FUNC_KEYS_KEY);
if (storedFuncKeys == null) return;
if (storedFuncKeys != null) {
const keys = storedFuncKeys.split(",");
for (const key of keys) {
const funcCode = localStorage.getItem(key);
if (funcCode == null) continue;
const keys = storedFuncKeys.split(",");
for (const key of keys) {
const funcCode = localStorage.getItem(key);
if (funcCode == null) continue;
const thumb = localStorage.getItem(`${key}_thumb`) ?? FALLBACK_THUMB;
setUserFuncs(userFuncs.length, { key, thumb });
const thumb = localStorage.getItem(`${key}_thumb`) ?? FALLBACK_THUMB;
setUserFuncs(userFuncs.length, { key, thumb });
}
}
// @ts-expect-error adding keys below
@ -96,7 +96,6 @@ export function Editor(props: Props) {
);
}
const thumb = localStorage.getItem(`${key}_thumb`) ?? FALLBACK_THUMB;
console.log(key, thumb.length);
thumbs[key as keyof typeof PRESET_MODULES] = thumb;
}
setPresetThumbs(thumbs);
@ -239,234 +238,257 @@ export function Editor(props: Props) {
setUserFuncs(userFuncs.length, { key, thumb: FALLBACK_THUMB });
localStorage.setItem(USER_FUNC_KEYS_KEY, keys.join(","));
setRenderFuncKey(key);
userSetCode(code, false);
userSetCode(code, true);
};
return (
<div class={props.class}>
<TextareaInput
placeholder="https://qrcode.kylezhe.ng"
placeholder="https://qrframe.kylezhe.ng"
setValue={(s) => setInputQr("text", s)}
/>
<Collapsible trigger="QR Code">
<Collapsible trigger="Data">
<Settings />
</Collapsible>
<Collapsible trigger="Rendering" defaultOpen>
<div class="mb-4">
<div class="text-sm py-2">Render function</div>
<div class="flex sm:flex-wrap gap-2">
<For each={Object.entries(PRESET_MODULES)}>
{([key, preset]) => (
<div
onMouseDown={() => {
setRenderFuncKey(key);
// @ts-expect-error assigning narrow to wider is ok b/c params validated
internalSetCode(preset);
}}
>
<div class="h-24 w-24 checkboard">
<Collapsible trigger="Render" defaultOpen>
<div class="py-4">
<div class="mb-4 h-[180px] md:(h-unset)">
<div class="flex justify-between">
<div class="text-sm py-2 border border-transparent">Render function</div>
<Show
when={!Object.keys(PRESET_MODULES).includes(renderFuncKey())}
>
<div class="flex gap-1">
<IconButtonDialog
title={`Rename ${renderFuncKey()}`}
triggerTitle="Rename"
triggerChildren={<Pencil class="w-5 h-5" />}
onOpenAutoFocus={(e) => e.preventDefault()}
>
{(close) => {
const [rename, setRename] = createSignal(renderFuncKey());
const [duplicate, setDuplicate] = createSignal(false);
let ref: HTMLInputElement;
onMount(() => ref.focus());
return (
<>
<TextInput
class="mt-2"
ref={ref!}
defaultValue={rename()}
onChange={setRename}
onInput={() => duplicate() && setDuplicate(false)}
placeholder={renderFuncKey()}
/>
<div class="absolute p-1 text-sm text-red-600">
<Show when={duplicate()}>
{rename()} already exists.
</Show>
</div>
<FillButton
class="px-3 py-2 float-right mt-4"
// input onChange runs after focus lost, so onMouseDown is too early
onClick={() => {
if (rename() === renderFuncKey()) return close();
const userFuncKeys = Object.values(userFuncs).map(
(func) => func.key
);
if (
Object.keys(PRESET_MODULES).includes(
rename()
) ||
userFuncKeys.includes(rename())
) {
setDuplicate(true);
} else {
localStorage.removeItem(renderFuncKey());
localStorage.setItem(rename(), code());
setUserFuncs(
userFuncKeys.indexOf(renderFuncKey()),
"key",
rename()
);
localStorage.setItem(
USER_FUNC_KEYS_KEY,
userFuncKeys.join(",")
);
setRenderFuncKey(rename());
close();
}
}}
>
Confirm
</FillButton>
</>
);
}}
</IconButtonDialog>
<IconButtonDialog
title={`Delete ${renderFuncKey()}`}
triggerTitle="Delete"
triggerChildren={<Trash2 class="w-5 h-5" />}
>
{(close) => (
<>
<p class="mb-4 text-sm">
Are you sure you want to delete this function?
</p>
<div class="flex justify-end gap-2">
<FillButton
onMouseDown={() => {
const userFuncKeys = Object.values(userFuncs).map(
(func) => func.key
);
setUserFuncs((funcs) =>
funcs.filter(
(func) => func.key !== renderFuncKey()
)
);
localStorage.removeItem(renderFuncKey());
localStorage.setItem(
USER_FUNC_KEYS_KEY,
userFuncKeys.join(",")
);
setRenderFuncKey("Square");
// @ts-expect-error renderSVG narrow to wider is fine b/c valid params
internalSetCode(PRESET_MODULES.Square);
close();
}}
>
Confirm
</FillButton>
<FlatButton onMouseDown={close}>Cancel</FlatButton>
</div>
</>
)}
</IconButtonDialog>
</div>
</Show>
</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">
<For each={Object.entries(PRESET_MODULES)}>
{([key, preset]) => (
<Preview
onClick={() => {
setRenderFuncKey(key);
// @ts-expect-error assigning narrow to wider is ok b/c params validated
internalSetCode(preset);
}}
label={key}
active={renderFuncKey() === key}
>
<img
class="w-full"
class="rounded-sm"
src={presetThumbs()[key as keyof typeof PRESET_MODULES]}
/>
</div>
<div class="text-center text-sm">{key}</div>
</div>
)}
</For>
<For each={userFuncs}>
{(func) => (
<div
onMouseDown={() => {
let storedCode = localStorage.getItem(func.key);
if (storedCode == null) {
storedCode = `Failed to load ${func.key}`;
}
setRenderFuncKey(func.key);
userSetCode(storedCode, false);
}}
>
<div class="h-24 w-24 checkboard">
<img src={func.thumb} />
</div>
<div class="text-center text-sm">{func.key}</div>
</div>
)}
</For>
{/* <GroupedSelect
options={[
{
label: "Presets",
options: Object.keys(PRESET_FUNCS),
},
{
label: "Custom",
options: [...userFuncKeys, ADD_NEW_FUNC_KEY],
},
]}
value={renderFuncKey()}
setValue={(key) => {
if (key === ADD_NEW_FUNC_KEY) {
createAndSelectFunc("render function", PRESET_FUNCS.Square);
} else {
let storedCode;
if (PRESET_FUNCS.hasOwnProperty(key)) {
storedCode = PRESET_FUNCS[key as keyof typeof PRESET_FUNCS];
} else {
storedCode = localStorage.getItem(key);
if (storedCode == null) {
storedCode = `Failed to load ${key}`;
}
}
setRenderFuncKey(key);
trySetCode(storedCode);
</Preview>
)}
</For>
<For each={userFuncs}>
{(func) => (
<Preview
onClick={() => {
let storedCode = localStorage.getItem(func.key);
if (storedCode == null) {
storedCode = `Failed to load ${func.key}`;
}
setRenderFuncKey(func.key);
userSetCode(storedCode, false);
}}
label={func.key}
active={renderFuncKey() === func.key}
>
<img class="rounded-sm" src={func.thumb} />
</Preview>
)}
</For>
<Preview
onClick={() =>
createAndSelectFunc("custom", PRESET_MODULES.Square.code)
}
}}
/> */}
<Show when={!Object.keys(PRESET_MODULES).includes(renderFuncKey())}>
<IconButtonDialog
title={`Rename ${renderFuncKey()}`}
triggerTitle="Rename"
triggerChildren={<Pencil class="w-5 h-5" />}
onOpenAutoFocus={(e) => e.preventDefault()}
label="Create new"
active={false}
>
{(close) => {
const [rename, setRename] = createSignal(renderFuncKey());
const [duplicate, setDuplicate] = createSignal(false);
let ref: HTMLInputElement;
onMount(() => ref.focus());
return (
<>
<TextInput
class="mt-2"
ref={ref!}
defaultValue={rename()}
onChange={setRename}
onInput={() => duplicate() && setDuplicate(false)}
placeholder={renderFuncKey()}
/>
<div class="absolute p-1 text-sm text-red-600">
<Show when={duplicate()}>
{rename()} already exists.
</Show>
</div>
<FillButton
class="px-3 py-2 float-right mt-4"
// input onChange runs after focus lost, so onMouseDown is too early
onClick={() => {
if (rename() === renderFuncKey()) return close();
const userFuncKeys = Object.values(userFuncs).map(
(func) => func.key
);
if (
Object.keys(PRESET_MODULES).includes(rename()) ||
userFuncKeys.includes(rename())
) {
setDuplicate(true);
} else {
localStorage.removeItem(renderFuncKey());
localStorage.setItem(rename(), code());
setUserFuncs(
userFuncKeys.indexOf(renderFuncKey()),
"key",
rename()
);
localStorage.setItem(
USER_FUNC_KEYS_KEY,
userFuncKeys.join(",")
);
setRenderFuncKey(rename());
close();
}
}}
>
Confirm
</FillButton>
</>
);
}}
</IconButtonDialog>
<IconButtonDialog
title={`Delete ${renderFuncKey()}`}
triggerTitle="Delete"
triggerChildren={<Trash2 class="w-5 h-5" />}
>
{(close) => (
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<path style="fill:#222" d="M0 0h100v100H0z" />
<path
style="fill:#fff"
d="m55 25-10 1v17H26v13l1 1h19l1 18v1l10-1h1l-1-18h23V43H56V26l-1-1z"
/>
</svg>
</Preview>
</div>
</div>
<div class="flex flex-col gap-2 mb-4">
<For each={Object.entries(paramsSchema())}>
{([label, { type, ...props }]) => {
return (
<>
<p class="mb-4 text-sm">
Are you sure you want to delete this function?
</p>
<div class="flex justify-end gap-2">
<FillButton
onMouseDown={() => {
const userFuncKeys = Object.values(userFuncs).map(
(func) => func.key
);
setUserFuncs((funcs) =>
funcs.filter((func) => func.key !== renderFuncKey())
);
localStorage.removeItem(renderFuncKey());
localStorage.setItem(
USER_FUNC_KEYS_KEY,
userFuncKeys.join(",")
);
setRenderFuncKey("Square");
// @ts-expect-error renderSVG narrow to wider is fine b/c valid params
internalSetCode(PRESET_MODULES.Square);
close();
}}
>
Confirm
</FillButton>
<FlatButton onMouseDown={close}>Cancel</FlatButton>
<div class="flex justify-between">
<div class="text-sm py-2 w-48">{label}</div>
{/* @ts-expect-error lose type b/c type and props destructured */}
<Dynamic
component={PARAM_COMPONENTS[type]}
{...props}
value={params[label]}
setValue={(v: any) => setParams(label, v)}
/>
</div>
</>
)}
</IconButtonDialog>
</Show>
);
}}
</For>
</div>
</div>
<div class="flex flex-col gap-2 mb-4">
<For each={Object.entries(paramsSchema())}>
{([label, { type, ...props }]) => {
return (
<>
<div class="flex justify-between">
<div class="text-sm py-2 w-48">{label}</div>
{/* @ts-expect-error lose type b/c type and props destructured */}
<Dynamic
component={PARAM_COMPONENTS[type]}
{...props}
value={params[label]}
setValue={(v: any) => setParams(label, v)}
/>
</div>
</>
);
<CodeEditor
initialValue={code()}
onSave={(code) => {
if (Object.keys(PRESET_MODULES).includes(renderFuncKey())) {
createAndSelectFunc(renderFuncKey(), code);
} else {
userSetCode(code, true);
}
}}
</For>
error={compileError()}
clearError={() => setCompileError(null)}
/>
</div>
<CodeEditor
initialValue={code()}
onSave={(code) => {
if (Object.keys(PRESET_MODULES).includes(renderFuncKey())) {
createAndSelectFunc(renderFuncKey(), code);
} else {
userSetCode(code, true);
}
}}
error={compileError()}
clearError={() => setCompileError(null)}
/>
</Collapsible>
</div>
);
}
type PreviewProps = {
label: string;
children: JSX.Element;
onClick: () => void;
active: boolean;
};
function Preview(props: PreviewProps) {
return (
<button
class="rounded-sm focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base)"
onClick={props.onClick}
>
<div
classList={{
"h-24 w-24 rounded-sm checkboard": true,
"ring-2 ring-fore-base ring-offset-4 ring-offset-back-base":
props.active,
}}
>
{props.children}
</div>
<div class="pt-1 text-center text-sm w-24 whitespace-pre overflow-hidden text-ellipsis">
{props.label}
</div>
</button>
);
}

Wyświetl plik

@ -18,7 +18,7 @@ export function Settings() {
const { inputQr, setInputQr } = useQrContext();
return (
<div class="flex flex-col gap-2 py-2">
<div class="flex flex-col gap-2 py-4">
<div class="flex justify-between">
<div class="text-sm py-2">Encoding mode</div>
<Select

Wyświetl plik

@ -126,6 +126,8 @@ function RenderedQrCode() {
// race condition is unrealistic (maybe with http requests)
// and can't be solved without double buffering
await fallbackRender!(outputQr(), paramsCopy, ctx);
setCanvasDims({ width: ctx.canvas.width, height: ctx.canvas.height });
}
setRuntimeError(null);
@ -153,9 +155,11 @@ function RenderedQrCode() {
{runtimeError()}
</div>
</Show>
<div class="text-center">
{canvasDims().width}x{canvasDims().height} px
</div>
<Show when={getRenderCanvas() != null}>
<div class="text-center">
{canvasDims().width}x{canvasDims().height} px
</div>
</Show>
<div class="px-2 grid grid-cols-2 gap-y-2 text-sm">
<div class="">
Version

Wyświetl plik

@ -14,9 +14,9 @@ export default function Home() {
return (
<QrContextProvider>
<main class="max-w-screen-2xl mx-auto">
<div class="flex flex-wrap">
<Editor class="flex-1 flex-grow-3 flex flex-col gap-2 px-4 py-8" />
<QrPreview class="flex-1 flex-grow-2 min-w-300px sticky top-0 self-start px-4 py-8 flex flex-col gap-4" />
<div class="md:flex py-8">
<Editor class="flex-1 flex-grow-3 flex flex-col gap-2 px-4" />
<QrPreview class="flex-1 flex-grow-2 min-w-300px sticky top-0 self-start px-4 flex flex-col gap-4" />
</div>
</main>
</QrContextProvider>

Wyświetl plik

@ -1,6 +1,7 @@
import { defineConfig } from "unocss";
import transformerVariantGroup from "@unocss/transformer-variant-group";
export default defineConfig({
blocklist: ["m55"],
transformers: [transformerVariantGroup()],
theme: {
colors: {