[Certs] Migrate UI from failed Electron rewrite

master
Araozu 2023-08-03 17:50:18 -05:00
parent 1eccc62056
commit d70d1edbd6
18 changed files with 464 additions and 92 deletions

View File

@ -12,19 +12,13 @@ export function Certs() {
onMount(ensureColors); onMount(ensureColors);
return ( return (
<div> <div class="grid grid-cols-[18rem_25rem_1fr]">
<h1 class="px-4 py-2 text-2xl font-bold">
Registrar certificado
</h1>
<Search setPerson={setPerson}/> <Search setPerson={setPerson}/>
<div class="grid grid-cols-2 gap-2"> <NewRegister
<Registers person={person()} lastUpdate={lastUpdate()} /> personId={person()?.id ?? null}
<NewRegister onSuccess={() => setLastUpdate((x) => x + 1)}
personId={person()?.id ?? -1} />
onSuccess={() => setLastUpdate((x) => x + 1)} <Registers person={person()} lastUpdate={lastUpdate()} />
/>
</div>
</div> </div>
); );
} }

View File

@ -0,0 +1,9 @@
import type { JSX } from "solid-js";
export function FilledCard(props: {children?: Array<JSX.Element> | JSX.Element, class?: string}) {
return (
<div class={`bg-c-surface-variant text-c-on-surface-variant rounded-xl m-2 shadow ${props.class ?? ""}`}>
{props.children}
</div>
);
}

View File

@ -2,6 +2,8 @@ import { createSignal, Show } from "solid-js";
import { SearchableSelect } from "./NewRegister/SearchableSelect"; import { SearchableSelect } from "./NewRegister/SearchableSelect";
import { JSX } from "solid-js/jsx-runtime"; import { JSX } from "solid-js/jsx-runtime";
import { subjects } from "../subjects"; import { subjects } from "../subjects";
import { FilledCard } from "./FilledCard";
import { RegisterPreview } from "./NewRegister/RegisterPreview";
type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & { type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & {
@ -34,12 +36,48 @@ type RegisterFn = (personId: number, subjectId: number, date: string) => Promise
const defaultLabelText = "3. Crear nuevos registros"; const defaultLabelText = "3. Crear nuevos registros";
type TabType = "Presets" | "Manual";
export function NewRegister(props: { export function NewRegister(props: {
personId: number | null, personId: number | null,
onSuccess: () => void, onSuccess: () => void,
registerFn?: RegisterFn, registerFn?: RegisterFn,
labelText?: string, labelText?: string,
}) { }) {
const [active, setActive] = createSignal<TabType>("Manual");
const [selections, setSelections] = createSignal<Array<[number, string]>>([]);
const onRegister = () => {
setSelections([]);
props.onSuccess();
};
return (
<div class="h-screen overflow-y-scroll">
<FilledCard class="border border-c-outline overflow-hidden">
<h2 class="p-4 font-bold text-xl">{props.labelText ?? defaultLabelText}</h2>
<RegisterTabs active={active()} setActive={setActive} />
<div class="bg-c-surface p-4 h-[22rem]">
<Show when={active() === "Presets"}>
<p>Proximamente...</p>
</Show>
<Show when={active() === "Manual"}>
<ManualCerts personId={props.personId} onAdd={(v) => setSelections((x) => [...x, v])} />
</Show>
</div>
</FilledCard>
<RegisterPreview
selections={selections()}
personId={props.personId}
onDelete={(deleteId) => setSelections((s) => [...s.filter(([id]) => id !== deleteId)])}
onRegister={onRegister}
/>
</div>
);
const [error, setError] = createSignal(""); const [error, setError] = createSignal("");
const [loading, setLoading] = createSignal(false); const [loading, setLoading] = createSignal(false);
// Used to update SearchableSelect.tsx manually // Used to update SearchableSelect.tsx manually
@ -138,3 +176,104 @@ export function NewRegister(props: {
</div> </div>
); );
} }
function RegisterTabs(props: {active: TabType, setActive: (v: TabType) => void}) {
const presetsClasses = () => ((props.active === "Presets") ? "font-bold border-c-primary" : "border-c-transparent");
const manualClasses = () => ((props.active === "Manual") ? "font-bold border-c-primary" : "border-c-transparent");
return (
<div class="grid grid-cols-2">
<button
class={`py-2 border-b-4 ${presetsClasses()}`}
onclick={() => props.setActive("Presets")}
>
Presets
</button>
<button
class={`py-2 border-b-4 ${manualClasses()}`}
onclick={() => props.setActive("Manual")}
>
Manual
</button>
</div>
);
}
function ManualCerts(props: {personId: number | null, onAdd: (v: [number, string]) => void}) {
// Used to update SearchableSelect.tsx manually
const [count, setCount] = createSignal(0);
const [error, setError] = createSignal("");
const [selectedSubject, setSelectedSubject] = createSignal<number | null>(null);
const datePicker = (
<input
id="create-date"
class="bg-c-surface text-c-on-surface border border-c-outline rounded-lg p-2"
type="date"
/>
);
const register: HTMLEventFn = async(ev) => {
ev.preventDefault();
const subject = selectedSubject();
const date = (datePicker as HTMLInputElement).value;
if (subject === null) {
setError("Selecciona un curso");
setTimeout(() => setError(""), 5000);
return;
}
if (date === "") {
setError("Selecciona una fecha");
setTimeout(() => setError(""), 5000);
return;
}
props.onAdd([subject, date]);
// This is used to update & refresh the <SearchableSelect> component
setCount((x) => x + 1);
};
console.log(`Person ID: ${props.personId}`);
return (
<>
<form onsubmit={register}>
<div>
<SearchableSelect
subjects={subjects()}
onChange={setSelectedSubject}
count={count()}
/>
</div>
<div class="relative my-4">
{datePicker}
<label for="create-date" class="absolute -top-2 left-2 text-xs bg-c-surface px-1">Fecha</label>
</div>
<input
class="bg-c-primary text-c-on-primary px-4 py-2 rounded-full cursor-pointer
disabled:opacity-50 disabled:cursor-not-allowed"
type="submit"
value="Agregar"
disabled={props.personId === null}
/>
</form>
<p
class="my-2 p-1 rounded w-fit mx-4 bg-c-error text-c-on-error"
style={{opacity: error() === "" ? "0" : "1", "user-select": "none"}}
>
{error()}&nbsp;
</p>
</>
);
}

View File

@ -0,0 +1,95 @@
import { FilledCard } from "../FilledCard";
import { For } from "solid-js";
import { XIcon } from "src/views/icons/XIcon";
import { subjects } from "src/views/subjects";
function isoDateToLocalDate(date: string): string {
const [,month, day] = /\d{4}-(\d{2})-(\d{2})/.exec(date) ?? "";
return `${day}/${month}`;
}
export function RegisterPreview(props: {selections: Array<[number, string]>, personId: number | null, onDelete: (v: number) => void, onRegister: () => void}) {
const submit = async() => {
console.log("Submit...");
for (const [courseId, date] of props.selections) {
const result = await defaultNewRegisterFn(
props.personId ?? -1,
courseId,
date,
);
if (result === null) {
console.log("Success");
} else {
console.log(`error. ${result}`);
}
}
props.onRegister();
};
return (
<FilledCard class="border border-c-outline overflow-hidden">
<h2 class="p-4 font-bold text-xl">Confirmar registro</h2>
<div class="bg-c-surface p-4">
<For each={props.selections}>
{([courseId, date]) => <Register courseId={courseId} date={date} onDelete={props.onDelete} />}
</For>
<button
class="bg-c-primary text-c-on-primary px-4 py-2 rounded-full cursor-pointer mt-4
disabled:opacity-50 disabled:cursor-not-allowed"
type="button"
disabled={props.selections.length === 0}
onclick={submit}
>
Registrar los {props.selections.length} cursos
</button>
</div>
</FilledCard>
);
}
function Register(props: {courseId: number, date: string, onDelete: (v: number) => void}) {
const courseName = () => {
const courses = subjects();
return courses.find((c) => c.id === props.courseId)?.nombre ?? "!";
};
return (
<div class="grid grid-cols-[auto_4rem_1.5rem] py-1 px-2 rounded-md border border-c-outline my-1">
{courseName()}
<span class="font-mono">{isoDateToLocalDate(props.date)}</span>
<button
class="hover:bg-c-surface-variant rounded-md"
onclick={() => props.onDelete(props.courseId)}
>
<XIcon fill="var(--c-on-surface)" />
</button>
</div>
);
}
async function defaultNewRegisterFn(personId: number, subjectId: number, date: string): Promise<null | string> {
const response = await fetch("/certificate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
personId,
subjectId,
date,
}),
});
if (response.ok) {
return null;
} else {
const data = await response.json();
return JSON.stringify(data);
}
}

View File

@ -85,7 +85,7 @@ export function SearchableSelect(props: {
{inputElement} {inputElement}
<br/> <br/>
<div <div
class="border-c-outline border-2 rounded overflow-y-scroll h-[18rem]" class="border-c-outline border-2 rounded overflow-y-scroll h-[10rem]"
> >
<For each={filteredOptions()}> <For each={filteredOptions()}>
{(s) => ( {(s) => (

View File

@ -37,55 +37,8 @@ export function Registers(props: { person: Person | null, lastUpdate: number })
return ( return (
<div class="p-4"> <div class="p-4">
<h2 class="mb-4 font-bold text-xl">2. Revisar registros actuales</h2>
<div class="px-4"> <div class="px-4">
<Show when={props.person !== null}> <Show when={props.person !== null}>
<div>
<input
type="text"
disabled
value={props.person?.nombreCompleto}
class="bg-c-background text-c-on-background border-c-outline border rounded px-2 py-1
disabled:cursor-text w-full"
/>
</div>
<div class="grid grid-cols-[3fr_3fr_2fr_2fr_1fr_1fr] gap-1">
<CopyButton
copyText={`${props.person!.nombres} ${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno}`}
>
NOM <b>AP</b>
<i class="ph ph-clipboard-text text-2xl align-middle ml-2"></i>
</CopyButton>
<CopyButton
copyText={`${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno} ${props.person!.nombres}`}
>
<b>AP</b> NOM
<i class="ph ph-clipboard-text text-2xl align-middle ml-2"></i>
</CopyButton>
<CopyButton
copyText={`${props.person!.nombres}`}
>
NOM
</CopyButton>
<CopyButton
copyText={`${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno}`}
>
<b>AP</b>
</CopyButton>
<CopyButton
copyText={`${props.person!.apellidoPaterno}`}
>
<b><i>Ap</i></b>
</CopyButton>
<CopyButton
copyText={`${props.person!.apellidoMaterno}`}
>
<b><i>Am</i></b>
</CopyButton>
</div>
<p <p
class="my-2" class="my-2"
style={{ display: loading() ? "block" : "none" }} style={{ display: loading() ? "block" : "none" }}

View File

@ -3,6 +3,9 @@ import { JSX } from "solid-js/jsx-runtime";
import { Person } from "../../types/Person"; import { Person } from "../../types/Person";
import { RegisterPerson } from "./Search/RegisterPerson"; import { RegisterPerson } from "./Search/RegisterPerson";
import QR from "qrcode"; import QR from "qrcode";
import { CopyIcon } from "../icons/CopyIcon";
import { MagnifyingGlassIcon } from "../icons/MagnifyingGlassIcon";
import { XIcon } from "../icons/XIcon";
type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & { type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & {
submitter: HTMLElement; submitter: HTMLElement;
@ -19,6 +22,7 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
const [error, setError] = createSignal(""); const [error, setError] = createSignal("");
const [warning, setWarning] = createSignal(""); const [warning, setWarning] = createSignal("");
const [qrBase64, setQrBase64] = createSignal<string | null>(null); const [qrBase64, setQrBase64] = createSignal<string | null>(null);
const [persona, setPersona] = createSignal<Person | null>(null);
// Update QR // Update QR
createEffect(() => { createEffect(() => {
@ -53,6 +57,7 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
const response = await fetch(`/person/${dni()}`); const response = await fetch(`/person/${dni()}`);
const body = await response.json(); const body = await response.json();
if (response.ok) { if (response.ok) {
setPersona(body);
props.setPerson(body); props.setPerson(body);
} else if (response.status === 404) { } else if (response.status === 404) {
console.error(body); console.error(body);
@ -69,30 +74,55 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
setLoading(false); setLoading(false);
}; };
return ( const nombresYApellidos = () => {
<div class="p-4 grid" style={{"grid-template-columns": "20rem auto"}}> const p = persona();
<div> if (p === null) {
<h2 class="my-2 font-bold text-xl">1. Buscar persona</h2> return "";
<form onSubmit={searchDNI} class="px-4"> }
<label for="search-dni">DNI</label> return `${p.nombres} ${p.apellidoPaterno} ${p.apellidoMaterno}`;
<br /> };
<InputBox dni={dni()} setDni={setDni} loading={loading()} />
<br /> const apellidosYNombres = () => {
<input const p = persona();
class="bg-c-primary text-c-on-primary px-4 py-2 rounded-md cursor-pointer if (p === null) {
disabled:opacity-50 disabled:cursor-not-allowed" return "";
type="submit" }
value="Buscar" return `${p.apellidoPaterno} ${p.apellidoMaterno} ${p.nombres}`;
disabled={loading()} };
/>
const apellidos = () => {
const p = persona();
if (p === null) {
return "";
}
return `${p.apellidoPaterno} ${p.apellidoMaterno}`;
};
const nombres = () => {
const p = persona();
if (p === null) {
return "";
}
return p.nombres;
};
return (
<div>
<div class="text-center">
<div class="my-4 inline-block w-[10rem] h-[10rem] rounded-lg bg-c-surface-variant overflow-hidden">
<img class={`${qrBase64() === null ? "hidden" : ""} inline-block w-[10rem] h-[10rem]`} src={qrBase64() ?? ""} />
</div>
</div>
<div>
<form onSubmit={searchDNI} class="px-4">
<InputBox dni={dni()} setDni={setDni} loading={loading()} />
</form> </form>
<p <p
class="my-2 p-1 rounded w-fit mx-4 bg-c-error text-c-on-error" class="my-2 p-1 rounded w-fit mx-4 bg-c-error text-c-on-error"
style={{display: error() === "" ? "none" : "block"}} style={{display: error() === "" ? "none" : "block"}}
> >
Error: Error:
<br /> <br />
{error()} {error()}
</p> </p>
@ -106,13 +136,45 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
{warning()} {warning()}
</p> </p>
<br />
<div class={`${persona() === null ? "opacity-50 cursor-not-allowed" : ""}`}>
<MaterialLabel text={persona()?.apellidoPaterno ?? null} resource="Apellido Paterno" />
<MaterialLabel text={persona()?.apellidoMaterno ?? null} resource="Apellido Materno" />
<MaterialLabel text={persona()?.nombres ?? null} resource="Nombres" />
<div class={"relative max-w-[14rem] mx-auto my-6"}>
<CopyButton copyText={nombresYApellidos()}>
<CopyIcon fill="var(--c-on-primary)" />
&nbsp;
Nombres y <b>Apellidos</b>
</CopyButton>
<CopyButton copyText={apellidosYNombres()}>
<CopyIcon fill="var(--c-on-primary)" />
&nbsp;
<b>Apellidos</b> y Nombres
</CopyButton>
<div class="grid grid-cols-2 gap-2">
<CopyButton copyText={apellidos()}>
<CopyIcon fill="var(--c-on-primary)" />
&nbsp;
<b>Apellidos</b>
</CopyButton>
<CopyButton copyText={nombres()}>
<CopyIcon fill="var(--c-on-primary)" />
&nbsp;
Nombres
</CopyButton>
</div>
</div>
</div>
<Show when={warning() !== ""}> <Show when={warning() !== ""}>
<RegisterPerson dni={dni()} onSuccess={search} /> <RegisterPerson dni={dni()} onSuccess={search} />
</Show> </Show>
</div> </div>
<div>
<img src={qrBase64() ?? ""} height="150" width="150" />
</div>
</div> </div>
); );
} }
@ -122,12 +184,10 @@ function InputBox(props: {
dni: string, dni: string,
setDni: (v: string) => void, setDni: (v: string) => void,
}) { }) {
const [successAnimation, setSuccessAnimation] = createSignal(false);
const inputElement = ( const inputElement = (
<input <input
id="search-dni" id="search-dni"
class="bg-c-background text-c-on-background border-c-outline border-2 rounded px-2 py-1 class="bg-c-background text-c-on-background border-c-outline border-2 rounded px-2 py-1 w-full
invalid:border-c-error invalid:text-c-error invalid:border-c-error invalid:text-c-error
focus:border-c-primary outline-none font-mono focus:border-c-primary outline-none font-mono
disabled:opacity-50 disabled:cursor-not-allowed" disabled:opacity-50 disabled:cursor-not-allowed"
@ -148,8 +208,6 @@ function InputBox(props: {
if (props.dni.length === 8) { if (props.dni.length === 8) {
navigator.clipboard.writeText(props.dni); navigator.clipboard.writeText(props.dni);
setSuccessAnimation(true);
setTimeout(() => setSuccessAnimation(false), 1000);
} }
}; };
@ -161,18 +219,86 @@ function InputBox(props: {
}; };
return ( return (
<div class="grid gap-2 grid-cols-[10rem_3rem_3rem]"> <div class="relative max-w-[14rem] mx-auto">
{inputElement} {inputElement}
<label for="search-dni" class="absolute -top-2 left-2 text-xs bg-c-surface px-1">DNI</label>
<button <button
class={`${successAnimation() ? "bg-c-success" : "bg-c-primary"} rounded transition-colors`} class="absolute top-1 right-[3.75rem] rounded hover:bg-c-surface-variant"
onclick={copyToClipboard}
type="button"
> >
<i class={`${successAnimation() ? "text-c-on-success" : "text-c-on-primary"} ph ph-clipboard-text text-2xl align-middle`}></i> <MagnifyingGlassIcon fill="var(--c-on-surface)" />
</button> </button>
<button class="bg-c-error rounded" onclick={clearDni} type="button"> <button
<i class="ph ph-trash text-2xl text-c-on-primary align-middle"></i> type="button"
class="absolute top-1 right-8 rounded hover:bg-c-surface-variant"
onclick={copyToClipboard}
>
<CopyIcon fill="var(--c-on-surface)" />
</button>
<button
type="button"
class="absolute top-1 right-1 rounded hover:bg-c-surface-variant"
onclick={clearDni}
>
<XIcon fill="var(--c-on-surface)" />
</button> </button>
</div> </div>
); );
} }
function MaterialLabel(props: {text: string | null, resource: string}) {
const copyToClipboard: HTMLButtonEvent = (ev) => {
ev.preventDefault();
if (props.text !== null) {
navigator.clipboard.writeText(props.text);
}
};
return (
<div class="relative max-w-[14rem] mx-auto my-6">
<label for="search-dni" class="absolute -top-2 left-2 text-xs bg-c-surface px-1 select-none">{props.resource}</label>
<span
class="bg-c-background text-c-on-background border-c-outline
border-2 rounded px-2 py-1 w-full inline-block font-mono
disabled:opacity-50 disabled:cursor-not-allowed"
>
{props.text ?? ""}
<span class="select-none">&nbsp;</span>
</span>
<button
type="button"
class="absolute top-1 right-1 rounded hover:bg-c-surface-variant"
onclick={copyToClipboard}
>
<CopyIcon fill="var(--c-on-surface)" />
</button>
</div>
);
}
function CopyButton(props: {copyText: string, children: Array<JSX.Element> | JSX.Element}) {
const [successAnimation, setSuccessAnimation] = createSignal(false);
const onclick = () => {
if (props.copyText !== "") {
navigator.clipboard.writeText(props.copyText);
setSuccessAnimation(true);
setTimeout(() => setSuccessAnimation(false), 500);
}
};
return (
<button
onclick={onclick}
class={
`${successAnimation() ? "bg-c-success text-c-on-success" : "bg-c-primary text-c-on-primary"
} rounded-lg transition-colors py-1 my-1 relative overflow-hidden inline-block w-full`
}
>
{props.children}
</button>
);
}

View File

@ -0,0 +1,5 @@
export function CopyIcon(props: {fill: string}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function DocxIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M52,144H36a8,8,0,0,0-8,8v56a8,8,0,0,0,8,8H52a36,36,0,0,0,0-72Zm0,56H44V160h8a20,20,0,0,1,0,40Zm169.53-4.91a8,8,0,0,1,.25,11.31A30.06,30.06,0,0,1,200,216c-17.65,0-32-16.15-32-36s14.35-36,32-36a30.06,30.06,0,0,1,21.78,9.6,8,8,0,0,1-11.56,11.06A14.24,14.24,0,0,0,200,160c-8.82,0-16,9-16,20s7.18,20,16,20a14.24,14.24,0,0,0,10.22-4.66A8,8,0,0,1,221.53,195.09ZM128,144c-17.65,0-32,16.15-32,36s14.35,36,32,36,32-16.15,32-36S145.65,144,128,144Zm0,56c-8.82,0-16-9-16-20s7.18-20,16-20,16,9,16,20S136.82,200,128,200ZM48,120a8,8,0,0,0,8-8V40h88V88a8,8,0,0,0,8,8h48v16a8,8,0,0,0,16,0V88a8,8,0,0,0-2.34-5.66l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v72A8,8,0,0,0,48,120ZM160,51.31,188.69,80H160Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function DownloadIcon(props: {fill: string}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H72a8,8,0,0,1,0,16H32v64H224V136H184a8,8,0,0,1,0-16h40A16,16,0,0,1,240,136Zm-117.66-2.34a8,8,0,0,0,11.32,0l48-48a8,8,0,0,0-11.32-11.32L136,108.69V24a8,8,0,0,0-16,0v84.69L85.66,74.34A8,8,0,0,0,74.34,85.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function HomeIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M218.83,103.77l-80-75.48a1.14,1.14,0,0,1-.11-.11,16,16,0,0,0-21.53,0l-.11.11L37.17,103.77A16,16,0,0,0,32,115.55V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V115.55A16,16,0,0,0,218.83,103.77ZM208,208H48V115.55l.11-.1L128,40l79.9,75.43.11.1Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function KeyIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M48,56V200a8,8,0,0,1-16,0V56a8,8,0,0,1,16,0Zm84,54.5L112,117V96a8,8,0,0,0-16,0v21L76,110.5a8,8,0,0,0-5,15.22l20,6.49-12.34,17a8,8,0,1,0,12.94,9.4l12.34-17,12.34,17a8,8,0,1,0,12.94-9.4l-12.34-17,20-6.49A8,8,0,0,0,132,110.5ZM238,115.64A8,8,0,0,0,228,110.5L208,117V96a8,8,0,0,0-16,0v21l-20-6.49a8,8,0,0,0-4.95,15.22l20,6.49-12.34,17a8,8,0,1,0,12.94,9.4l12.34-17,12.34,17a8,8,0,1,0,12.94-9.4l-12.34-17,20-6.49A8,8,0,0,0,238,115.64Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function MagnifyingGlassIcon(props: {fill: string}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function PaletteIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M200.77,53.89A103.27,103.27,0,0,0,128,24h-1.07A104,104,0,0,0,24,128c0,43,26.58,79.06,69.36,94.17A32,32,0,0,0,136,192a16,16,0,0,1,16-16h46.21a31.81,31.81,0,0,0,31.2-24.88,104.43,104.43,0,0,0,2.59-24A103.28,103.28,0,0,0,200.77,53.89Zm13,93.71A15.89,15.89,0,0,1,198.21,160H152a32,32,0,0,0-32,32,16,16,0,0,1-21.31,15.07C62.49,194.3,40,164,40,128a88,88,0,0,1,87.09-88h.9a88.35,88.35,0,0,1,88,87.25A88.86,88.86,0,0,1,213.81,147.6ZM140,76a12,12,0,1,1-12-12A12,12,0,0,1,140,76ZM96,100A12,12,0,1,1,84,88,12,12,0,0,1,96,100Zm0,56a12,12,0,1,1-12-12A12,12,0,0,1,96,156Zm88-56a12,12,0,1,1-12-12A12,12,0,0,1,184,100Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function ScanIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M224,40V80a8,8,0,0,1-16,0V48H176a8,8,0,0,1,0-16h40A8,8,0,0,1,224,40ZM80,208H48V176a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H80a8,8,0,0,0,0-16Zm136-40a8,8,0,0,0-8,8v32H176a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V176A8,8,0,0,0,216,168ZM40,88a8,8,0,0,0,8-8V48H80a8,8,0,0,0,0-16H40a8,8,0,0,0-8,8V80A8,8,0,0,0,40,88Zm128,96H88a16,16,0,0,1-16-16V88A16,16,0,0,1,88,72h80a16,16,0,0,1,16,16v80A16,16,0,0,1,168,184ZM88,168h80V88H88Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function StackIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M230.91,172A8,8,0,0,1,228,182.91l-96,56a8,8,0,0,1-8.06,0l-96-56A8,8,0,0,1,36,169.09l92,53.65,92-53.65A8,8,0,0,1,230.91,172ZM220,121.09l-92,53.65L36,121.09A8,8,0,0,0,28,134.91l96,56a8,8,0,0,0,8.06,0l96-56A8,8,0,1,0,220,121.09ZM24,80a8,8,0,0,1,4-6.91l96-56a8,8,0,0,1,8.06,0l96,56a8,8,0,0,1,0,13.82l-96,56a8,8,0,0,1-8.06,0l-96-56A8,8,0,0,1,24,80Zm23.88,0L128,126.74,208.12,80,128,33.26Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function TrashIcon(props: {fill: string}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
);
}

View File

@ -0,0 +1,6 @@
export function XIcon(props: {fill: string}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M165.66,101.66,139.31,128l26.35,26.34a8,8,0,0,1-11.32,11.32L128,139.31l-26.34,26.35a8,8,0,0,1-11.32-11.32L116.69,128,90.34,101.66a8,8,0,0,1,11.32-11.32L128,116.69l26.34-26.35a8,8,0,0,1,11.32,11.32ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path></svg>
);
}