This commit is contained in:
kolaflash
2025-12-16 20:36:09 +01:00
parent 897c414b44
commit 1dc9c803b3
35 changed files with 114 additions and 36 deletions
+33
View File
@@ -0,0 +1,33 @@
.file-browser {
background: var(--c-bg-0);
margin-left: calc(var(--sp-2) * -1);
}
.file-list {
margin: 0;
list-style: none;
padding: 0 0 0 var(--sp-2);
}
.file-item {
display: flex;
flex-flow: column nowrap;
}
.file-label {
display: flex;
justify-content: flex-start;
align-items: center;
padding: var(--sp-1);
border-radius: var(--sp-1);
cursor: pointer;
svg {
margin-right: var(--sp-1);
}
&.active,
&:hover {
background: var(--c-bg-2);
}
}
+77
View File
@@ -0,0 +1,77 @@
import "./FileBrowser.css";
import { type Component, createSignal, Index, Show } from "solid-js";
import { FileIcon, FolderIcon, FolderOpenIcon } from "#/dev/icons/index.ts";
export type BrowserFolder = Map<string, string | BrowserFolder>;
export type FileHandler = (path: string) => void;
function isFolder(entry: string | BrowserFolder): entry is BrowserFolder {
return typeof (entry as BrowserFolder).size === "number";
}
export const FileBrowser: Component<{
root: BrowserFolder;
handler: FileHandler;
}> = ({ root, handler }) => {
const FileEntry: Component<{
name: string;
path: string;
}> = ({ name, path }) => {
return (
<div class="file-label" onClick={[handler, path]}>
<FileIcon />
<span>{name}</span>
</div>
);
};
const FolderEntry: Component<{
folder: BrowserFolder;
name: string;
open: boolean;
}> = ({ folder, name, open }) => {
const [isOpen, setIsOpen] = createSignal(open);
return (
<>
<div class="file-label" onClick={() => setIsOpen(!isOpen())}>
{isOpen() ? <FolderOpenIcon /> : <FolderIcon />}
<span>{name}</span>
</div>
<Show when={isOpen()}>
<FileList folder={folder} />
</Show>
</>
);
};
const FileList: Component<{ folder: BrowserFolder }> = ({ folder }) => {
return (
<ul class="file-list">
<Index each={[...folder.entries()]}>
{(item, index) => {
const [name, entry] = item();
return (
<li class="file-item">
{isFolder(entry) ? (
<FolderEntry folder={entry} name={name} open={index < 1} />
) : (
<FileEntry name={name} path={entry} />
)}
</li>
);
}}
</Index>
</ul>
);
};
return (
<div class="file-browser">
<FileList folder={root} />
</div>
);
};
+42
View File
@@ -0,0 +1,42 @@
.input-field {
width: 100%;
display: block;
background: var(--c-bg-2);
color: var(--c-fg-1);
border-radius: var(--sp-1);
}
.input-field input {
width: 100%;
height: 2em;
font-size: medium;
margin: 0;
padding: var(--sp-1);
border: none;
background: var(--c-bg-2);
color: inherit;
border-radius: var(--sp-1);
}
.input-field.icon {
display: flex;
flex-flow: row nowrap;
align-items: center;
svg {
padding: var(--sp-1);
display: inline-block;
height: 100%;
width: auto;
}
&:has(:focus) {
outline-width: 2px;
outline-style: solid;
outline-color: var(--c-primary);
input {
outline: none;
}
}
}
+24
View File
@@ -0,0 +1,24 @@
import "./InputField.css";
import type { Component, JSX } from "solid-js";
export const InputField: Component<{
name: string;
value: string;
type?: string;
icon?: JSX.Element;
children?: JSX.Element;
onChange?: (value: string) => void;
onInput?: (value: string) => void;
}> = (props) => (
<label class="input-field" classList={{ icon: !!props.icon }}>
{props.children}
{props.icon}
<input
type={props.type ?? "text"}
name={props.name}
value={props.value}
onChange={(e) => props.onChange?.(e.target.value)}
onInput={(e) => props.onInput?.(e.target.value)}
/>
</label>
);
+69
View File
@@ -0,0 +1,69 @@
.range-slider {
width: 100%;
color: var(--c-fg-1);
display: flex;
flex-flow: row nowrap;
justify-content: center;
align-items: center;
gap: var(--sp-2);
font-family: var(--font-sans);
font-weight: bold;
}
.range-slider [type="range"] {
cursor: pointer;
appearance: none;
background: transparent;
border-radius: var(--sp-2);
height: var(--sp-3);
flex: auto;
&:focus {
outline: 0;
}
&::-webkit-slider-runnable-track {
width: 100%;
height: var(--sp-2);
border-radius: var(--sp-3);
color: transparent;
background: var(--c-bg-2);
border: none;
}
&::-webkit-slider-thumb {
height: var(--sp-3);
width: var(--sp-3);
border-radius: var(--sp-3);
border: none;
background: var(--c-fg-1);
margin-top: calc(var(--sp-1) * -1);
--webkit-appearance: none;
appearance: none;
}
&:focus::-webkit-slider-thumb {
outline-width: 2px;
outline-style: solid;
outline-color: var(--c-primary);
}
&::-moz-range-track {
width: 100%;
height: var(--sp-2);
border-radius: var(--sp-3);
color: transparent;
background: var(--c-bg-2);
border: none;
}
&::-moz-range-thumb {
height: var(--sp-3);
width: var(--sp-3);
border-radius: var(--sp-3);
border: none;
background: var(--c-fg-1);
}
&:focus::-moz-range-thumb {
outline-width: 2px;
outline-style: solid;
outline-color: var(--c-primary);
}
}
+27
View File
@@ -0,0 +1,27 @@
import "./RangeSlider.css";
import type { Component, JSX } from "solid-js";
export const RangeSlider: Component<{
name: string;
min: number;
max: number;
step: number;
value: number;
children?: JSX.Element;
onChange?: (value: number) => void;
onInput?: (value: number) => void;
}> = (props) => (
<label class="range-slider">
{props.children}
<input
type="range"
name={props.name}
min={props.min}
max={props.max}
step={props.step}
value={props.value}
onChange={(e) => props.onChange?.(e.target.valueAsNumber)}
onInput={(e) => props.onInput?.(e.target.valueAsNumber)}
/>
</label>
);
+17
View File
@@ -0,0 +1,17 @@
export const ArrowAutofitHeightIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
d="M12 20H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h6m6 10v7m0-18v7m-3 8l3 3l3-3M15 6l3-3l3 3"
></path>
</svg>
);
+17
View File
@@ -0,0 +1,17 @@
export const ArrowAutofitWidthIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
d="M4 12V6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v6m-10 6H3m18 0h-7m-8-3l-3 3l3 3m12-6l3 3l-3 3"
></path>
</svg>
);
+19
View File
@@ -0,0 +1,19 @@
export const FileIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
>
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2"></path>
</g>
</svg>
);
+20
View File
@@ -0,0 +1,20 @@
export const FileDeltaIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
>
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2"></path>
<path d="M9 17h6l-3-6z"></path>
</g>
</svg>
);
+20
View File
@@ -0,0 +1,20 @@
export const FileMusicIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
>
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2"></path>
<path d="M10 16a1 1 0 1 0 2 0a1 1 0 1 0-2 0m2 0v-5l2 1"></path>
</g>
</svg>
);
+20
View File
@@ -0,0 +1,20 @@
export const FilePencilIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
>
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2"></path>
<path d="m10 18l5-5a1.414 1.414 0 0 0-2-2l-5 5v2z"></path>
</g>
</svg>
);
+20
View File
@@ -0,0 +1,20 @@
export const FileUnknownIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
>
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2m-5-4v.01"></path>
<path d="M12 14a1.5 1.5 0 1 0-1.14-2.474"></path>
</g>
</svg>
);
+19
View File
@@ -0,0 +1,19 @@
export const FloppyDiscIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
>
<path d="M6 4h10l4 4v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"></path>
<path d="M10 14a2 2 0 1 0 4 0a2 2 0 1 0-4 0m4-10v4H8V4"></path>
</g>
</svg>
);
+17
View File
@@ -0,0 +1,17 @@
export const FolderIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
d="M5 4h4l3 3h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"
></path>
</svg>
);
+17
View File
@@ -0,0 +1,17 @@
export const FolderOpenIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
d="m5 19l2.757-7.351A1 1 0 0 1 8.693 11H21a1 1 0 0 1 .986 1.164l-.996 5.211A2 2 0 0 1 19.026 19za2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h4l3 3h7a2 2 0 0 1 2 2v2"
></path>
</svg>
);
+20
View File
@@ -0,0 +1,20 @@
export const LassoIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
>
<path d="M4.028 13.252C3.371 12.28 3 11.174 3 10c0-3.866 4.03-7 9-7s9 3.134 9 7s-4.03 7-9 7c-1.913 0-3.686-.464-5.144-1.255"></path>
<path d="M3 15a2 2 0 1 0 4 0a2 2 0 1 0-4 0"></path>
<path d="M5 17c0 1.42.316 2.805 1 4"></path>
</g>
</svg>
);
+20
View File
@@ -0,0 +1,20 @@
export const LassoPolygonIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
>
<path d="M4.028 13.252L3 10l2-7l7 5l8-3l1 9l-9 3l-5.144-1.255"></path>
<path d="M3 15a2 2 0 1 0 4 0a2 2 0 1 0-4 0"></path>
<path d="M5 17c0 1.42.316 2.805 1 4"></path>
</g>
</svg>
);
+17
View File
@@ -0,0 +1,17 @@
export const LayoutGridIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
d="M4 5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm10 0a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1zM4 15a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm10 0a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1z"
></path>
</svg>
);
+17
View File
@@ -0,0 +1,17 @@
export const PencilIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
d="M4 20h4L18.5 9.5a2.828 2.828 0 1 0-4-4L4 16zm9.5-13.5l4 4"
></path>
</svg>
);
+17
View File
@@ -0,0 +1,17 @@
export const PolygonIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
d="M10 5a2 2 0 1 0 4 0a2 2 0 1 0-4 0m7 3a2 2 0 1 0 4 0a2 2 0 1 0-4 0M3 11a2 2 0 1 0 4 0a2 2 0 1 0-4 0m10 8a2 2 0 1 0 4 0a2 2 0 1 0-4 0M6.5 9.5l3.5-3m4-1L17 7m1.5 3L16 17m-2.5.5l-7-5"
></path>
</svg>
);
+17
View File
@@ -0,0 +1,17 @@
export const RefreshIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"
></path>
</svg>
);
+17
View File
@@ -0,0 +1,17 @@
export const RefreshAlertIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4m-4-6v3m0 3h.01"
></path>
</svg>
);
+17
View File
@@ -0,0 +1,17 @@
export const RefreshDotIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4m-5-3a1 1 0 1 0 2 0a1 1 0 1 0-2 0"
></path>
</svg>
);
+17
View File
@@ -0,0 +1,17 @@
export const ScriptIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
d="M17 20H6a3 3 0 0 1 0-6h11a3 3 0 0 0 0 6h1a3 3 0 0 0 3-3V6a2 2 0 0 0-2-2H9a2 2 0 0 0-2 2v8"
></path>
</svg>
);
+19
View File
@@ -0,0 +1,19 @@
export const StackBackwardIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
>
<path d="m14 12l6-3l-8-4l-8 4l6 3"></path>
<path fill="currentColor" d="m10 12l-6 3l8 4l8-4l-6-3l-2 1z"></path>
</g>
</svg>
);
+19
View File
@@ -0,0 +1,19 @@
export const StackForwardIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
>
<path fill="currentColor" d="M12 5L4 9l8 4l8-4z"></path>
<path d="m10 12l-6 3l8 4l8-4l-6-3"></path>
</g>
</svg>
);
+17
View File
@@ -0,0 +1,17 @@
export const TransformPointIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
d="M3 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zm0 14a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zM17 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1zm0 14a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1zM11 5h2m-8 6v2m14-2v2m-8 6h2"
></path>
</svg>
);
+17
View File
@@ -0,0 +1,17 @@
export const ZoomScanIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={2}
d="M4 8V6a2 2 0 0 1 2-2h2M4 16v2a2 2 0 0 0 2 2h2m8-16h2a2 2 0 0 1 2 2v2m-4 12h2a2 2 0 0 0 2-2v-2M8 11a3 3 0 1 0 6 0a3 3 0 0 0-6 0m8 5l-2.5-2.5"
></path>
</svg>
);
+23
View File
@@ -0,0 +1,23 @@
export * from "./ArrowAutofitHeight.tsx";
export * from "./ArrowAutofitWidth.tsx";
export * from "./File.tsx";
export * from "./FileDelta.tsx";
export * from "./FileMusic.tsx";
export * from "./FilePencil.tsx";
export * from "./FileUnknown.tsx";
export * from "./FloppyDisc.tsx";
export * from "./Folder.tsx";
export * from "./FolderOpen.tsx";
export * from "./Lasso.tsx";
export * from "./LassoPolygon.tsx";
export * from "./LayoutGrid.tsx";
export * from "./Pencil.tsx";
export * from "./Polygon.tsx";
export * from "./Refresh.tsx";
export * from "./RefreshAlert.tsx";
export * from "./RefreshDot.tsx";
export * from "./Script.tsx";
export * from "./StackBackward.tsx";
export * from "./StackForward.tsx";
export * from "./TransformPoint.tsx";
export * from "./ZoomScan.tsx";
+120
View File
@@ -0,0 +1,120 @@
.dev-tools {
--font-sans:
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--rgb-blurple: 101, 114, 245;
--c-blurple: rgb(var(--rgb-blurple));
--rgb-fuchsia: 238, 69, 158;
--c-fuchsia: rgb(var(--rgb-fuchsia));
--rgb-green: 35, 165, 89;
--c-green: rgb(var(--rgb-green));
--rgb-yellow: 240, 178, 50;
--c-yellow: rgb(var(--rgb-yellow));
--rgb-orange: 255, 144, 0;
--c-orange: rgb(var(--rgb-orange));
--rgb-red: 218, 55, 60;
--c-red: rgb(var(--rgb-red));
--rgb-cyan: 95, 205, 228;
--c-cyan: rgb(var(--rgb-cyan));
--rgb-pink: 246, 42, 245;
--c-pink: rgb(var(--rgb-pink));
--rgb-gray: 117, 126, 138;
--c-gray: rgb(var(--rgb-gray));
--rgb-white: 255, 255, 255;
--c-white: rgb(var(--rgb-white));
--rgb-black: 30, 31, 34;
--c-black: rgb(var(--rgb-black));
--c-primary: var(--c-blurple);
--c-secondary: var(--c-fuchsia);
--c-alert: var(--c-orange);
--c-error: var(--c-red);
--c-bg-0: rgb(30, 31, 34);
--c-bg-1: rgb(43, 45, 49);
--c-bg-2: rgb(49, 51, 56);
--c-bg-3: rgb(64, 66, 73);
--c-fg-0: rgb(255 255 225);
--c-fg-1: rgb(219 222 225);
--c-fg-2: rgb(181 186 193);
--c-fg-3: rgb(113 116 120);
--sp-1: 4px;
--sp-2: 8px;
--sp-3: 16px;
--sp-4: 24px;
--sp-5: 32px;
--sp-6: 48px;
height: 100%;
min-width: 1px;
max-width: 100%;
position: absolute;
top: 0;
left: 0;
order: -1;
font-size: medium;
font-family: var(--font-sans);
*,
*::after,
*::before {
box-sizing: border-box;
}
*:focus {
outline-width: 2px;
outline-style: solid;
outline-color: var(--c-primary);
}
}
.tool-bar {
overflow: hidden;
border-right: 1px solid var(--c-fg-3);
background: var(--c-bg-0);
color: var(--c-fg-1);
width: 100%;
height: 100%;
padding: var(--sp-2);
display: flex;
flex-flow: column nowrap;
gap: var(--sp-2);
}
.tool-bar-resize {
position: absolute;
top: 0;
right: -3px;
width: 5px;
height: 100%;
cursor: ew-resize;
user-select: none;
}
.scene-props {
display: flex;
gap: var(--sp-2);
flex-flow: row wrap;
& > :not(:first-child) {
width: calc(50% - var(--sp-1));
flex-basis: calc(50% - var(--sp-1));
}
}
+139
View File
@@ -0,0 +1,139 @@
import "./main.css";
import { type Component, createSignal, onMount } from "solid-js";
import { render } from "solid-js/web";
import { InputField } from "#/dev/controls/InputField.tsx";
import { RangeSlider } from "#/dev/controls/RangeSlider.tsx";
import { currentScene } from "#/engine/Scene.ts";
import { setScale, viewport } from "#/engine/Viewport.ts";
import { type BrowserFolder, FileBrowser } from "./FileBrowser.tsx";
import {
ArrowAutofitHeightIcon,
ArrowAutofitWidthIcon,
FileIcon,
ZoomScanIcon,
} from "./icons/index.ts";
function sceneTreeBuilder(
current: BrowserFolder,
entries: string[],
index: number,
ref: string[],
): void {
console.log(entries);
const next = current.has(entries[0])
? (current.get(entries[0]) as BrowserFolder)
: (new Map() as BrowserFolder);
if (entries.length > 1) {
current.set(entries.shift() as string, next);
sceneTreeBuilder(next, entries, index, ref);
} else {
// if (entries[0] !== "index.ts") {
// return;
// }
current.set(entries[0], entries[0]);
}
}
async function fetchSceneIndex(): Promise<BrowserFolder> {
const indexRef: string[] = await (await fetch("/api/scenes")).json();
return indexRef.reduce((root, entry, index) => {
const entries = `scenes${entry}`.split("/");
sceneTreeBuilder(root, entries, index, indexRef);
return root;
}, new Map() as BrowserFolder);
}
const root = await fetchSceneIndex();
function adjustGameContainer(gameContainer: HTMLElement, width: number): void {
gameContainer.style = `position: relative; top: 0; left: ${width}px; width: ${document.body.offsetWidth - width}px;`;
}
const DevTools: Component<{ gameContainer: HTMLElement }> = ({
gameContainer,
}) => {
const [width, setWidth] = createSignal(320);
let isResizing = false;
const startResizing = (): void => {
isResizing = true;
};
const stopResizing = (): void => {
isResizing = false;
};
const handleMouseMove = (event: MouseEvent) => {
if (isResizing) {
setWidth(event.clientX);
self.dispatchEvent(new Event("resize"));
}
};
adjustGameContainer(gameContainer, width());
onMount(() => {
self.addEventListener("resize", () => {
adjustGameContainer(gameContainer, width());
});
self.dispatchEvent(new Event("resize"));
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", stopResizing);
});
return (
<div class="dev-tools" style={{ width: `${width()}px` }}>
<div class="tool-bar">
<RangeSlider
name="scale"
min={1}
max={8}
step={1}
value={viewport.initialScale}
onInput={setScale}
>
<ZoomScanIcon />
</RangeSlider>
<div class="scene-props">
<InputField
name="file"
value={currentScene.data.id}
icon={FileIcon()}
></InputField>
<InputField
type="number"
name="width"
value={String(currentScene.data.width)}
icon={ArrowAutofitWidthIcon()}
onChange={(value) => {
currentScene.data.width = parseInt(value);
self.dispatchEvent(new Event("resize"));
}}
></InputField>
<InputField
type="number"
name="height"
value={String(currentScene.data.height)}
icon={ArrowAutofitHeightIcon()}
onChange={(value) => {
currentScene.data.height = parseInt(value);
self.dispatchEvent(new Event("resize"));
}}
></InputField>
</div>
<FileBrowser root={root} handler={console.log} />
</div>
<div class="tool-bar-resize" onMouseDown={startResizing}></div>
</div>
);
};
export function use(container: HTMLElement): void {
render(() => <DevTools gameContainer={container} />, document.body);
}
+60
View File
@@ -0,0 +1,60 @@
import { readdirSync, writeFileSync } from "node:fs";
import type { IncomingMessage, ServerResponse } from "node:http";
function buildBrowserIndex(name: string, dir: string): string[] {
const dirents = readdirSync(dir, {
withFileTypes: true,
recursive: true,
});
return dirents
.filter((dirent) => dirent.isFile() && dirent.name !== name)
.map((dirent) => {
const path = dirent.parentPath.substring(dir.length);
return `${path}/${dirent.name}`;
});
}
function get(req: IncomingMessage, res: ServerResponse): void {
console.log(req.url);
const match = req.url?.match(/\/(\w+)$/);
if (!match) {
res.statusCode = 400;
res.end();
}
const sceneData = buildBrowserIndex("scenes", "scenes"); // FIXME
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(sceneData));
}
function post(req: IncomingMessage, res: ServerResponse): void {
// let body = "";
// req.on("data", (chunk) => {
// body += chunk.toString();
// });
// req.on("end", () => {
// const data: SceneData = JSON.parse(body);
// writeFileSync(
// `${dev.scenesDir}/${data.name}.json`,
// JSON.stringify(data, null, 2),
// "utf8",
// );
// res.statusCode = 204;
// res.end();
// });
}
export default function (req: IncomingMessage, res: ServerResponse): void {
switch (req.method) {
case "GET": {
get(req, res);
break;
}
case "POST": {
post(req, res);
break;
}
}
}