implement basic toolbar

feat/devtool
Manuel Serret 4 months ago
parent a274b10456
commit fc5427dbb1

@ -146,7 +146,6 @@
},
"devDependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"@neodrag/svelte": "^2.3.2",
"@playwright/test": "^1.46.1",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",

@ -1,6 +1,7 @@
<script>
import { draggable } from '@neodrag/svelte';
import { mount, onMount, tick, unmount } from 'svelte';
import Icon from './Icon.svelte';
import { SvelteMap } from 'svelte/reactivity';
let {
/** @type import('./public.d.ts').ResolvedConfig */
@ -8,40 +9,136 @@
} = $props();
let open = $state(true); // todo change this to false
/** @type {string[]} */
let active_tool_names = $state([]);
/** @type {SvelteMap<string, Record<string, any>>} */
let active_tools = $state(new SvelteMap());
/** @type {HTMLElement} */
let toolbar;
/** @type {HTMLElement} */
let toolbarPanels;
let dragOffsetX = 0;
let dragOffsetY = 0;
onMount(() => {
recalculate_toolbar_panel_position();
});
/**
* @param {import('./public').Tool} tool
*/
function toggle_tool(tool) {
const active = active_tool_names.includes(tool.name);
const active = active_tools.has(tool.name);
if (!active) {
active_tool_names.push(tool.name);
let mounted_component;
if (tool.component) mounted_component = mountTool(tool.component, tool.name, { tool });
active_tools.set(tool.name, mounted_component);
tool.activate();
} else {
active_tool_names.splice(active_tool_names.indexOf(tool.name), 1);
const mounted_component = active_tools.get(tool.name);
if (tool.component && mounted_component) unmountTool(mounted_component, tool.name);
tool.deactivate();
active_tools.delete(tool.name);
}
}
/**
* @param {import('svelte').Component} component
* @param {string} id
* @param {Record<string, any>} props
*/
function mountTool(component, id, props) {
if (document.getElementById(id) != null) {
throw Error(`${id} already exists, skipping`);
}
console.log(active_tool_names);
const el = document.createElement('div');
el.setAttribute('id', `svelte-toolbar-${id}`);
toolbarPanels.appendChild(el);
const mounted_component = mount(component, { target: el, props });
return mounted_component;
}
/**
* @param {string} id
* @param {Record<string, any>} component
*/
async function unmountTool(component, id) {
await unmount(component);
const el = document.getElementById(`svelte-toolbar-${id}`);
if (el) el.remove();
}
/**
* @param {DragEvent} event
*/
function drag_start(event) {
const rect = toolbar.getBoundingClientRect();
dragOffsetX = event.clientX - rect.x;
dragOffsetY = event.clientY - rect.y;
}
/**
* @param {DragEvent} event
*/
function drag(event) {
if (event.clientX === 0 || event.clientY === 0) return;
const rect = toolbar.getBoundingClientRect();
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';
recalculate_toolbar_panel_position();
}
async function toggle_toolbar() {
open = !open;
// need to wait here, so that the toolbar can close first
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 + 'px';
}
</script>
<div class="toolbar" use:draggable={{ bounds: document.body }}>
<svelte:window onresize={recalculate_toolbar_panel_position} />
<div
class="toolbar"
bind:this={toolbar}
draggable="true"
ondrag={drag}
ondragstart={drag_start}
role="toolbar"
tabindex="-1"
>
{#if open}
<ul class="tools">
{#each config.tools as tool}
<li class:active={active_tool_names.includes(tool.name)}>
<button onclick={() => toggle_tool(tool)}>{tool.name}</button>
<li class:active={active_tools.has(tool.name)}>
<button onclick={() => toggle_tool(tool)} aria-label={tool.name}>{@html tool.icon}</button
>
</li>
{/each}
</ul>
{/if}
<button type="button" class="toolbar-selector" onclick={() => (open = !open)}>
<button type="button" class="toolbar-selector" onclick={toggle_toolbar}>
<Icon />
</button>
</div>
<div class="toolbar-panels" bind:this={toolbarPanels}></div>
<style>
.toolbar-selector {
@ -54,7 +151,6 @@
}
.tools {
background-color: #666; /* TODO: consider dark / light mode */
list-style: none;
margin: 0;
padding: 0;
@ -68,21 +164,42 @@
border: #111 1px solid;
border-radius: 50%;
margin: 0 10px;
padding: 10px;
height: 30px;
height: 50px;
width: 50px;
}
.tools li.active {
border-color: #ff3e00;
}
.tools li button {
padding: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.tools li button :global(svg) {
height: 30px;
width: 30px;
}
.toolbar {
display: inline-flex;
background-color: #666; /* TODO: consider dark / light mode */
color: white;
position: static;
position: fixed;
right: 0;
bottom: 0;
}
.toolbar > * {
/* display: inline-block; */
.toolbar-panels {
position: fixed;
background-color: #999;
right: 0;
bottom: 0;
display: flex;
}
</style>

@ -1,8 +1,8 @@
import {mountUI} from './runtime.svelte.js';
import { mountUI } from './runtime.svelte.js';
import { configureSvelte } from './configure.svelte.js';
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]});
configureSvelte({ tools: [svelte_inspector, svelte_config] });
mountUI();

@ -1,3 +1,5 @@
import type { Component } from 'svelte';
export * from './index.js';
export interface Tool {
@ -7,6 +9,7 @@ export interface Tool {
deactivate: () => void;
keyCombo?: string;
disabled?: boolean;
component?: Component;
}
type ToolFn = () => Tool;

@ -1,20 +1,20 @@
import ToolBar from './ToolBar.svelte';
import { mount } from 'svelte';
import {getConfig} from './configure.svelte.js';
import { mount, unmount } from 'svelte';
import { getConfig } from './configure.svelte.js';
export function mountUI() {
if(typeof window !== 'undefined') {
if (typeof window !== 'undefined') {
const id = 'svelte-toolbar-host';
if (document.getElementById(id) != null) {
console.debug('svelte-toolbar-host already exists, skipping');
return
return;
}
const props = $state({config: getConfig()})
const props = $state({ config: getConfig() });
const el = document.createElement('div');
el.setAttribute('id', id);
// appending to documentElement adds it outside of body
document.documentElement.appendChild(el);
mount(ToolBar, { target: el,props });
mount(ToolBar, { target: el, props });
}
}

@ -0,0 +1,48 @@
<div>
<label for="position"
>Position
<select id="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>
</select>
</label>
</div>
<div>
<label for="position"
>Position
<select id="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>
</select>
</label>
</div>
<div>
<label for="position"
>Position
<select id="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>
</select>
</label>
</div>
<div>
<label for="position"
>Position
<select id="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>
</select>
</label>
</div>

@ -0,0 +1,24 @@
import Config from './Config.svelte'
const icon = `
<svg fill="#000000" height="200px" width="200px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 612.004 612.004" xml:space="preserve"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g> <path d="M593.676,241.87h-48.719c-5.643-21.066-13.982-41.029-24.649-59.482l34.459-34.459c7.158-7.154,7.158-18.755,0-25.912 l-64.783-64.783c-7.154-7.156-18.757-7.156-25.909,0l-34.461,34.461c-18.453-10.667-38.414-19.005-59.482-24.647V18.325 c0-10.121-8.201-18.324-18.324-18.324h-91.616c-10.123,0-18.324,8.203-18.324,18.324V67.05c-21.068,5.64-41.027,13.98-59.48,24.647 l-34.459-34.459c-7.158-7.158-18.755-7.158-25.912,0l-64.785,64.781c-7.158,7.156-7.158,18.755,0,25.913l34.461,34.461 C81.03,200.845,72.69,220.804,67.051,241.87H18.326C8.205,241.87,0,250.073,0,260.193v91.618c0,10.121,8.205,18.324,18.326,18.324 h48.725c5.64,21.066,13.98,41.027,24.645,59.478l-34.461,34.461c-7.158,7.154-7.158,18.757,0,25.911l64.781,64.783 c7.16,7.158,18.759,7.158,25.916,0l34.459-34.459c18.451,10.665,38.412,19.005,59.48,24.645v48.727 c0,10.119,8.201,18.324,18.324,18.324h91.616c10.123,0,18.324-8.205,18.324-18.324v-48.727c21.068-5.64,41.029-13.98,59.482-24.647 l34.461,34.459c7.154,7.158,18.755,7.158,25.913,0l64.781-64.781c7.158-7.158,7.158-18.759,0-25.913l-34.459-34.459 c10.667-18.453,19.007-38.414,24.649-59.479h48.721c10.123,0,18.324-8.203,18.324-18.324v-91.618 C612,250.073,603.799,241.87,593.676,241.87z M306.002,397.619c-50.601,0-91.616-41.021-91.616-91.616 c0-50.597,41.017-91.616,91.616-91.616s91.616,41.019,91.616,91.616C397.616,356.598,356.601,397.619,306.002,397.619z"></path> </g> </g></svg>
`;
/** @return {import('../../public.d.ts').ResolvedConfig.tools[0]} */
export function svelte_config() {
/** @type {Record<string, any>} */
return {
name: 'config',
icon,
keyCombo: 'ctrl-c',
component: Config,
activate: () => {
console.log("activate config")
},
deactivate: () => {
console.log("de-activate config")
}
}
}

@ -2549,6 +2549,7 @@ declare module 'svelte/events' {
}
declare module 'svelte/toolbar' {
import type { Component } from 'svelte';
export interface Tool {
name: string;
icon: string; // url or svg
@ -2556,6 +2557,7 @@ declare module 'svelte/toolbar' {
deactivate: () => void;
keyCombo?: string;
disabled?: boolean;
component?: Component;
}
type ToolFn = () => Tool;
@ -2565,12 +2567,11 @@ declare module 'svelte/toolbar' {
}
export interface ResolvedConfig extends Config {
tools: Tool[]
tools: Tool[];
}
export function configure(options: Partial<Config>): void;
export function configureSvelte(options: Partial<Config>): void;
export function getConfig(): ResolvedConfig;
export function mountUI(): void;
export {};
}

@ -105,9 +105,6 @@ importers:
'@jridgewell/trace-mapping':
specifier: ^0.3.25
version: 0.3.25
'@neodrag/svelte':
specifier: ^2.3.2
version: 2.3.2(svelte@5.28.2)
'@playwright/test':
specifier: ^1.46.1
version: 1.46.1
@ -480,11 +477,6 @@ packages:
'@manypkg/get-packages@1.1.3':
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
'@neodrag/svelte@2.3.2':
resolution: {integrity: sha512-1aPgnv2LqikkPEoTVgCPCqtXUXOD7OjCHLAIssTv0hkmyHLG9i697Cu+Tzco4/Q4KgOQXCDnLsa4Fvya5wXP9g==}
peerDependencies:
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -2129,10 +2121,6 @@ packages:
svelte:
optional: true
svelte@5.28.2:
resolution: {integrity: sha512-FbWBxgWOpQfhKvoGJv/TFwzqb4EhJbwCD17dB0tEpQiw1XyUEKZJtgm4nA4xq3LLsMo7hu5UY/BOFmroAxKTMg==}
engines: {node: '>=18'}
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
@ -2809,10 +2797,6 @@ snapshots:
globby: 11.1.0
read-yaml-file: 1.1.0
'@neodrag/svelte@2.3.2(svelte@5.28.2)':
dependencies:
svelte: 5.28.2
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -3005,10 +2989,6 @@ snapshots:
dependencies:
acorn: 8.14.0
'@sveltejs/acorn-typescript@1.0.5(acorn@8.14.1)':
dependencies:
acorn: 8.14.1
'@sveltejs/eslint-config@8.1.0(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4))(eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.26.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4)':
dependencies:
'@stylistic/eslint-plugin-js': 1.8.0(eslint@9.9.1)
@ -4461,23 +4441,6 @@ snapshots:
optionalDependencies:
svelte: link:packages/svelte
svelte@5.28.2:
dependencies:
'@ampproject/remapping': 2.3.0
'@jridgewell/sourcemap-codec': 1.5.0
'@sveltejs/acorn-typescript': 1.0.5(acorn@8.14.1)
'@types/estree': 1.0.7
acorn: 8.14.1
aria-query: 5.3.1
axobject-query: 4.1.0
clsx: 2.1.1
esm-env: 1.2.1
esrap: 1.4.6
is-reference: 3.0.3
locate-character: 3.0.0
magic-string: 0.30.17
zimmerframe: 1.1.2
symbol-tree@3.2.4: {}
tapable@2.2.1: {}

Loading…
Cancel
Save