tab context menu and shortcuts

This commit is contained in:
vcoppe
2024-04-29 17:03:23 +02:00
parent a6bcebf306
commit 49053bcaaa
20 changed files with 409 additions and 24 deletions

View File

@@ -9,7 +9,7 @@
"version": "0.0.1",
"dependencies": {
"@mapbox/mapbox-gl-geocoder": "^5.0.2",
"bits-ui": "^0.21.4",
"bits-ui": "^0.21.5",
"chart.js": "^4.4.2",
"clsx": "^2.1.0",
"gpx": "file:../gpx",
@@ -1891,9 +1891,9 @@
}
},
"node_modules/bits-ui": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.21.4.tgz",
"integrity": "sha512-IL+7s19GW561jwkeYk23dwkTfQ9606I062qqv2AtjCdhhIdoOEJNVBX0kjP5xefSaS6ojL0HGG54att0aRTcAQ==",
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.21.5.tgz",
"integrity": "sha512-EDGHWkxnlcV2fbXn2tMps3SfpS7k6bfX3BrQ4s/h79jT6yprBS8DdDficlDK0SDHmPYHBZ0hSy4OgQUDodS/6w==",
"dependencies": {
"@internationalized/date": "^3.5.1",
"@melt-ui/svelte": "0.76.2",

View File

@@ -42,7 +42,7 @@
"type": "module",
"dependencies": {
"@mapbox/mapbox-gl-geocoder": "^5.0.2",
"bits-ui": "^0.21.4",
"bits-ui": "^0.21.5",
"chart.js": "^4.4.2",
"clsx": "^2.1.0",
"gpx": "file:../gpx",

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import { fileOrder, files, getFileIndex, selectedFiles, selectFiles } from '$lib/stores';
import { Button } from '$lib/components/ui/button';
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
import Sortable from 'sortablejs/Sortable';
@@ -9,6 +8,7 @@
import { afterUpdate, onMount } from 'svelte';
import { get } from 'svelte/store';
import FileListItem from './FileListItem.svelte';
let container: HTMLDivElement;
let buttons: HTMLDivElement[] = [];
@@ -133,9 +133,7 @@
data-id={index}
class="pointer-events-auto first:ml-1 last:mr-1 mb-1 bg-transparent"
>
<Button variant="outline" class="h-9 px-1.5 py-1 border-none shadow-md">
{get(file).metadata.name}
</Button>
<FileListItem {file} />
</div>
{/each}
</div>
@@ -148,7 +146,7 @@
@apply hover:bg-background;
}
div :global(.sortable-selected > button) {
div :global(.sortable-selected button) {
@apply bg-background;
}
</style>

View File

@@ -0,0 +1,50 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as ContextMenu from '$lib/components/ui/context-menu';
import Shortcut from './Shortcut.svelte';
import { Copy, Trash2 } from 'lucide-svelte';
import type { GPXFile } from 'gpx';
import { get, type Writable } from 'svelte/store';
import {
duplicateSelectedFiles,
removeSelectedFiles,
selectedFiles,
selectFiles
} from '$lib/stores';
import { _ } from 'svelte-i18n';
export let file: Writable<GPXFile>;
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
on:contextmenu={() => {
if (!get(selectedFiles).has(get(file))) {
get(selectFiles).select(get(file));
}
}}
>
<ContextMenu.Root>
<ContextMenu.Trigger>
<Button variant="outline" class="h-9 px-1.5 py-1 border-none shadow-md">
{$file.metadata.name}
</Button>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item on:click={duplicateSelectedFiles}>
<Copy size="16" class="mr-1" />
{$_('menu.duplicate')}
<Shortcut key="D" ctrl={true} /></ContextMenu.Item
>
<ContextMenu.Separator />
<ContextMenu.Item on:click={removeSelectedFiles}
><Trash2 size="16" class="mr-1" />
{$_('menu.delete')}
<Shortcut key="⌫" ctrl={true} /></ContextMenu.Item
>
</ContextMenu.Content>
</ContextMenu.Root>
</div>

View File

@@ -2,6 +2,7 @@
import * as Menubar from '$lib/components/ui/menubar/index.js';
import { Button } from '$lib/components/ui/button';
import Logo from './Logo.svelte';
import Shortcut from './Shortcut.svelte';
import { Plus, Copy, Download, Undo2, Redo2, Trash2, Upload, Cloud, Heart } from 'lucide-svelte';
import {
@@ -55,13 +56,13 @@
<Menubar.Item on:click={createFile}>
<Plus size="16" class="mr-1" />
{$_('menu.new')}
<Menubar.Shortcut>⌘N</Menubar.Shortcut>
<Shortcut key="N" ctrl={true} />
</Menubar.Item>
<Menubar.Separator />
<Menubar.Item on:click={triggerFileInput}>
<Upload size="16" class="mr-1" />
{$_('menu.load_desktop')}
<Menubar.Shortcut>⌘O</Menubar.Shortcut>
<Shortcut key="O" ctrl={true} />
</Menubar.Item>
<Menubar.Item>
<Cloud size="16" class="mr-1" />
@@ -71,18 +72,18 @@
<Menubar.Item on:click={duplicateSelectedFiles} disabled={$selectedFiles.size == 0}>
<Copy size="16" class="mr-1" />
{$_('menu.duplicate')}
<Menubar.Shortcut>⌘D</Menubar.Shortcut>
<Shortcut key="D" ctrl={true} />
</Menubar.Item>
<Menubar.Separator />
<Menubar.Item on:click={exportSelectedFiles} disabled={$selectedFiles.size == 0}>
<Download size="16" class="mr-1" />
{$_('menu.export')}
<Menubar.Shortcut>⌘S</Menubar.Shortcut>
<Shortcut key="S" ctrl={true} />
</Menubar.Item>
<Menubar.Item on:click={exportAllFiles} disabled={$files.length == 0}>
<Download size="16" class="mr-1" />
{$_('menu.export_all')}
<Menubar.Shortcut>⇧⌘S</Menubar.Shortcut>
<Shortcut key="S" ctrl={true} shift={true} />
</Menubar.Item>
</Menubar.Content>
</Menubar.Menu>
@@ -92,27 +93,34 @@
<Menubar.Item>
<Undo2 size="16" class="mr-1" />
{$_('menu.undo')}
<Menubar.Shortcut>⌘Z</Menubar.Shortcut>
<Shortcut key="Z" ctrl={true} />
</Menubar.Item>
<Menubar.Item>
<Redo2 size="16" class="mr-1" />
{$_('menu.redo')}
<Menubar.Shortcut>⇧⌘Z</Menubar.Shortcut>
<Shortcut key="Z" ctrl={true} shift={true} />
</Menubar.Item>
<Menubar.Separator />
<Menubar.Item on:click={() => $selectFiles.selectAllFiles()}>
<span class="w-4 mr-1"></span>
{$_('menu.select_all')}
<Shortcut key="A" ctrl={true} />
</Menubar.Item>
<Menubar.Separator />
<Menubar.Item on:click={removeSelectedFiles} disabled={$selectedFiles.size == 0}>
<Trash2 size="16" class="mr-1" />
{$_('menu.delete')}
<Menubar.Shortcut>⌘⌫</Menubar.Shortcut></Menubar.Item
>
<Shortcut key="⌫" ctrl={true} />
</Menubar.Item>
<Menubar.Item
class="text-destructive data-[highlighted]:text-destructive"
on:click={removeAllFiles}
disabled={$files.length == 0}
>
<Trash2 size="16" class="mr-1" />
{$_('menu.delete_all')}<Menubar.Shortcut>⇧⌘⌫</Menubar.Shortcut></Menubar.Item
>
{$_('menu.delete_all')}
<Shortcut key="⌫" ctrl={true} shift={true} />
</Menubar.Item>
</Menubar.Content>
</Menubar.Menu>
<Menubar.Menu>

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
export let key: string;
export let shift: boolean = false;
export let ctrl: boolean = false;
export let click: boolean = false;
let isMac = false;
onMount(() => {
isMac = navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
});
</script>
<span class="ml-auto pl-2 text-xs tracking-widest text-muted-foreground"
>{shift ? '⇧' : ''}{ctrl ? (isMac ? '⌘' : $_('menu.ctrl') + '+') : ''}{key}{click
? $_('menu.click')
: ''}</span
>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import Shortcut from '$lib/components/Shortcut.svelte';
import { Trash2 } from 'lucide-svelte';
import { _ } from 'svelte-i18n';
@@ -15,8 +16,11 @@
class="w-full px-2 py-1 h-6 justify-start"
variant="ghost"
on:click={() => element.dispatchEvent(new CustomEvent('delete'))}
><Trash2 size="16" class="mr-1" /> {$_('menu.delete')}</Button
>
<Trash2 size="16" class="mr-1" />
{$_('menu.delete')}
<Shortcut key="" shift={true} click={true} />
</Button>
</Card.Content>
</Card.Root>
</div>

View File

@@ -130,13 +130,19 @@ export class RoutingControls {
});
marker.on('dragend', this.moveAnchor.bind(this));
marker.getElement().addEventListener('click', (e) => {
e.stopPropagation();
if (Date.now() - lastDragEvent < 100) { // Prevent click event during drag
return;
}
if (e.shiftKey) {
this.deleteAnchor(anchor);
return;
}
marker.setPopup(this.popup);
marker.togglePopup();
e.stopPropagation();
let deleteThisAnchor = this.getDeleteAnchor(anchor);
this.popupElement.addEventListener('delete', deleteThisAnchor); // Register the delete event for this anchor

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import { cn } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.CheckboxItemProps;
type $$Events = ContextMenuPrimitive.CheckboxItemEvents;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = undefined;
export { className as class };
</script>
<ContextMenuPrimitive.CheckboxItem
bind:checked
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.CheckboxIndicator>
<Check class="h-4 w-4" />
</ContextMenuPrimitive.CheckboxIndicator>
</span>
<slot />
</ContextMenuPrimitive.CheckboxItem>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
export { className as class };
</script>
<ContextMenuPrimitive.Content
{transition}
{transitionConfig}
class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none",
className
)}
{...$$restProps}
on:keydown
>
<slot />
</ContextMenuPrimitive.Content>

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.ItemProps & {
inset?: boolean;
};
type $$Events = ContextMenuPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<ContextMenuPrimitive.Item
class={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<slot />
</ContextMenuPrimitive.Item>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.LabelProps & {
inset?: boolean;
};
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<ContextMenuPrimitive.Label
class={cn("px-2 py-1.5 text-sm font-semibold text-foreground", inset && "pl-8", className)}
{...$$restProps}
>
<slot />
</ContextMenuPrimitive.Label>

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
type $$Props = ContextMenuPrimitive.RadioGroupProps;
export let value: $$Props["value"] = undefined;
</script>
<ContextMenuPrimitive.RadioGroup {...$$restProps} bind:value>
<slot />
</ContextMenuPrimitive.RadioGroup>

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.RadioItemProps;
type $$Events = ContextMenuPrimitive.RadioItemEvents;
let className: $$Props["class"] = undefined;
export let value: ContextMenuPrimitive.RadioItemProps["value"];
export { className as class };
</script>
<ContextMenuPrimitive.RadioItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{value}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.RadioIndicator>
<Circle class="h-2 w-2 fill-current" />
</ContextMenuPrimitive.RadioIndicator>
</span>
<slot />
</ContextMenuPrimitive.RadioItem>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.SeparatorProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<ContextMenuPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-border", className)}
{...$$restProps}
/>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span
class={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...$$restProps}
>
<slot />
</span>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.SubContentProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = {
x: -10,
y: 0,
};
export { className as class };
</script>
<ContextMenuPrimitive.SubContent
{transition}
{transitionConfig}
class={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none",
className
)}
{...$$restProps}
on:keydown
on:focusout
on:pointermove
>
<slot />
</ContextMenuPrimitive.SubContent>

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.SubTriggerProps & {
inset?: boolean;
};
type $$Events = ContextMenuPrimitive.SubTriggerEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<ContextMenuPrimitive.SubTrigger
class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
>
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>

View File

@@ -0,0 +1,49 @@
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import Item from "./context-menu-item.svelte";
import Label from "./context-menu-label.svelte";
import Content from "./context-menu-content.svelte";
import Shortcut from "./context-menu-shortcut.svelte";
import RadioItem from "./context-menu-radio-item.svelte";
import Separator from "./context-menu-separator.svelte";
import RadioGroup from "./context-menu-radio-group.svelte";
import SubContent from "./context-menu-sub-content.svelte";
import SubTrigger from "./context-menu-sub-trigger.svelte";
import CheckboxItem from "./context-menu-checkbox-item.svelte";
const Sub = ContextMenuPrimitive.Sub;
const Root = ContextMenuPrimitive.Root;
const Trigger = ContextMenuPrimitive.Trigger;
const Group = ContextMenuPrimitive.Group;
export {
Sub,
Root,
Item,
Label,
Group,
Trigger,
Content,
Shortcut,
Separator,
RadioItem,
SubContent,
SubTrigger,
RadioGroup,
CheckboxItem,
//
Root as ContextMenu,
Sub as ContextMenuSub,
Item as ContextMenuItem,
Label as ContextMenuLabel,
Group as ContextMenuGroup,
Content as ContextMenuContent,
Trigger as ContextMenuTrigger,
Shortcut as ContextMenuShortcut,
RadioItem as ContextMenuRadioItem,
Separator as ContextMenuSeparator,
RadioGroup as ContextMenuRadioGroup,
SubContent as ContextMenuSubContent,
SubTrigger as ContextMenuSubTrigger,
CheckboxItem as ContextMenuCheckboxItem,
};

View File

@@ -13,6 +13,7 @@
"redo": "Redo",
"delete": "Delete",
"delete_all": "Delete all",
"select_all": "Select all",
"settings": "Settings",
"distance_units": "Distance units",
"metric": "Metric",
@@ -28,7 +29,9 @@
"distance_markers": "Show distance markers",
"direction_markers": "Show direction markers",
"about": "About",
"donate": "Donate"
"donate": "Donate",
"ctrl": "Ctrl",
"click": "Click"
},
"toolbar": {
"routing": {