chore: some updates
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m6s
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m6s
This commit is contained in:
45
packages/ui/src/lib/inputs/Checkbox.svelte
Normal file
45
packages/ui/src/lib/inputs/Checkbox.svelte
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
let { value = $bindable(false) }: Props = $props();
|
||||
|
||||
$effect(() => {
|
||||
if (typeof value === 'string') {
|
||||
value = value === 'true';
|
||||
} else if (typeof value === 'number') {
|
||||
value = value === 1;
|
||||
} else if (!(typeof value === 'boolean')) {
|
||||
value = !!value;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<label
|
||||
class="relative inline-flex h-[22px] w-[22px] cursor-pointer items-center justify-center bg-[var(--layer-2)] rounded-[5px]"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={value}
|
||||
class="peer absolute h-px w-px overflow-hidden whitespace-nowrap border-0 p-0 [clip:rect(0,0,0,0)]"
|
||||
/>
|
||||
<span
|
||||
class="absolute opacity-0 peer-checked:opacity-100 transition-opacity duration-100 flex w-full h-full items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 19 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-[10px] w-[12px] text-[var(--text-color)]"
|
||||
>
|
||||
<path
|
||||
d="M2 7L7 12L17 2"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</label>
|
||||
180
packages/ui/src/lib/inputs/Float.svelte
Normal file
180
packages/ui/src/lib/inputs/Float.svelte
Normal file
@@ -0,0 +1,180 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
value?: number;
|
||||
step?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
value = $bindable(0.5),
|
||||
step = 0.01,
|
||||
min = $bindable(0),
|
||||
max = $bindable(1)
|
||||
}: Props = $props();
|
||||
|
||||
if (min > max) {
|
||||
[min, max] = [max, min];
|
||||
}
|
||||
if (value > max) {
|
||||
max = value;
|
||||
}
|
||||
|
||||
function strip(input: number) {
|
||||
return +parseFloat(input + '').toPrecision(2);
|
||||
}
|
||||
|
||||
let inputEl: HTMLInputElement | undefined = $state();
|
||||
|
||||
let oldValue: number;
|
||||
function handleChange() {
|
||||
if (value === oldValue) return;
|
||||
oldValue = value;
|
||||
}
|
||||
|
||||
let isMouseDown = $state(false);
|
||||
let downV = 0;
|
||||
let vx = 0;
|
||||
let rect: DOMRect;
|
||||
|
||||
function handleMouseDown(ev: MouseEvent) {
|
||||
ev.preventDefault();
|
||||
|
||||
if (!inputEl) return;
|
||||
|
||||
inputEl.focus();
|
||||
|
||||
isMouseDown = true;
|
||||
|
||||
downV = value;
|
||||
rect = inputEl.getBoundingClientRect();
|
||||
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.cursor = 'ew-resize';
|
||||
(ev.target as HTMLElement)?.blur();
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
isMouseDown = false;
|
||||
|
||||
if (downV === value) {
|
||||
inputEl?.focus();
|
||||
}
|
||||
|
||||
if (value > max) {
|
||||
max = value;
|
||||
}
|
||||
|
||||
if (value < min) {
|
||||
min = value;
|
||||
}
|
||||
|
||||
document.body.style.cursor = 'unset';
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
}
|
||||
|
||||
function handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === 'Escape' || ev.key === 'Enter') {
|
||||
handleMouseUp();
|
||||
inputEl?.blur();
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseMove(ev: MouseEvent) {
|
||||
vx = (ev.clientX - rect.left) / rect.width;
|
||||
|
||||
if (ev.ctrlKey) {
|
||||
let v = min + (max - min) * vx;
|
||||
value = v;
|
||||
} else {
|
||||
value = Math.max(Math.min(min + (max - min) * vx, max), min);
|
||||
}
|
||||
(ev.target as HTMLElement)?.blur();
|
||||
}
|
||||
$effect(() => {
|
||||
if ((value || 0).toString().length > 5) {
|
||||
value = strip(value || 0);
|
||||
}
|
||||
value !== undefined && handleChange();
|
||||
});
|
||||
let width = $derived(
|
||||
Number.isFinite(value) ? Math.max((value?.toString().length ?? 1) * 8, 50) + 'px' : '20px'
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="component-wrapper" class:is-down={isMouseDown}>
|
||||
<span class="overlay" style={`width: ${((value - min) / (max - min)) * 100}%`}></span>
|
||||
<input
|
||||
bind:value
|
||||
bind:this={inputEl}
|
||||
{step}
|
||||
{max}
|
||||
{min}
|
||||
onkeydown={handleKeyDown}
|
||||
onmousedown={handleMouseDown}
|
||||
onmouseup={handleMouseUp}
|
||||
type="number"
|
||||
style={`width:${width};`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.component-wrapper {
|
||||
position: relative;
|
||||
background-color: var(--layer-2, #4b4b4b);
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
transition: box-shadow 0.3s ease;
|
||||
border: solid 1px var(--outline);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius, 2px);
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
box-sizing: border-box;
|
||||
-webkit-appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
font-family: var(--font-family);
|
||||
font-variant-numeric: tabular-nums;
|
||||
cursor: pointer;
|
||||
color: var(--text-color);
|
||||
background-color: transparent;
|
||||
padding: var(--padding, 6px);
|
||||
font-size: 1em;
|
||||
padding-inline: 10px;
|
||||
text-align: center;
|
||||
border: none;
|
||||
border-style: none;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.is-down > input {
|
||||
cursor: ew-resize !important;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
background-color: var(--text-color);
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.is-down > .overlay {
|
||||
transition: none !important;
|
||||
}
|
||||
</style>
|
||||
173
packages/ui/src/lib/inputs/Integer.svelte
Normal file
173
packages/ui/src/lib/inputs/Integer.svelte
Normal file
@@ -0,0 +1,173 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
interface Props {
|
||||
min?: number | undefined;
|
||||
max?: number | undefined;
|
||||
step?: number;
|
||||
value?: number;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
min = undefined,
|
||||
max = undefined,
|
||||
step = 1,
|
||||
value = $bindable(0),
|
||||
id = ''
|
||||
}: Props = $props();
|
||||
|
||||
if (!value) {
|
||||
value = 0;
|
||||
}
|
||||
|
||||
let inputEl: HTMLInputElement | undefined = $state();
|
||||
let wrapper: HTMLDivElement | undefined = $state();
|
||||
|
||||
let prev = -1;
|
||||
function update() {
|
||||
if (prev === value) return;
|
||||
prev = value;
|
||||
dispatch('change', parseFloat(value + ''));
|
||||
}
|
||||
|
||||
function handleChange(change: number) {
|
||||
value = Math.max(min ?? -Infinity, Math.min(+value + change, max ?? Infinity));
|
||||
}
|
||||
|
||||
let downX = 0;
|
||||
let downV = 0;
|
||||
let rect: DOMRect;
|
||||
|
||||
function handleMouseDown(ev: MouseEvent) {
|
||||
ev.preventDefault();
|
||||
|
||||
downV = value;
|
||||
downX = ev.clientX;
|
||||
rect = wrapper.getBoundingClientRect();
|
||||
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.cursor = 'ew-resize';
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
if (downV === value) {
|
||||
inputEl.focus();
|
||||
} else {
|
||||
inputEl.blur();
|
||||
}
|
||||
|
||||
document.body.style.cursor = 'unset';
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
}
|
||||
|
||||
function handleMouseMove(ev: MouseEvent) {
|
||||
if (!ev.ctrlKey && typeof min === 'number' && typeof max === 'number') {
|
||||
const vx = (ev.clientX - rect.left) / rect.width;
|
||||
value = Math.max(Math.min(Math.round(min + (max - min) * vx), max), min);
|
||||
} else {
|
||||
const vx = ev.clientX - downX;
|
||||
value = downV + Math.round(vx / 10);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
value !== undefined && update();
|
||||
});
|
||||
let width = $derived(
|
||||
Number.isFinite(value) ? Math.max((value?.toString().length ?? 1) * 8, 30) + 'px' : '20px'
|
||||
);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="component-wrapper"
|
||||
bind:this={wrapper}
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuenow={value}
|
||||
onmousedown={handleMouseDown}
|
||||
onmouseup={handleMouseUp}
|
||||
>
|
||||
{#if typeof min !== 'undefined' && typeof max !== 'undefined'}
|
||||
<span class="overlay" style={`width: ${Math.min((value - min) / (max - min), 1) * 100}%`}
|
||||
></span>
|
||||
{/if}
|
||||
<button onclick={() => handleChange(-step)}>-</button>
|
||||
<input
|
||||
bind:value
|
||||
bind:this={inputEl}
|
||||
{id}
|
||||
{step}
|
||||
{max}
|
||||
{min}
|
||||
type="number"
|
||||
style={`width:${width};`}
|
||||
/>
|
||||
|
||||
<button onclick={() => handleChange(+step)}>+</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.component-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
background-color: var(--layer-2, #4b4b4b);
|
||||
border-radius: 2px;
|
||||
user-select: none;
|
||||
transition: box-shadow 0.3s ease;
|
||||
outline: solid 1px var(--outline);
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius, 2px);
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
-webkit-appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
font-family: var(--font-family);
|
||||
padding-top: 8px;
|
||||
flex: 1;
|
||||
width: 72%;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
background-color: var(--layer-3);
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
line-height: 0px;
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
margin-inline: 6px;
|
||||
}
|
||||
|
||||
div input[type='number'] {
|
||||
color: var(--text-color);
|
||||
background-color: transparent;
|
||||
padding: var(--padding, 6px);
|
||||
padding-inline: 0px;
|
||||
text-align: center;
|
||||
border: none;
|
||||
border-style: none;
|
||||
}
|
||||
</style>
|
||||
27
packages/ui/src/lib/inputs/Select.svelte
Normal file
27
packages/ui/src/lib/inputs/Select.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
options?: string[];
|
||||
value?: number;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
let { options = [], value = $bindable(0), id = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<select {id} bind:value>
|
||||
{#each options as label, i}
|
||||
<option value={i}>{label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<style>
|
||||
select {
|
||||
background: var(--layer-2);
|
||||
color: var(--text-color);
|
||||
font-family: var(--font-family);
|
||||
outline: solid 1px var(--outline);
|
||||
padding: 0.8em 1em;
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
34
packages/ui/src/lib/inputs/Vec3.svelte
Normal file
34
packages/ui/src/lib/inputs/Vec3.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import Float from './Float.svelte';
|
||||
|
||||
interface Props {
|
||||
value?: any;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
let { value = $bindable([0, 0, 0]), id = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Float id={`${id}-x`} bind:value={value[0]} />
|
||||
<Float id={`${id}-y`} bind:value={value[1]} />
|
||||
<Float id={`${id}-z`} bind:value={value[2]} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div > :global(.component-wrapper:nth-child(1)) {
|
||||
border-radius: 4px 4px 0px 0px !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
div > :global(.component-wrapper:nth-child(2)) {
|
||||
border-radius: 0px !important;
|
||||
outline: none;
|
||||
border: solid thin var(--outline);
|
||||
border-top: solid thin color-mix(in srgb, var(--outline) 50%, transparent);
|
||||
border-bottom: solid thin color-mix(in srgb, var(--outline) 50%, transparent);
|
||||
}
|
||||
div > :global(.component-wrapper:nth-child(3)) {
|
||||
border-top: none !important;
|
||||
border-radius: 0px 0px 4px 4px !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user