kopia lustrzana https://github.com/zhengkyl/qrframe
sortable array + implicit any
rodzic
ef072f00dc
commit
f6bafd0e0f
|
@ -4,7 +4,7 @@ type Props = {
|
|||
};
|
||||
export function ColorInput(props: Props) {
|
||||
return (
|
||||
<label class="border rounded-md font-mono inline-flex items-center py-1.5 px-2 gap-1 cursor-pointer hover:bg-fore-base/5 focus-within:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base)">
|
||||
<label class="border rounded-md font-mono inline-flex items-center py-1.5 px-2 gap-1 cursor-pointer bg-back-base hover:bg-fore-base/5 focus-within:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base)">
|
||||
{props.value}
|
||||
<input
|
||||
class="w-0 h-0"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import X from "lucide-solid/icons/x";
|
||||
import { FlatButton } from "./Button";
|
||||
import { Show } from "solid-js";
|
||||
import { FlatButton } from "./Button";
|
||||
|
||||
type Props = {
|
||||
value: File | null;
|
||||
|
@ -12,7 +12,7 @@ export function ImageInput(props: Props) {
|
|||
return (
|
||||
<div class="inline-flex items-center gap-1">
|
||||
<input
|
||||
class="border rounded-md text-sm px-1 py-2 file:(bg-transparent border-none text-fore-base) hover:bg-fore-base/5 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base)"
|
||||
class="border rounded-md text-sm px-1 py-2 file:(bg-transparent border-none text-fore-base) bg-back-base hover:bg-fore-base/5 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base)"
|
||||
ref={input!}
|
||||
type="file"
|
||||
accept=".jpeg, .jpg, .png"
|
||||
|
|
|
@ -56,7 +56,7 @@ export function NumberInput(props: Props) {
|
|||
</Slider.Track>
|
||||
</Slider>
|
||||
<NumberField
|
||||
class="relative rounded-md focus-within:(ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) hover:bg-fore-base/5"
|
||||
class="relative rounded-md focus-within:(ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) bg-back-base hover:bg-fore-base/5"
|
||||
minValue={props.min}
|
||||
maxValue={props.max}
|
||||
rawValue={focused() ? rawValue() : props.value}
|
||||
|
|
|
@ -27,7 +27,6 @@ export function Select(props: Props) {
|
|||
props.setValue(retainedValue());
|
||||
}
|
||||
}}
|
||||
// @ts-expect-error e is typed wtf
|
||||
onKeyDown={(e) => {
|
||||
const index = props.options.indexOf(props.value);
|
||||
switch (e.key) {
|
||||
|
@ -65,7 +64,7 @@ export function Select(props: Props) {
|
|||
</KSelect.Item>
|
||||
)}
|
||||
>
|
||||
<KSelect.Trigger class="leading-tight w-full inline-flex justify-between items-center rounded-md border pl-3 pr-2 py-2 focus:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) hover:bg-fore-base/5">
|
||||
<KSelect.Trigger class="leading-tight w-full inline-flex justify-between items-center rounded-md border pl-3 pr-2 py-2 focus:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) bg-back-base hover:bg-fore-base/5">
|
||||
<KSelect.Value>
|
||||
{(state) => state.selectedOption() as string}
|
||||
</KSelect.Value>
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
import { Button } from "@kobalte/core/button";
|
||||
import { DropdownMenu } from "@kobalte/core/dropdown-menu";
|
||||
import { NumberField } from "@kobalte/core/number-field";
|
||||
import { Popover } from "@kobalte/core/popover";
|
||||
import ChevronDown from "lucide-solid/icons/chevron-down";
|
||||
import Download from "lucide-solid/icons/download";
|
||||
import { createSignal } from "solid-js";
|
||||
import { FillButton } from "./Button";
|
||||
|
||||
type Props = {
|
||||
onClick: (width: number, height: number) => void;
|
||||
onClick: (width, height) => void;
|
||||
};
|
||||
export function SplitButton(props: Props) {
|
||||
const [customWidth, setCustomWidth] = createSignal(1000);
|
||||
const [customHeight, setCustomHeight] = createSignal(1000);
|
||||
|
||||
const onClick = (width: number, height: number) => {
|
||||
const onClick = (width, height) => {
|
||||
props.onClick(width, height);
|
||||
setOpen(false)
|
||||
setOpen(false);
|
||||
};
|
||||
const [open, setOpen] = createSignal(false);
|
||||
return (
|
||||
|
@ -24,31 +24,24 @@ export function SplitButton(props: Props) {
|
|||
<Download size={20} />
|
||||
PNG
|
||||
</Button>
|
||||
<DropdownMenu gutter={4} open={open()} onOpenChange={setOpen}>
|
||||
<DropdownMenu.Trigger
|
||||
class="dropdown-menu__trigger border rounded-md rounded-s-none hover:bg-fore-base/5 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) p-2"
|
||||
>
|
||||
<DropdownMenu.Icon class="block data-[expanded]:rotate-180 transition-transform">
|
||||
<ChevronDown size={20} />
|
||||
</DropdownMenu.Icon>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="dropdown-menu__content bg-back-base rounded-md border p-2 outline-none min-w-150px leading-tight">
|
||||
<Popover gutter={4} open={open()} onOpenChange={setOpen}>
|
||||
<Popover.Trigger class="group border rounded-md rounded-s-none hover:bg-fore-base/5 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) p-2">
|
||||
<ChevronDown
|
||||
size={20}
|
||||
class="block group-data-[expanded]:rotate-180 transition-transform"
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content class="bg-back-base rounded-md border p-2 outline-none min-w-150px leading-tight">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-sm font-bold">Select size</div>
|
||||
<FillButton
|
||||
class="w-full p-2"
|
||||
onClick={() => onClick(300, 300)}
|
||||
>
|
||||
<FillButton class="w-full p-2" onClick={() => onClick(300, 300)}>
|
||||
300x300
|
||||
</FillButton>
|
||||
<FillButton
|
||||
class="w-full p-2"
|
||||
onClick={() => onClick(500, 500)}
|
||||
>
|
||||
<FillButton class="w-full p-2" onClick={() => onClick(500, 500)}>
|
||||
500x500
|
||||
</FillButton>
|
||||
<DropdownMenu.Separator class="dropdown-menu__separator" />
|
||||
<hr />
|
||||
<div class="text-sm font-bold">Custom</div>
|
||||
<div class="flex gap-2">
|
||||
<MenuNumberInput
|
||||
|
@ -71,9 +64,9 @@ export function SplitButton(props: Props) {
|
|||
Download custom
|
||||
</FillButton>
|
||||
</div>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -88,7 +81,7 @@ type NumberProps = {
|
|||
function MenuNumberInput(props: NumberProps) {
|
||||
const [rawValue, setRawValue] = createSignal(props.value);
|
||||
|
||||
const safeSetValue = (value: number) => {
|
||||
const safeSetValue = (value) => {
|
||||
setRawValue(value);
|
||||
if (
|
||||
value < props.min ||
|
||||
|
|
|
@ -17,7 +17,7 @@ export function Switch(props: Props) {
|
|||
<KSwitch.Label class="text-sm">{props.label}</KSwitch.Label>
|
||||
)}
|
||||
<KSwitch.Input class="peer" />
|
||||
<KSwitch.Control class="inline-flex items-center w-11 h-6 px-0.5 bg-fore-base/20 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.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.Control>
|
||||
</KSwitch>
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
import Minus from "lucide-solid/icons/minus";
|
||||
import Plus from "lucide-solid/icons/plus";
|
||||
import { createSignal, For, Index, Show } from "solid-js";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
import { PARAM_COMPONENTS } from "~/lib/params";
|
||||
import { useQrContext } from "~/lib/QrContext";
|
||||
import { FlatButton } from "../Button";
|
||||
import { transformStyle, useDragDropContext } from "@thisbeyond/solid-dnd";
|
||||
import {
|
||||
DragDropProvider,
|
||||
DragDropSensors,
|
||||
DragOverlay,
|
||||
SortableProvider,
|
||||
createSortable,
|
||||
closestCenter,
|
||||
} from "@thisbeyond/solid-dnd";
|
||||
import GripVertical from "lucide-solid/icons/grip-vertical";
|
||||
|
||||
export function ParamsEditor() {
|
||||
const { paramsSchema, params, setParams } = useQrContext();
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-2 mb-4">
|
||||
<For each={Object.entries(paramsSchema())}>
|
||||
{([label, { type, ...other }]) => {
|
||||
if (type === "Array") {
|
||||
return <ArrayParam label={label} other={other} />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div class="flex justify-between">
|
||||
<div class="text-sm py-2 w-36 shrink-0">{label}</div>
|
||||
<Dynamic
|
||||
component={PARAM_COMPONENTS[type]}
|
||||
{...other}
|
||||
value={params[label]}
|
||||
setValue={(v: any) => setParams(label, v)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ArrayParam({ label, other }) {
|
||||
const { params, setParams } = useQrContext();
|
||||
|
||||
// 0 is falsey and not a valid key
|
||||
const idFromIndex = (i) => i + 1;
|
||||
const indexFromId = (k) => k - 1;
|
||||
const [activeId, setActiveId] = createSignal(null);
|
||||
|
||||
const onDragStart = ({ draggable }) => setActiveId(draggable.id);
|
||||
const onDragEnd = ({ draggable, droppable }) => {
|
||||
const fromIndex = indexFromId(draggable.id);
|
||||
const toIndex = indexFromId(droppable.id);
|
||||
if (fromIndex !== toIndex) {
|
||||
setParams(label, (prev: any[]) => {
|
||||
const updatedItems = prev.slice();
|
||||
updatedItems.splice(toIndex, 0, ...updatedItems.splice(fromIndex, 1));
|
||||
return updatedItems;
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div class="grid grid-cols-[144px_1fr] justify-items-end gap-y-2">
|
||||
<div class="text-sm py-2 w-36">{label}</div>
|
||||
<div class="flex gap-1">
|
||||
<Show when={other.resizable}>
|
||||
<FlatButton
|
||||
class="p-1.5"
|
||||
onClick={() => setParams(label, (prev: any[]) => prev.slice(0, -1))}
|
||||
>
|
||||
<Minus />
|
||||
</FlatButton>
|
||||
<FlatButton
|
||||
class="p-1.5"
|
||||
onClick={() =>
|
||||
setParams(label, (prev: any[]) => [...prev, other.props.default])
|
||||
}
|
||||
>
|
||||
<Plus />
|
||||
</FlatButton>
|
||||
</Show>
|
||||
</div>
|
||||
<DragDropProvider
|
||||
onDragStart={onDragStart}
|
||||
// @ts-expect-error droppable always exists
|
||||
onDragEnd={onDragEnd}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<DragDropSensors />
|
||||
<SortableProvider
|
||||
ids={Array.from({ length: params[label].length }, (_, i) =>
|
||||
idFromIndex(i)
|
||||
)}
|
||||
>
|
||||
<Index each={params[label]}>
|
||||
{(v, i) => {
|
||||
const sortable = createSortable(idFromIndex(i));
|
||||
const [state] = useDragDropContext()!;
|
||||
return (
|
||||
<>
|
||||
<div class="text-sm py-2 pl-4 w-full text-left">{i}</div>
|
||||
<div
|
||||
class="flex w-full justify-end items-center"
|
||||
classList={{
|
||||
"opacity-25": sortable.isActiveDraggable,
|
||||
"transition-transform": !!state.active.draggable,
|
||||
}}
|
||||
ref={sortable.ref}
|
||||
style={transformStyle(sortable.transform)}
|
||||
>
|
||||
<Dynamic
|
||||
component={
|
||||
PARAM_COMPONENTS[
|
||||
other.props.type as keyof typeof PARAM_COMPONENTS
|
||||
]
|
||||
}
|
||||
{...other.props}
|
||||
value={v()}
|
||||
setValue={(v: any) => setParams(label, i, v)}
|
||||
/>
|
||||
<div class="px-1 cursor-move" {...sortable.dragActivators}>
|
||||
<GripVertical />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Index>
|
||||
</SortableProvider>
|
||||
<DragOverlay>
|
||||
<div class="flex w-full justify-end items-center">
|
||||
<Dynamic
|
||||
component={
|
||||
PARAM_COMPONENTS[
|
||||
other.props.type as keyof typeof PARAM_COMPONENTS
|
||||
]
|
||||
}
|
||||
{...other.props}
|
||||
value={params[label][indexFromId(activeId()!)]}
|
||||
/>
|
||||
<div class="px-1 cursor-move">
|
||||
<GripVertical />
|
||||
</div>
|
||||
</div>
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -29,6 +29,7 @@ import { Settings } from "./Settings";
|
|||
import { clearToasts, toastError } from "../ErrorToasts";
|
||||
import Minus from "lucide-solid/icons/minus";
|
||||
import Plus from "lucide-solid/icons/plus";
|
||||
import { ParamsEditor } from "./ParamsEditor";
|
||||
|
||||
type Props = {
|
||||
class?: string;
|
||||
|
@ -55,7 +56,6 @@ export function Editor(props: Props) {
|
|||
setInputQr,
|
||||
paramsSchema,
|
||||
setParamsSchema,
|
||||
params,
|
||||
setParams,
|
||||
renderKey,
|
||||
setRenderKey,
|
||||
|
@ -429,79 +429,7 @@ export function Editor(props: Props) {
|
|||
</Preview>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 mb-4">
|
||||
<For each={Object.entries(paramsSchema())}>
|
||||
{([label, { type, ...props }]) => {
|
||||
if (type === "Array") {
|
||||
return (
|
||||
<div>
|
||||
<div class="grid grid-cols-[144px_1fr] justify-items-end gap-y-2">
|
||||
<div class="text-sm py-2 w-36">{label}</div>
|
||||
<div class="flex gap-1">
|
||||
<Show when={props.resizable}>
|
||||
<FlatButton
|
||||
class="p-1.5"
|
||||
onClick={() =>
|
||||
setParams(label, (prev: any[]) =>
|
||||
prev.slice(0, -1)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Minus />
|
||||
</FlatButton>
|
||||
<FlatButton
|
||||
class="p-1.5"
|
||||
onClick={() =>
|
||||
setParams(label, (prev: any[]) => [
|
||||
...prev,
|
||||
props.props.default,
|
||||
])
|
||||
}
|
||||
>
|
||||
<Plus />
|
||||
</FlatButton>
|
||||
</Show>
|
||||
</div>
|
||||
<Index each={params[label]}>
|
||||
{(v, i) => (
|
||||
<>
|
||||
<div class="text-sm py-2 pl-4 w-full text-left">
|
||||
{i}
|
||||
</div>
|
||||
<Dynamic
|
||||
component={
|
||||
PARAM_COMPONENTS[
|
||||
props.props
|
||||
.type as keyof typeof PARAM_COMPONENTS
|
||||
]
|
||||
}
|
||||
{...props.props}
|
||||
value={v()}
|
||||
setValue={(v: any) => setParams(label, i, v)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Index>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div class="flex justify-between">
|
||||
<div class="text-sm py-2 w-36 shrink-0">{label}</div>
|
||||
<Dynamic
|
||||
component={PARAM_COMPONENTS[type]}
|
||||
{...props}
|
||||
value={params[label]}
|
||||
setValue={(v: any) => setParams(label, v)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
<ParamsEditor/>
|
||||
<CodeEditor
|
||||
initialValue={code()}
|
||||
onSave={(code, updateThumbnail) => {
|
||||
|
|
|
@ -123,7 +123,6 @@ function parseField(value: any) {
|
|||
() => value.props.default
|
||||
);
|
||||
} else {
|
||||
// @ts-expect-error adding default, type validated above
|
||||
value.default = PARAM_DEFAULTS[value.type];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export function debounce(func: any, delay: number) {
|
||||
export function debounce(func, delay) {
|
||||
let timer: any;
|
||||
return (...args: any) => {
|
||||
clearTimeout(timer);
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"types": ["vinxi/types/client"],
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
|
|
|
@ -13,6 +13,7 @@ export default defineConfig({
|
|||
back: {
|
||||
base: "#0e0f0f",
|
||||
subtle: "#1a1b1c",
|
||||
distinct: "#3d3e3e"
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
|
|
Ładowanie…
Reference in New Issue