Merge pull request #49 from agnosticeng/chore/sidebar-mobile

chore: hide sidebar on mobile
This commit is contained in:
Didier Franc
2025-01-09 12:57:43 +01:00
committed by GitHub
21 changed files with 259 additions and 387 deletions

28
package-lock.json generated
View File

@@ -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"
}
}

View File

@@ -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",

View File

@@ -6,7 +6,6 @@
"main"
],
"permissions": [
"core:default",
"core:window:allow-start-dragging"
"core:default"
]
}

View File

@@ -40,7 +40,6 @@
"width": 1024,
"height": 768,
"theme": "Dark",
"titleBarStyle": "Overlay",
"useHttpsScheme": true
}
]

View 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>

View File

@@ -72,6 +72,7 @@
.content {
height: 18px;
font-weight: 600;
padding: 3px 0;
line-height: 1.15;

View File

@@ -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;

View File

@@ -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;

View File

@@ -52,6 +52,7 @@
}
& > nav {
flex-shrink: 0;
padding: 7px 5px;
border-top: 1px solid hsl(0deg 0% 29%);
user-select: none;

View File

@@ -55,6 +55,9 @@
<style>
section {
height: 100%;
width: 100%;
padding: 14px 18px;
background-color: hsl(0deg 0% 9%);
display: flex;

View File

@@ -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>

View File

@@ -1,2 +0,0 @@
// TODO: Drop lib when `@rich_harris/svelte-split-pane` is updated for svelte5
export { default as SplitPane } from './SplitPane.svelte';

View File

@@ -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}`);
}

View File

@@ -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>

View 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
View 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
View 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>

View File

@@ -4,8 +4,6 @@
@layer variables {
:root {
--window-title-bar-height: 28px;
font-size: 0.75rem;
}
}

View File

@@ -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
View File

@@ -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>;

View File

@@ -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'
}
}));