move toolbar ui

feat/devtool
Manuel Serret 4 months ago
parent c9f713d9a0
commit 416720a322

@ -1,34 +1,136 @@
<script>
import { mount, onMount, tick, unmount } from 'svelte';
import { onMount, tick } from 'svelte';
import Icon from './Icon.svelte';
import { SvelteMap } from 'svelte/reactivity';
let {
/** @type import('./public.d.ts').ResolvedConfig */
config
} = $props();
let open = $state(false); // Default to closed
/** @type {{ config: import('./public.d.ts').ResolvedConfig }} */
let { config } = $props();
let open = $state(false);
/** @type {import('svelte').Component} */
let ActiveComponent = $state(null);
/** @type {import('svelte').Component | undefined} */
let ActiveComponent = $state();
/** @type {HTMLElement} */
let toolbar;
let toolbarSelector;
/** @type {HTMLElement} */
let toolbarPanels;
/** @type {HTMLElement} */
let toolbarTools;
let dragOffsetX = 0;
let dragOffsetY = 0;
let toolbarScreenOffset = 20;
/** @type {import('./public').Config['position']} */
let computedPosition = config.position;
onMount(() => {
toolbar.style.right = '20px';
toolbar.style.bottom = '20px';
recalculate_toolbar_panel_position();
computedPosition = config.position;
layout_selector();
});
$effect(() => {
computedPosition = config.position;
layout_selector();
layout_toolbar();
});
function layout_selector() {
const rect = toolbarSelector.getBoundingClientRect();
let x = 0;
let y = 0;
switch (computedPosition) {
case 'top-left':
x = toolbarScreenOffset;
y = toolbarScreenOffset;
break;
case 'top-right':
x = window.innerWidth - rect.width - toolbarScreenOffset;
y = toolbarScreenOffset;
break;
case 'bottom-right':
x = window.innerWidth - rect.width - toolbarScreenOffset;
y = window.innerHeight - rect.height - toolbarScreenOffset;
break;
case 'bottom-left':
x = toolbarScreenOffset;
y = window.innerHeight - rect.height - toolbarScreenOffset;
break;
default:
break;
}
toolbarSelector.style.left = x + 'px';
toolbarSelector.style.top = y + 'px';
}
function layout_toolbar() {
const toolbarSelectorRect = toolbarSelector.getBoundingClientRect();
const toolbarToolsRect = toolbarTools.getBoundingClientRect();
const toolbarPanelsRect = toolbarPanels.getBoundingClientRect();
switch (computedPosition) {
case 'top-left':
toolbarTools.style.top = toolbarSelector.style.top;
toolbarTools.style.left =
parseFloat(toolbarSelector.style.left) + toolbarSelectorRect.width + 'px';
toolbarPanels.style.top =
parseFloat(toolbarSelector.style.top) + toolbarSelectorRect.height + 'px';
toolbarPanels.style.left = toolbarSelectorRect.x + 'px';
break;
case 'top-right':
toolbarTools.style.top = toolbarSelector.style.top;
toolbarTools.style.left =
parseFloat(toolbarSelector.style.left) - toolbarToolsRect.width + 'px';
toolbarPanels.style.top =
parseFloat(toolbarSelector.style.top) + toolbarSelectorRect.height + 'px';
toolbarPanels.style.left =
parseFloat(toolbarSelector.style.left) -
toolbarPanelsRect.width +
toolbarSelectorRect.width +
'px';
break;
case 'bottom-left':
toolbarTools.style.top = toolbarSelector.style.top;
toolbarTools.style.left =
parseFloat(toolbarSelector.style.left) + toolbarSelectorRect.width + 'px';
toolbarPanels.style.top =
parseFloat(toolbarSelector.style.top) - toolbarPanelsRect.height + 'px';
toolbarPanels.style.left = toolbarSelectorRect.x + 'px';
break;
case 'bottom-right':
toolbarTools.style.top = toolbarSelector.style.top;
toolbarTools.style.left =
parseFloat(toolbarSelector.style.left) - toolbarToolsRect.width + 'px';
toolbarPanels.style.top =
parseFloat(toolbarSelector.style.top) - toolbarPanelsRect.height + 'px';
toolbarPanels.style.left =
parseFloat(toolbarSelector.style.left) -
toolbarPanelsRect.width +
toolbarSelectorRect.width +
'px';
break;
default:
break;
}
}
/**
* @param {import('./public').Tool} tool
*/
function toggle_tool(tool) {
async function toggle_tool(tool) {
if (tool.component === ActiveComponent) {
ActiveComponent = undefined;
} else {
@ -37,13 +139,16 @@
if (!ActiveComponent) toolbarPanels.style.display = 'none';
else toolbarPanels.style.display = 'block';
await tick();
layout_toolbar();
}
/**
* @param {DragEvent} event
*/
function drag_start(event) {
const rect = toolbar.getBoundingClientRect();
const rect = toolbarSelector.getBoundingClientRect();
dragOffsetX = event.clientX - rect.x;
dragOffsetY = event.clientY - rect.y;
}
@ -54,58 +159,67 @@
function drag(event) {
if (event.clientX === 0 || event.clientY === 0) return;
const rect = toolbar.getBoundingClientRect();
const x = event.clientX - dragOffsetX;
const y = event.clientY - dragOffsetY;
toolbarSelector.style.left = x + 'px';
toolbarSelector.style.top = y + 'px';
let top = false;
let left = false;
if (y < window.innerHeight / 2) top = true;
if (x < window.innerWidth / 2) left = true;
const x = window.innerWidth - event.clientX + dragOffsetX - rect.width;
const y = window.innerHeight - event.clientY + dragOffsetY - rect.height;
toolbar.style.right = x + 'px';
toolbar.style.bottom = y + 'px';
if (top && left) computedPosition = 'top-left';
if (top && !left) computedPosition = 'top-right';
if (!top && left) computedPosition = 'bottom-left';
if (!top && !left) computedPosition = 'bottom-right';
recalculate_toolbar_panel_position();
layout_toolbar();
}
async function toggle_toolbar() {
open = !open;
await tick();
recalculate_toolbar_panel_position();
}
function recalculate_toolbar_panel_position() {
const rect = toolbar.getBoundingClientRect();
toolbarPanels.style.right = toolbar.style.right;
toolbarPanels.style.bottom = parseFloat(toolbar.style.bottom ?? 0) + rect.height + 10 + 'px'; // Add a small gap
}
</script>
<svelte:window onresize={recalculate_toolbar_panel_position} />
<div
class="svelte-toolbar"
bind:this={toolbar}
draggable="true"
ondrag={drag}
ondragstart={drag_start}
role="toolbar"
tabindex="-1"
>
{#if open}
<ul class="svelte-toolbar-tools">
{#each config.tools as tool}
<li class:active={tool.component === ActiveComponent}>
<button onclick={() => toggle_tool(tool)} aria-label={tool.name}>{@html tool.icon}</button
>
</li>
{/each}
</ul>
{/if}
<button type="button" class="svelte-toolbar-selector" onclick={toggle_toolbar}>
<Icon />
</button>
</div>
<div class="svelte-toolbar-panels" bind:this={toolbarPanels}>
{#if ActiveComponent}
<ActiveComponent />
{/if}
<svelte:window onresize={layout_toolbar} />
<div class="svelte-toolbar">
<div
bind:this={toolbarSelector}
draggable="true"
ondrag={drag}
ondragstart={drag_start}
role="toolbar"
tabindex="-1"
class="svelte-toolbar-selector svelte-toolbar-base"
>
<button type="button" onclick={toggle_toolbar}>
<Icon />
</button>
</div>
<div class="svelte-toolbar-tools svelte-toolbar-base" bind:this={toolbarTools}>
{#if open}
<ul>
{#each config.tools as tool}
<li class:active={tool.component === ActiveComponent}>
<button onclick={() => toggle_tool(tool)} aria-label={tool.name}>
{@html tool.icon}
</button>
</li>
{/each}
</ul>
{/if}
</div>
<div class="svelte-toolbar-panels" bind:this={toolbarPanels}>
{#if ActiveComponent}
<ActiveComponent {config} />
{/if}
</div>
</div>
<style>
@ -113,20 +227,25 @@
display: inline-flex;
background-color: var(--toolbar-background);
color: var(--toolbar-color);
position: fixed;
z-index: 1000;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 8px;
}
.svelte-toolbar-selector {
position: fixed;
}
.svelte-toolbar-selector button {
cursor: pointer;
background: none;
}
.svelte-toolbar-base {
border: none;
padding: 8px;
border-radius: 6px;
transition: background-color 0.2s ease-in-out;
background-color: var(--toolbar-background);
margin: 0;
}
.svelte-toolbar-selector:hover {
@ -140,9 +259,14 @@
}
.svelte-toolbar-tools {
position: fixed;
background-color: var(--toolbar-background);
}
.svelte-toolbar-tools ul {
list-style: none;
margin: 0;
padding: 0;
padding: 8px;
display: flex;
align-items: center;
}
@ -204,7 +328,9 @@
color: var(--toolbar-color);
}
:root {
.svelte-toolbar-selector,
.svelte-toolbar,
.svelte-toolbar-panels {
--toolbar-background: #f0f0f0;
--toolbar-color: #222;
--toolbar-selector-hover-background: #e0e0e0;
@ -218,7 +344,9 @@
}
@media (prefers-color-scheme: dark) {
:root {
.svelte-toolbar-selector,
.svelte-toolbar,
.svelte-toolbar-panels {
--toolbar-background: #1e1e27;
--toolbar-color: white;
--toolbar-selector-hover-background: #333344;

@ -4,5 +4,8 @@ import { svelte_inspector } from './tools/inspector/index.js';
import { svelte_config } from './tools/config/index.js';
export * from './configure.svelte.js';
configureSvelte({ tools: [svelte_inspector, svelte_config] });
configureSvelte({
position: 'top-left',
tools: [svelte_inspector, svelte_config]
});
mountUI();

@ -4,7 +4,7 @@ export * from './index.js';
export interface Tool {
name: string;
icon: string; // url or svg
icon: string; // TODO: url or svg
activate: () => void;
deactivate: () => void;
keyCombo?: string;
@ -14,7 +14,7 @@ export interface Tool {
type ToolFn = () => Tool;
export interface Config {
position?: 'top' | 'bottom';
position?: 'top-left' | 'top-right' | 'bottom-right' | 'bottom-left';
tools?: (Tool | ToolFn)[];
}

@ -1,12 +1,27 @@
<script>
import { configureSvelte } from '../../configure.svelte';
/** @type {{ config: import('../../public.d.ts').ResolvedConfig }} */
let { config } = $props();
/** @type {import('../../public').Config["position"]} */
let position = $state(config.position);
// TODO: This probably should not be an effect, but I failed to get the typing right with the onchange event
$effect(() => {
configureSvelte({ position });
});
</script>
<div>
<label for="position"
>Position
<label for="position">
Position
<select id="position">
<select id="position" bind:value={position}>
<option value="top-left">top left</option>
<option value="top-left">top right</option>
<option value="top-left">bottom right</option>
<option value="top-left">bottom left</option>
<option value="top-right">top right</option>
<option value="bottom-right">bottom right</option>
<option value="bottom-left">bottom left</option>
</select>
</label>
</div>

Loading…
Cancel
Save