Merge pull request #49 from agnosticeng/chore/sidebar-mobile
chore: hide sidebar on mobile
This commit is contained in:
28
package-lock.json
generated
28
package-lock.json
generated
@@ -19,6 +19,7 @@
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"@codemirror/view": "^6.36.1",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@rich_harris/svelte-split-pane": "^2.0.0",
|
||||
"@tauri-apps/api": "^2.2.0",
|
||||
"d3": "^7.9.0",
|
||||
"dayjs": "^1.11.13",
|
||||
@@ -64,7 +65,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
@@ -575,7 +575,6 @@
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
@@ -590,7 +589,6 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -600,7 +598,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -610,14 +607,12 @@
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
@@ -661,6 +656,14 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rich_harris/svelte-split-pane": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@rich_harris/svelte-split-pane/-/svelte-split-pane-2.0.0.tgz",
|
||||
"integrity": "sha512-mwdBUw59nlozluhoU3fbMO11YZlJ6IM48ZT6jxen8hAJbGbjTZMDKpMwD1neuK8gxgBM1QKAHJLZN7DpSY+hjA==",
|
||||
"peerDependencies": {
|
||||
"svelte": "^5"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.3.tgz",
|
||||
@@ -1509,7 +1512,6 @@
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
@@ -1533,7 +1535,6 @@
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -1546,7 +1547,6 @@
|
||||
"version": "1.4.13",
|
||||
"resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz",
|
||||
"integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": ">=8.9.0"
|
||||
@@ -1556,7 +1556,6 @@
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -1566,7 +1565,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -1592,7 +1590,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -2113,14 +2110,12 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz",
|
||||
"integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.3.2.tgz",
|
||||
"integrity": "sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
@@ -2206,7 +2201,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.6"
|
||||
@@ -2226,14 +2220,12 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
@@ -2488,7 +2480,6 @@
|
||||
"version": "5.16.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.16.2.tgz",
|
||||
"integrity": "sha512-S4mKWbjv53ik1NtGuO95TC7kBA8GYBIeT9fM6y2wHdLNqdCmPXJSWLVuO7vlJZ7TUksp+6qnvqCCtWnVXeTCyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
@@ -2707,7 +2698,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
|
||||
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"@codemirror/view": "^6.36.1",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@rich_harris/svelte-split-pane": "^2.0.0",
|
||||
"@tauri-apps/api": "^2.2.0",
|
||||
"d3": "^7.9.0",
|
||||
"dayjs": "^1.11.13",
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging"
|
||||
"core:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
"width": 1024,
|
||||
"height": 768,
|
||||
"theme": "Dark",
|
||||
"titleBarStyle": "Overlay",
|
||||
"useHttpsScheme": true
|
||||
}
|
||||
]
|
||||
|
||||
59
src/lib/components/Drawer.svelte
Normal file
59
src/lib/components/Drawer.svelte
Normal file
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
position?: 'left' | 'right';
|
||||
width: number;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { open = $bindable(false), position = 'left', width, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="drawer-backdrop"
|
||||
onclick={() => (open = false)}
|
||||
transition:fly={{ duration: 200, opacity: 0 }}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
role="menu"
|
||||
class="drawer {position}"
|
||||
style="width: {width}px"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.key === 'Escape' && (open = false)}
|
||||
tabindex="-1"
|
||||
transition:fly={{ duration: 300, x: position === 'left' ? -width : width }}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.drawer-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: transparent;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -72,6 +72,7 @@
|
||||
|
||||
.content {
|
||||
height: 18px;
|
||||
font-weight: 600;
|
||||
padding: 3px 0;
|
||||
line-height: 1.15;
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
dialog {
|
||||
display: block;
|
||||
inset: 0;
|
||||
top: var(--window-title-bar-height);
|
||||
max-inline-size: min(90vw, 60ch);
|
||||
max-block-size: min(80vh, 100%);
|
||||
margin-top: 0;
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
height: 18px;
|
||||
font-weight: 600;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -149,6 +150,7 @@
|
||||
& > span.name {
|
||||
display: block;
|
||||
height: 18px;
|
||||
font-weight: 600;
|
||||
padding: 3px 0;
|
||||
line-height: 1.15;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
}
|
||||
|
||||
& > nav {
|
||||
flex-shrink: 0;
|
||||
padding: 7px 5px;
|
||||
border-top: 1px solid hsl(0deg 0% 29%);
|
||||
user-select: none;
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
|
||||
<style>
|
||||
section {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
padding: 14px 18px;
|
||||
background-color: hsl(0deg 0% 9%);
|
||||
display: flex;
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { type Length, constrain } from './utils';
|
||||
interface Props {
|
||||
id?: string;
|
||||
orientation: 'horizontal' | 'vertical';
|
||||
position?: Length;
|
||||
min?: Length;
|
||||
max?: Length;
|
||||
disabled?: boolean;
|
||||
a: Snippet;
|
||||
b: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
id,
|
||||
orientation,
|
||||
position: pos = '50%',
|
||||
disabled = false,
|
||||
min = '0%',
|
||||
max = '100%',
|
||||
a,
|
||||
b
|
||||
}: Props = $props();
|
||||
|
||||
let container: HTMLElement;
|
||||
|
||||
let dragging = $state(false);
|
||||
let width = $state(0);
|
||||
let height = $state(0);
|
||||
|
||||
let position = $state(pos);
|
||||
|
||||
$effect(() => {
|
||||
if (container) {
|
||||
const size = orientation === 'horizontal' ? width : height;
|
||||
position = constrain(container, size, min, max, position);
|
||||
}
|
||||
});
|
||||
|
||||
function update(x: number, y: number) {
|
||||
if (disabled) return;
|
||||
|
||||
const { top, left } = container.getBoundingClientRect();
|
||||
|
||||
const pos_px = orientation === 'horizontal' ? x - left : y - top;
|
||||
const size = orientation === 'horizontal' ? width : height;
|
||||
|
||||
position = pos.endsWith('%') ? `${(100 * pos_px) / size}%` : `${pos_px}px`;
|
||||
}
|
||||
|
||||
function drag(node: HTMLElement, callback: (event: PointerEvent) => void) {
|
||||
const pointerdown = (event: PointerEvent) => {
|
||||
if (
|
||||
(event.pointerType === 'mouse' && event.button === 2) ||
|
||||
(event.pointerType !== 'mouse' && !event.isPrimary)
|
||||
)
|
||||
return;
|
||||
|
||||
node.setPointerCapture(event.pointerId);
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
dragging = true;
|
||||
|
||||
const onpointerup = () => {
|
||||
dragging = false;
|
||||
|
||||
node.setPointerCapture(event.pointerId);
|
||||
|
||||
window.removeEventListener('pointermove', callback, false);
|
||||
window.removeEventListener('pointerup', onpointerup, false);
|
||||
};
|
||||
|
||||
window.addEventListener('pointermove', callback, false);
|
||||
window.addEventListener('pointerup', onpointerup, false);
|
||||
};
|
||||
|
||||
node.addEventListener('pointerdown', pointerdown, { capture: true, passive: false });
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('pointerdown', pointerdown);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--
|
||||
@component
|
||||
@description heavily inspired by `@rich_harris/svelte-split-pane` package. Basically just a migration to svelte5, drop the component and use the library once Rich updates it
|
||||
-->
|
||||
|
||||
<div
|
||||
data-pane={id}
|
||||
class="container {orientation}"
|
||||
bind:this={container}
|
||||
bind:clientWidth={width}
|
||||
bind:clientHeight={height}
|
||||
style="--pos: {position}"
|
||||
>
|
||||
<div class="pane">
|
||||
{@render a()}
|
||||
</div>
|
||||
|
||||
<div class="pane">
|
||||
{@render b()}
|
||||
</div>
|
||||
|
||||
{#if pos !== '0%' && pos !== '100%'}
|
||||
<div
|
||||
class="{orientation} divider"
|
||||
class:disabled
|
||||
use:drag={(e) => update(e.clientX, e.clientY)}
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if dragging}
|
||||
<div class="mousecatcher"></div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.container {
|
||||
--sp-thickness: var(--thickness, 8px);
|
||||
--sp-color: var(--color, transparent);
|
||||
display: grid;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container.vertical {
|
||||
grid-template-rows: var(--pos) 1fr;
|
||||
}
|
||||
|
||||
.container.horizontal {
|
||||
grid-template-columns: var(--pos) 1fr;
|
||||
}
|
||||
|
||||
.pane {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pane > :global(*) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mousecatcher {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.0001);
|
||||
}
|
||||
|
||||
.horizontal + .mousecatcher {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.vertical + .mousecatcher {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.divider {
|
||||
position: absolute;
|
||||
touch-action: none !important;
|
||||
}
|
||||
|
||||
.divider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: var(--sp-color);
|
||||
}
|
||||
|
||||
.horizontal > .divider {
|
||||
padding: 0 calc(0.5 * var(--sp-thickness));
|
||||
width: 0;
|
||||
height: 100%;
|
||||
cursor: ew-resize;
|
||||
left: var(--pos);
|
||||
transform: translate(calc(-0.5 * var(--sp-thickness)), 0);
|
||||
}
|
||||
|
||||
.horizontal > .divider.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.horizontal > .divider::after {
|
||||
left: 50%;
|
||||
top: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vertical > .divider {
|
||||
padding: calc(0.5 * var(--sp-thickness)) 0;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
cursor: ns-resize;
|
||||
top: var(--pos);
|
||||
transform: translate(0, calc(-0.5 * var(--sp-thickness)));
|
||||
}
|
||||
|
||||
.vertical > .divider.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.vertical > .divider::after {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,2 +0,0 @@
|
||||
// TODO: Drop lib when `@rich_harris/svelte-split-pane` is updated for svelte5
|
||||
export { default as SplitPane } from './SplitPane.svelte';
|
||||
@@ -1,48 +0,0 @@
|
||||
export type Length = `${number}px` | `${number}%` | `${number}em` | `${number}rem`;
|
||||
|
||||
export function constrain(
|
||||
element: HTMLElement,
|
||||
size: number,
|
||||
min: Length,
|
||||
max: Length,
|
||||
pos: Length
|
||||
): Length {
|
||||
let min_px = normalize(min, element, size);
|
||||
let max_px = normalize(max, element, size);
|
||||
let pos_px = normalize(pos, element, size);
|
||||
|
||||
if (min_px < 0) min_px += size;
|
||||
if (max_px < 0) max_px += size;
|
||||
|
||||
pos_px = Math.max(min_px, Math.min(max_px, pos_px));
|
||||
|
||||
const position: Length = pos.endsWith('%')
|
||||
? size
|
||||
? `${(100 * pos_px) / size}%`
|
||||
: '0%'
|
||||
: `${pos_px}px`;
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
function normalize(str: string, element: HTMLElement, size: number) {
|
||||
const num = parseFloat(str);
|
||||
|
||||
if (str.endsWith('px')) {
|
||||
return num;
|
||||
}
|
||||
|
||||
if (str.endsWith('%')) {
|
||||
return (size * num) / 100;
|
||||
}
|
||||
|
||||
if (str.endsWith('rem')) {
|
||||
return num * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
}
|
||||
|
||||
if (str.endsWith('em')) {
|
||||
return num * parseFloat(getComputedStyle(element).fontSize);
|
||||
}
|
||||
|
||||
throw new Error(`Invalid length: ${str}`);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
actions: Snippet;
|
||||
}
|
||||
|
||||
let { actions }: Props = $props();
|
||||
</script>
|
||||
|
||||
<!--
|
||||
@component
|
||||
@description Placeholder for a custom window titlebar.
|
||||
It can only work with `titleBarStyle` set to `Overlay` in `tauri.conf.json`
|
||||
-->
|
||||
|
||||
<header data-tauri-drag-region>
|
||||
<div>
|
||||
{@render actions()}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--window-title-bar-height);
|
||||
background-color: hsl(0deg 0% 18%);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
padding: 0 8px;
|
||||
}
|
||||
</style>
|
||||
20
src/lib/icons/Bars3.svelte
Normal file
20
src/lib/icons/Bars3.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
|
||||
interface Props extends Omit<SvelteHTMLElements['svg'], 'width' | 'height'> {
|
||||
size?: string | number | null;
|
||||
}
|
||||
|
||||
let { size = 24, ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" width={size} height={size} {...rest} data-name="bars-3">
|
||||
<path
|
||||
fill="none"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||
/>
|
||||
</svg>
|
||||
20
src/lib/icons/Play.svelte
Normal file
20
src/lib/icons/Play.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
|
||||
interface Props extends Omit<SvelteHTMLElements['svg'], 'width' | 'height'> {
|
||||
size?: string | number | null;
|
||||
}
|
||||
|
||||
let { size = 24, ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" width={size} height={size} {...rest} data-name="play">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z"
|
||||
/>
|
||||
</svg>
|
||||
16
src/lib/icons/Save.svelte
Normal file
16
src/lib/icons/Save.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
|
||||
interface Props extends Omit<SvelteHTMLElements['svg'], 'width' | 'height'> {
|
||||
size?: string | number | null;
|
||||
}
|
||||
|
||||
let { size = 24, ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" {...rest}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M3 5a2 2 0 0 1 2-2h11.586A2 2 0 0 1 18 3.586l2.707 2.707A1 1 0 0 1 21 7v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2zm6 14h6v-6H9zm8 0h2V7.414l-2-2V7a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2V5H5v14h2v-6a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2zM9 5v2h6V5z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
@layer variables {
|
||||
:root {
|
||||
--window-title-bar-height: 28px;
|
||||
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenuState } from '$lib/components/ContextMenu';
|
||||
import ContextMenu from '$lib/components/ContextMenu/ContextMenu.svelte';
|
||||
import Drawer from '$lib/components/Drawer.svelte';
|
||||
import { Editor } from '$lib/components/Editor';
|
||||
import { SaveQueryModal } from '$lib/components/Queries';
|
||||
import Result from '$lib/components/Result.svelte';
|
||||
import SideBar from '$lib/components/SideBar.svelte';
|
||||
import { SplitPane } from '$lib/components/SplitPane';
|
||||
import WindowTitleBar from '$lib/components/WindowTitleBar.svelte';
|
||||
import { set_app_context } from '$lib/context';
|
||||
import Bars3 from '$lib/icons/Bars3.svelte';
|
||||
import Play from '$lib/icons/Play.svelte';
|
||||
import Save from '$lib/icons/Save.svelte';
|
||||
import type { Table } from '$lib/olap-engine';
|
||||
import { engine, type OLAPResponse } from '$lib/olap-engine';
|
||||
import { history_repository, type HistoryEntry } from '$lib/repositories/history';
|
||||
import { query_repository, type Query } from '$lib/repositories/queries';
|
||||
import type { PageData } from './$types';
|
||||
import { SplitPane } from '@rich_harris/svelte-split-pane';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let response = $state.raw<OLAPResponse>();
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let query = $state('');
|
||||
let loading = $state(false);
|
||||
|
||||
@@ -27,12 +28,13 @@
|
||||
}
|
||||
|
||||
loading = true;
|
||||
response = await engine.exec(query).finally(() => (loading = false));
|
||||
const query_to_execute = query;
|
||||
response = await engine.exec(query_to_execute).finally(() => (loading = false));
|
||||
|
||||
const last = await history_repository.getLast();
|
||||
|
||||
if (response && last?.content !== query) {
|
||||
await addHistoryEntry();
|
||||
if (response && last?.content !== query_to_execute) {
|
||||
await addHistoryEntry(query_to_execute);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +54,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
async function addHistoryEntry() {
|
||||
async function addHistoryEntry(query: string) {
|
||||
try {
|
||||
const entry = await history_repository.add(query);
|
||||
history = [entry, ...history];
|
||||
@@ -63,6 +65,7 @@
|
||||
|
||||
function handleHistoryClick(entry: HistoryEntry) {
|
||||
query = entry.content;
|
||||
if (is_mobile) open_drawer = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
@@ -80,53 +83,104 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateQuery({
|
||||
name
|
||||
}: Parameters<NonNullable<ComponentProps<typeof SaveQueryModal>['onCreate']>>['0']) {
|
||||
const q = await query_repository.create(name, query);
|
||||
queries = queries.concat(q);
|
||||
}
|
||||
|
||||
async function handleDeleteQuery(query: Query) {
|
||||
await query_repository.delete(query.id);
|
||||
const index = queries.indexOf(query);
|
||||
queries = queries.slice(0, index).concat(queries.slice(index + 1));
|
||||
}
|
||||
|
||||
function handleQueryOpen(_query: Query) {
|
||||
query = _query.sql;
|
||||
if (is_mobile) open_drawer = false;
|
||||
}
|
||||
|
||||
async function handleQueryRename(query: Query) {
|
||||
const updated = await query_repository.update(query);
|
||||
const index = queries.findIndex((query) => query.id === updated.id);
|
||||
if (index !== -1) {
|
||||
queries = queries
|
||||
.slice(0, index)
|
||||
.concat(updated)
|
||||
.concat(queries.slice(index + 1));
|
||||
}
|
||||
}
|
||||
|
||||
const context_menu = new ContextMenuState();
|
||||
set_app_context({ context_menu });
|
||||
|
||||
let screen_width = $state(0);
|
||||
let is_mobile = $derived(screen_width < 768 && WEB_APP);
|
||||
let open_drawer = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (!is_mobile) open_drawer = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeyDown} />
|
||||
<svelte:window onkeydown={handleKeyDown} bind:innerWidth={screen_width} />
|
||||
|
||||
<ContextMenu state={context_menu} />
|
||||
|
||||
<WindowTitleBar>
|
||||
{#snippet actions()}
|
||||
<button onclick={() => save_query_modal?.show()} disabled={!query}>Save</button>
|
||||
<button onclick={handleExec} disabled={loading}>Run</button>
|
||||
{/snippet}
|
||||
</WindowTitleBar>
|
||||
{#snippet sidebar()}
|
||||
<SideBar
|
||||
{tables}
|
||||
{history}
|
||||
onHistoryClick={handleHistoryClick}
|
||||
{queries}
|
||||
onQueryDelete={handleDeleteQuery}
|
||||
onQueryOpen={handleQueryOpen}
|
||||
onQueryRename={handleQueryRename}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
<section class="screen">
|
||||
<SplitPane orientation="horizontal" position="242px" min="242px" max="40%">
|
||||
{#if is_mobile}
|
||||
<Drawer bind:open={open_drawer} width={242}>
|
||||
{@render sidebar()}
|
||||
</Drawer>
|
||||
{/if}
|
||||
<SplitPane
|
||||
type="horizontal"
|
||||
disabled={is_mobile}
|
||||
pos={is_mobile ? '0px' : '242px'}
|
||||
min={is_mobile ? '0px' : '242px'}
|
||||
max="40%"
|
||||
>
|
||||
{#snippet a()}
|
||||
<SideBar
|
||||
{tables}
|
||||
{history}
|
||||
onHistoryClick={handleHistoryClick}
|
||||
{queries}
|
||||
onQueryDelete={async (query) => {
|
||||
await query_repository.delete(query.id);
|
||||
const index = queries.indexOf(query);
|
||||
queries = queries.slice(0, index).concat(queries.slice(index + 1));
|
||||
}}
|
||||
onQueryOpen={(q) => {
|
||||
query = q.sql;
|
||||
}}
|
||||
onQueryRename={async (q) => {
|
||||
const updated = await query_repository.update(q);
|
||||
const index = queries.findIndex((_q) => _q.id === updated.id);
|
||||
if (index !== -1) {
|
||||
queries = queries
|
||||
.slice(0, index)
|
||||
.concat(updated)
|
||||
.concat(queries.slice(index + 1));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if !is_mobile}
|
||||
{@render sidebar()}
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet b()}
|
||||
<SplitPane orientation="vertical" min="20%" max="80%" --color="hsl(0deg 0% 12%)">
|
||||
<SplitPane type="vertical" min="20%" max="80%" --color="hsl(0deg 0% 12%)">
|
||||
{#snippet a()}
|
||||
<Editor bind:value={query} onExec={handleExec} {tables} />
|
||||
<div>
|
||||
<nav class="Tabs">
|
||||
<div class="left">
|
||||
{#if is_mobile}
|
||||
<button onclick={() => (open_drawer = true)}>
|
||||
<Bars3 size="12" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="right">
|
||||
<button onclick={() => save_query_modal?.show()} disabled={!query}>
|
||||
<Save size="12" />
|
||||
</button>
|
||||
<button onclick={handleExec} disabled={loading}><Play size="12" /></button>
|
||||
</div>
|
||||
</nav>
|
||||
<div>
|
||||
<Editor bind:value={query} onExec={handleExec} {tables} />
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet b()}
|
||||
<Result {response} />
|
||||
@@ -136,15 +190,33 @@
|
||||
</SplitPane>
|
||||
</section>
|
||||
|
||||
<SaveQueryModal
|
||||
bind:this={save_query_modal}
|
||||
onCreate={async ({ name }) => {
|
||||
const q = await query_repository.create(name, query);
|
||||
queries = queries.concat(q);
|
||||
}}
|
||||
/>
|
||||
<SaveQueryModal bind:this={save_query_modal} onCreate={handleCreateQuery} />
|
||||
|
||||
<style>
|
||||
.Tabs {
|
||||
height: 28px;
|
||||
display: flex;
|
||||
|
||||
& > .left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
& > .right {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
& button {
|
||||
height: 100%;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
& ~ div {
|
||||
height: calc(100% - 28px);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
appearance: none;
|
||||
outline: none;
|
||||
@@ -161,7 +233,6 @@
|
||||
}
|
||||
|
||||
.screen {
|
||||
padding-top: var(--window-title-bar-height);
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
3
src/vite-env.d.ts
vendored
3
src/vite-env.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
declare const FORCE_REMOTE_ENGINE: string;
|
||||
declare const FORCE_REMOTE_ENGINE: boolean;
|
||||
declare const WEB_APP: boolean;
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
@@ -22,6 +22,7 @@ export default defineConfig(async () => ({
|
||||
exclude: ['@sqlite.org/sqlite-wasm']
|
||||
},
|
||||
define: {
|
||||
FORCE_REMOTE_ENGINE: process.env.FORCE_REMOTE_ENGINE === 'true'
|
||||
FORCE_REMOTE_ENGINE: process.env.FORCE_REMOTE_ENGINE === 'true',
|
||||
WEB_APP: process.env.WEB_APP === 'true'
|
||||
}
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user