Compare commits
1 commit
main
...
dropdown-m
Author | SHA1 | Date | |
---|---|---|---|
5d42490492 |
19 changed files with 569 additions and 15 deletions
|
@ -15,11 +15,13 @@
|
||||||
"typesafe-i18n": "tsx scripts/import_translations.ts"
|
"typesafe-i18n": "tsx scripts/import_translations.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.6.0",
|
||||||
"@fontsource-variable/inter": "^5.0.16",
|
"@fontsource-variable/inter": "^5.0.16",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@resreq/event-hub": "^1.6.0",
|
"@resreq/event-hub": "^1.6.0",
|
||||||
"daisyui": "^4.7.2",
|
"daisyui": "^4.7.2",
|
||||||
"fast-average-color": "^9.4.0",
|
"fast-average-color": "^9.4.0",
|
||||||
|
"svelte-floating-ui": "^1.5.8",
|
||||||
"svelte-inview": "^4.0.2",
|
"svelte-inview": "^4.0.2",
|
||||||
"svelte-local-storage-store": "^0.6.4",
|
"svelte-local-storage-store": "^0.6.4",
|
||||||
"typesafe-i18n": "^5.26.2"
|
"typesafe-i18n": "^5.26.2"
|
||||||
|
|
|
@ -5,6 +5,9 @@ settings:
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@floating-ui/core':
|
||||||
|
specifier: ^1.6.0
|
||||||
|
version: 1.6.0
|
||||||
'@fontsource-variable/inter':
|
'@fontsource-variable/inter':
|
||||||
specifier: ^5.0.16
|
specifier: ^5.0.16
|
||||||
version: 5.0.16
|
version: 5.0.16
|
||||||
|
@ -20,6 +23,9 @@ dependencies:
|
||||||
fast-average-color:
|
fast-average-color:
|
||||||
specifier: ^9.4.0
|
specifier: ^9.4.0
|
||||||
version: 9.4.0
|
version: 9.4.0
|
||||||
|
svelte-floating-ui:
|
||||||
|
specifier: ^1.5.8
|
||||||
|
version: 1.5.8
|
||||||
svelte-inview:
|
svelte-inview:
|
||||||
specifier: ^4.0.2
|
specifier: ^4.0.2
|
||||||
version: 4.0.2(svelte@4.2.11)
|
version: 4.0.2(svelte@4.2.11)
|
||||||
|
@ -392,18 +398,15 @@ packages:
|
||||||
resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==}
|
resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/utils': 0.2.1
|
'@floating-ui/utils': 0.2.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@floating-ui/dom@1.6.3:
|
/@floating-ui/dom@1.6.3:
|
||||||
resolution: {integrity: sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==}
|
resolution: {integrity: sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/core': 1.6.0
|
'@floating-ui/core': 1.6.0
|
||||||
'@floating-ui/utils': 0.2.1
|
'@floating-ui/utils': 0.2.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@floating-ui/utils@0.2.1:
|
/@floating-ui/utils@0.2.1:
|
||||||
resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==}
|
resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@fontsource-variable/inter@5.0.16:
|
/@fontsource-variable/inter@5.0.16:
|
||||||
resolution: {integrity: sha512-k+BUNqksTL+AN+o+OV7ILeiE9B5M5X+/jA7LWvCwjbV9ovXTqZyKRhA/x7uYv/ml8WQ0XNLBM7cRFIx4jW0/hg==}
|
resolution: {integrity: sha512-k+BUNqksTL+AN+o+OV7ILeiE9B5M5X+/jA7LWvCwjbV9ovXTqZyKRhA/x7uYv/ml8WQ0XNLBM7cRFIx4jW0/hg==}
|
||||||
|
@ -2892,6 +2895,13 @@ packages:
|
||||||
svelte: 4.2.11
|
svelte: 4.2.11
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/svelte-floating-ui@1.5.8:
|
||||||
|
resolution: {integrity: sha512-dVvJhZ2bT+kQDHlE4Lep8t+sgEc0XD96fXLzAi2DDI2bsaegBbClxXVNMma0C2WsG+n9GJSYx292dTvA8CYRtw==}
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/core': 1.6.0
|
||||||
|
'@floating-ui/dom': 1.6.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/svelte-hmr@0.15.3(svelte@4.2.11):
|
/svelte-hmr@0.15.3(svelte@4.2.11):
|
||||||
resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==}
|
resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==}
|
||||||
engines: {node: ^12.20 || ^14.13.1 || >= 16}
|
engines: {node: ^12.20 || ^14.13.1 || >= 16}
|
||||||
|
|
150
src/lib/components/contextmenu/BottomSheet.svelte
Normal file
150
src/lib/components/contextmenu/BottomSheet.svelte
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import swipeable from "$lib/util/actions/swipeable";
|
||||||
|
import { type Coords, Direction } from "$lib/util/types";
|
||||||
|
import { mdiCog } from "@mdi/js";
|
||||||
|
import Icon from "$lib/components/ui/Icon.svelte";
|
||||||
|
import { screenHeight } from "$lib/stores/layout";
|
||||||
|
import { clamp } from "$lib/util/functions";
|
||||||
|
|
||||||
|
let offset = 50; // 0: top, 50: half
|
||||||
|
|
||||||
|
// onMount(reset);
|
||||||
|
|
||||||
|
let swiping = false;
|
||||||
|
|
||||||
|
function onSwipeStart() {
|
||||||
|
swiping = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSwipeMove(event: CustomEvent<Coords & { dx: number; dy: number }>) {
|
||||||
|
let dperc = (event.detail.dy / $screenHeight) * 100;
|
||||||
|
offset = clamp(offset + dperc, 0, 95);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSwipeEnd(event: CustomEvent<{ direction: Direction | null }>) {
|
||||||
|
if (offset > 60) {
|
||||||
|
offset = 100;
|
||||||
|
} else if (offset < 40) {
|
||||||
|
offset = 0;
|
||||||
|
} else {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
swiping = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
swiping = false;
|
||||||
|
offset = 50;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bottomsheet h-full"
|
||||||
|
class:extended={!swiping && offset === 0}
|
||||||
|
style="top: {offset}%"
|
||||||
|
use:swipeable={{ thresholdProvider: () => $screenHeight * 0.1 }}
|
||||||
|
on:swipeStart={onSwipeStart}
|
||||||
|
on:swipeMove={onSwipeMove}
|
||||||
|
on:swipeEnd={onSwipeEnd}
|
||||||
|
on:swipeFailed={reset}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="handle" />
|
||||||
|
|
||||||
|
<ul class="bottomsheet-menu">
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 1</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 2</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 3</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 4</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 5</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 6</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 7</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 8</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 9</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 10</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 11</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 12</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 13</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 14</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 15</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 16</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 17</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 18</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 19</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 20</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 21</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 22</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 23</a></li>
|
||||||
|
<li><Icon path={mdiCog} size={1.4} /><a>Item 24</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.bottomsheet {
|
||||||
|
@apply fixed flex left-0 bottom-0 z-50 w-full select-none;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
@apply bg-base-300 rounded-t-xl m-2 w-full h-full;
|
||||||
|
@apply flex flex-col items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.extended > div {
|
||||||
|
@apply overflow-x-auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
@apply w-12 h-1 m-2 flex-shrink-0 rounded-full bg-base-content/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomsheet-menu {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
li {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 40px;
|
||||||
|
@apply p-2 pr-4 gap-2;
|
||||||
|
|
||||||
|
.icon-r {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply bg-base-content/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.noicon {
|
||||||
|
grid-template-columns: [txt] 1fr;
|
||||||
|
@apply px-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-sm {
|
||||||
|
height: 1.75rem;
|
||||||
|
@apply py-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sep::before {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
@apply border-b-neutral-content/50;
|
||||||
|
content: "";
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
134
src/lib/components/contextmenu/TestCtxMenu2.svelte
Normal file
134
src/lib/components/contextmenu/TestCtxMenu2.svelte
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { TEST_MENU } from "./menus";
|
||||||
|
import { type ContentAction } from "svelte-floating-ui";
|
||||||
|
import TestCtxMenu2Item from "./TestCtxMenu2Item.svelte";
|
||||||
|
import { kbd } from "@melt-ui/svelte/internal/helpers";
|
||||||
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
import outclick from "$lib/util/actions/outclick";
|
||||||
|
import { LIST_PAGENAV_N } from "$lib/util/constants";
|
||||||
|
import { clamp } from "$lib/util/functions";
|
||||||
|
|
||||||
|
export let menu = TEST_MENU;
|
||||||
|
export let menuOpen = false;
|
||||||
|
export let floatingContent: ContentAction | (() => void) = () => {};
|
||||||
|
export let mainMenuClose: (() => void) | undefined = undefined;
|
||||||
|
export let submenuClose: (() => void) | undefined = undefined;
|
||||||
|
|
||||||
|
let menuElements: TestCtxMenu2Item[] = [];
|
||||||
|
let openSubmenu: Writable<string | null> = writable(null);
|
||||||
|
|
||||||
|
export function open() {
|
||||||
|
menuOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function close() {
|
||||||
|
menuOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggle() {
|
||||||
|
if (menuOpen) close();
|
||||||
|
else open();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function focusFirst() {
|
||||||
|
if (menuElements.length > 0 && getCurrentIndex() === -1) menuElements[0].focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentIndex(): number {
|
||||||
|
return menuElements.findIndex((elm) => elm.getIsActive());
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(e: KeyboardEvent) {
|
||||||
|
if (handleMenuNavigation(e)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/melt-ui/melt-ui/blob/develop/src/lib/builders/menu/create.ts
|
||||||
|
|
||||||
|
function handleMenuNavigation(e: KeyboardEvent): boolean {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Index of the currently focused item in the candidate nodes array
|
||||||
|
const currentIndex = getCurrentIndex();
|
||||||
|
if (currentIndex === -1) return false;
|
||||||
|
|
||||||
|
// Calculate the index of the next menu item
|
||||||
|
let nextIndex = currentIndex;
|
||||||
|
switch (e.key) {
|
||||||
|
case kbd.ARROW_DOWN:
|
||||||
|
if (e.ctrlKey) nextIndex = menuElements.length - 1;
|
||||||
|
else nextIndex++;
|
||||||
|
break;
|
||||||
|
case kbd.ARROW_UP:
|
||||||
|
if (e.ctrlKey) nextIndex = 0;
|
||||||
|
else nextIndex--;
|
||||||
|
break;
|
||||||
|
case kbd.HOME:
|
||||||
|
nextIndex = 0;
|
||||||
|
break;
|
||||||
|
case kbd.END:
|
||||||
|
nextIndex = menuElements.length - 1;
|
||||||
|
break;
|
||||||
|
case kbd.PAGE_DOWN:
|
||||||
|
nextIndex += LIST_PAGENAV_N;
|
||||||
|
break;
|
||||||
|
case kbd.PAGE_UP:
|
||||||
|
nextIndex -= LIST_PAGENAV_N;
|
||||||
|
break;
|
||||||
|
case kbd.ARROW_RIGHT:
|
||||||
|
if (menu[currentIndex].submenu) {
|
||||||
|
menuElements[currentIndex].focus(true, true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case kbd.ARROW_LEFT:
|
||||||
|
if (submenuClose) {
|
||||||
|
submenuClose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
nextIndex = clamp(nextIndex, 0, menuElements.length - 1);
|
||||||
|
menuElements[nextIndex].focus(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOutclick() {
|
||||||
|
console.log("outclick");
|
||||||
|
menuOpen = false;
|
||||||
|
openSubmenu.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const condOutclick = submenuClose ? () => {} : outclick;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === kbd.ESCAPE) {
|
||||||
|
onOutclick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if menuOpen}
|
||||||
|
<div
|
||||||
|
class="ctxmenu"
|
||||||
|
role="menu"
|
||||||
|
use:floatingContent
|
||||||
|
tabindex={submenuClose ? -1 : 0}
|
||||||
|
on:keydown={onKeydown}
|
||||||
|
use:condOutclick
|
||||||
|
on:outclick={onOutclick}
|
||||||
|
>
|
||||||
|
{#each menu as menuItem, i}
|
||||||
|
<TestCtxMenu2Item
|
||||||
|
{menuItem}
|
||||||
|
{openSubmenu}
|
||||||
|
mainMenuClose={mainMenuClose ?? close}
|
||||||
|
bind:this={menuElements[i]}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
114
src/lib/components/contextmenu/TestCtxMenu2Item.svelte
Normal file
114
src/lib/components/contextmenu/TestCtxMenu2Item.svelte
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import LL from "$i18n/i18n-svelte";
|
||||||
|
import { mdiMenuRight } from "@mdi/js";
|
||||||
|
import { type CtxMenuEntry } from "./menus";
|
||||||
|
import Icon from "$lib/components/ui/Icon.svelte";
|
||||||
|
import { createFloatingActions } from "svelte-floating-ui";
|
||||||
|
import { shift, flip, offset } from "svelte-floating-ui/dom";
|
||||||
|
import TestCtxMenu2 from "./TestCtxMenu2.svelte";
|
||||||
|
import { generateId, kbd, sleep } from "@melt-ui/svelte/internal/helpers";
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
|
||||||
|
export let menuItem: CtxMenuEntry;
|
||||||
|
export let openSubmenu: Writable<string | null>;
|
||||||
|
export let mainMenuClose: (() => void) | undefined = undefined;
|
||||||
|
|
||||||
|
let [floatingRef, floatingContent] = menuItem.submenu
|
||||||
|
? createFloatingActions({
|
||||||
|
strategy: "absolute",
|
||||||
|
placement: "right-start",
|
||||||
|
middleware: [shift(), flip(), offset(4)]
|
||||||
|
})
|
||||||
|
: [() => {}, () => {}];
|
||||||
|
|
||||||
|
let itemElm: HTMLElement;
|
||||||
|
let submenuElm: TestCtxMenu2 | undefined;
|
||||||
|
let active = false;
|
||||||
|
let id = generateId();
|
||||||
|
|
||||||
|
// @ts-expect-error ignore localization arguments
|
||||||
|
$: title = $LL[menuItem.title]();
|
||||||
|
$: submenuOpen = $openSubmenu === id;
|
||||||
|
|
||||||
|
export function getId(): string {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getElement(): HTMLElement {
|
||||||
|
return itemElm;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIsActive(): boolean {
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function focus(sm = false, jump = false) {
|
||||||
|
active = true;
|
||||||
|
itemElm.focus();
|
||||||
|
if (sm) {
|
||||||
|
openSubmenu.set(menuItem.submenu ? id : null);
|
||||||
|
if (jump) {
|
||||||
|
sleep(1).then(() => submenuElm?.focusFirst());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function submenuClose() {
|
||||||
|
openSubmenu.set(null);
|
||||||
|
focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unfocus() {
|
||||||
|
active = false;
|
||||||
|
itemElm.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyup(e: KeyboardEvent) {
|
||||||
|
if (e.key === kbd.ENTER) {
|
||||||
|
clickAction();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(e: MouseEvent) {
|
||||||
|
focus(true);
|
||||||
|
clickAction();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickAction() {
|
||||||
|
if(menuItem.onClick) menuItem.onClick();
|
||||||
|
if (mainMenuClose) mainMenuClose();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
role="menuitem"
|
||||||
|
use:floatingRef
|
||||||
|
tabindex={active ? 0 : -1}
|
||||||
|
bind:this={itemElm}
|
||||||
|
on:click={onClick}
|
||||||
|
on:keyup={onKeyup}
|
||||||
|
on:mouseenter={() => focus(true)}
|
||||||
|
on:mouseleave={unfocus}
|
||||||
|
on:focus={() => (active = true)}
|
||||||
|
on:blur={() => (active = false)}
|
||||||
|
>
|
||||||
|
{#if menuItem.icon}
|
||||||
|
<Icon path={menuItem.icon} size={1.4} />
|
||||||
|
{/if}
|
||||||
|
<span>{title}</span>
|
||||||
|
{#if menuItem.submenu}
|
||||||
|
<div class="icon-r"><Icon path={mdiMenuRight} /></div>
|
||||||
|
|
||||||
|
<TestCtxMenu2
|
||||||
|
menu={menuItem.submenu}
|
||||||
|
menuOpen={submenuOpen}
|
||||||
|
{floatingContent}
|
||||||
|
{mainMenuClose}
|
||||||
|
{submenuClose}
|
||||||
|
bind:this={submenuElm}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
66
src/lib/components/contextmenu/menus.ts
Normal file
66
src/lib/components/contextmenu/menus.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import type { TranslationFunctions } from "$i18n/i18n-types";
|
||||||
|
import {
|
||||||
|
mdiContentCopy,
|
||||||
|
mdiDownload,
|
||||||
|
mdiPlaylistPlay,
|
||||||
|
mdiPlaylistPlus,
|
||||||
|
mdiShare,
|
||||||
|
} from "@mdi/js";
|
||||||
|
|
||||||
|
export type CtxMenuEntry = {
|
||||||
|
/** Item text (translation key) */
|
||||||
|
title: keyof TranslationFunctions;
|
||||||
|
/** Item icon */
|
||||||
|
icon?: string;
|
||||||
|
/** Item click action */
|
||||||
|
onClick?: () => void;
|
||||||
|
/** Place a separator before the menu item */
|
||||||
|
separator?: boolean;
|
||||||
|
/** Submenu entries */
|
||||||
|
submenu?: CtxMenuEntry[];
|
||||||
|
key?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TEST_MENU: CtxMenuEntry[] = [
|
||||||
|
{
|
||||||
|
title: "play_next",
|
||||||
|
icon: mdiPlaylistPlay,
|
||||||
|
onClick: () => console.log("play_next"),
|
||||||
|
key: "n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "add_to_queue",
|
||||||
|
icon: mdiPlaylistPlus,
|
||||||
|
key: "q",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "share",
|
||||||
|
icon: mdiShare,
|
||||||
|
separator: true,
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
title: "copy_url",
|
||||||
|
icon: mdiContentCopy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "download_audio_file",
|
||||||
|
icon: mdiDownload,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "share",
|
||||||
|
icon: mdiShare,
|
||||||
|
separator: true,
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
title: "copy_url",
|
||||||
|
icon: mdiContentCopy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "download_audio_file",
|
||||||
|
icon: mdiDownload,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
];
|
|
@ -10,8 +10,8 @@ The header sticks to the top of the page when scrolling down. -->
|
||||||
import type { Color } from "$lib/util/types";
|
import type { Color } from "$lib/util/types";
|
||||||
import {
|
import {
|
||||||
COLOR_B3,
|
COLOR_B3,
|
||||||
|
DROPDOWN_CFG,
|
||||||
MIN_CONTRAST_TITLE,
|
MIN_CONTRAST_TITLE,
|
||||||
POSITIONING_DROPDOWN,
|
|
||||||
} from "$lib/util/constants";
|
} from "$lib/util/constants";
|
||||||
|
|
||||||
import ImgFrame from "$lib/components/ui/ImgFrame.svelte";
|
import ImgFrame from "$lib/components/ui/ImgFrame.svelte";
|
||||||
|
@ -50,7 +50,7 @@ The header sticks to the top of the page when scrolling down. -->
|
||||||
elements: { trigger, menu, item },
|
elements: { trigger, menu, item },
|
||||||
builders: { createSubmenu },
|
builders: { createSubmenu },
|
||||||
states: { open: ddnOpen },
|
states: { open: ddnOpen },
|
||||||
} = createDropdownMenu({ positioning: POSITIONING_DROPDOWN });
|
} = createDropdownMenu(DROPDOWN_CFG);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
import LL from "$i18n/i18n-svelte";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import { melt, createDropdownMenu } from "@melt-ui/svelte";
|
import { melt, createDropdownMenu } from "@melt-ui/svelte";
|
||||||
|
|
||||||
import { POSITIONING_DROPDOWN } from "$lib/util/constants";
|
import { DROPDOWN_CFG } from "$lib/util/constants";
|
||||||
|
|
||||||
import SettingsMenu from "$lib/components/contextmenu/OptionsMenu.svelte";
|
import SettingsMenu from "$lib/components/contextmenu/OptionsMenu.svelte";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
elements: { trigger, menu, item },
|
elements: { trigger, menu, item },
|
||||||
states: { open },
|
states: { open },
|
||||||
} = createDropdownMenu({ positioning: POSITIONING_DROPDOWN });
|
} = createDropdownMenu(DROPDOWN_CFG);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -192,6 +192,12 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case kbd.HOME:
|
||||||
|
nextSel = 0;
|
||||||
|
break;
|
||||||
|
case kbd.END:
|
||||||
|
nextSel = tracks.length - 1;
|
||||||
|
break;
|
||||||
case kbd.PAGE_UP:
|
case kbd.PAGE_UP:
|
||||||
nextSel -= LIST_PAGENAV_N;
|
nextSel -= LIST_PAGENAV_N;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
formatDuration,
|
formatDuration,
|
||||||
} from "$lib/util/functions";
|
} from "$lib/util/functions";
|
||||||
import { mainPhone } from "$lib/stores/layout";
|
import { mainPhone } from "$lib/stores/layout";
|
||||||
import { POSITIONING_DROPDOWN } from "$lib/util/constants";
|
import { CTXMENU_CFG, DROPDOWN_CFG } from "$lib/util/constants";
|
||||||
import { ListView, type FilteredItem, type Track, Direction } from "$lib/util/types";
|
import { ListView, type FilteredItem, type Track, Direction } from "$lib/util/types";
|
||||||
|
|
||||||
import Icon from "$lib/components/ui/Icon.svelte";
|
import Icon from "$lib/components/ui/Icon.svelte";
|
||||||
|
@ -65,12 +65,12 @@
|
||||||
elements: { trigger: ctxTrigger, menu: ctxMenu, item: ctxItem },
|
elements: { trigger: ctxTrigger, menu: ctxMenu, item: ctxItem },
|
||||||
builders: { createSubmenu: createCtxSubmenu },
|
builders: { createSubmenu: createCtxSubmenu },
|
||||||
states: { open: ctxOpen },
|
states: { open: ctxOpen },
|
||||||
} = createContextMenu();
|
} = createContextMenu(CTXMENU_CFG);
|
||||||
const {
|
const {
|
||||||
elements: { trigger: ddnTrigger, menu: ddnMenu, item: ddnItem },
|
elements: { trigger: ddnTrigger, menu: ddnMenu, item: ddnItem },
|
||||||
builders: { createSubmenu: createDdnSubmenu },
|
builders: { createSubmenu: createDdnSubmenu },
|
||||||
states: { open: ddnOpen },
|
states: { open: ddnOpen },
|
||||||
} = createDropdownMenu({ positioning: POSITIONING_DROPDOWN });
|
} = createDropdownMenu(DROPDOWN_CFG);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
import NavbarMobileItem from "./NavbarMobileItem.svelte";
|
import NavbarMobileItem from "./NavbarMobileItem.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="btm-nav btm-nav-sm text-xs bg-base-300 z-50">
|
<div class="btm-nav btm-nav-sm text-xs bg-base-300 z-20">
|
||||||
<NavbarMobileItem
|
<NavbarMobileItem
|
||||||
title={$LL.home()}
|
title={$LL.home()}
|
||||||
icon={mdiHomeOutline}
|
icon={mdiHomeOutline}
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
import { clientState, toggleShowQueue } from "$lib/stores/clientState";
|
import { clientState, toggleShowQueue } from "$lib/stores/clientState";
|
||||||
import { formatDuration } from "$lib/util/functions";
|
import { formatDuration } from "$lib/util/functions";
|
||||||
import { testTrack } from "$lib/util/testdata";
|
import { testTrack } from "$lib/util/testdata";
|
||||||
import { POSITIONING_DROPDOWN } from "$lib/util/constants";
|
import { DROPDOWN_CFG } from "$lib/util/constants";
|
||||||
|
|
||||||
let position = 81;
|
let position = 81;
|
||||||
let displayPosition = position;
|
let displayPosition = position;
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
elements: { trigger, menu, item },
|
elements: { trigger, menu, item },
|
||||||
builders: { createSubmenu },
|
builders: { createSubmenu },
|
||||||
states: { open: ddnOpen },
|
states: { open: ddnOpen },
|
||||||
} = createDropdownMenu({ positioning: POSITIONING_DROPDOWN });
|
} = createDropdownMenu(DROPDOWN_CFG);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="playerbar">
|
<div id="playerbar">
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="playerbar-mobile"
|
id="playerbar-mobile"
|
||||||
class="shadow rounded-md z-50"
|
class="shadow rounded-md z-20"
|
||||||
class:transition-colors={colorTrans}
|
class:transition-colors={colorTrans}
|
||||||
style={`background-color: ${bgColorHex}`}
|
style={`background-color: ${bgColorHex}`}
|
||||||
aria-label={$LL.now_playing({ title: track.name, artist: track.artists[0].name })}
|
aria-label={$LL.now_playing({ title: track.name, artist: track.artists[0].name })}
|
||||||
|
|
|
@ -5,6 +5,53 @@ import type { Color } from "./types";
|
||||||
|
|
||||||
const YELLOW: Color = { r: 255, g: 255, b: 127 };
|
const YELLOW: Color = { r: 255, g: 255, b: 127 };
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
{
|
||||||
|
hex: "#ffffff",
|
||||||
|
color: { r: 255, g: 255, b: 255 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#000000",
|
||||||
|
color: { r: 0, g: 0, b: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#ff0000",
|
||||||
|
color: { r: 255, g: 0, b: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#00ff00",
|
||||||
|
color: { r: 0, g: 255, b: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#0000ff",
|
||||||
|
color: { r: 0, g: 0, b: 255 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#8ECAE6",
|
||||||
|
color: { r: 142, g: 202, b: 230 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#219EBC",
|
||||||
|
color: { r: 33, g: 158, b: 188 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#023047",
|
||||||
|
color: { r: 2, g: 48, b: 71 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#FFB703",
|
||||||
|
color: { r: 255, g: 183, b: 3 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hex: "#FB8500",
|
||||||
|
color: { r: 251, g: 133, b: 0 },
|
||||||
|
},
|
||||||
|
])("color conversion", ({ hex, color }) => {
|
||||||
|
it("colorToHex", () => {
|
||||||
|
expect(colorToHex(color)).toBe(hex.toLowerCase());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("correctBgColor", () => {
|
describe("correctBgColor", () => {
|
||||||
it("yellow", () => {
|
it("yellow", () => {
|
||||||
expect(correctBgColor(WHITE, YELLOW, 5)).toEqual({ r: 114, g: 114, b: 57 });
|
expect(correctBgColor(WHITE, YELLOW, 5)).toEqual({ r: 114, g: 114, b: 57 });
|
||||||
|
|
|
@ -26,6 +26,9 @@ export const POSITIONING_SUBMENU: FloatingConfig = {
|
||||||
export const POSITIONING_DROPDOWN: FloatingConfig = {
|
export const POSITIONING_DROPDOWN: FloatingConfig = {
|
||||||
placement: "bottom-start",
|
placement: "bottom-start",
|
||||||
};
|
};
|
||||||
|
export const SUBMENU_CFG = { arrowSize: 0, positioning: POSITIONING_SUBMENU };
|
||||||
|
export const CTXMENU_CFG = { positioning: POSITIONING_DROPDOWN, typeahead: false };
|
||||||
|
export const DROPDOWN_CFG = { positioning: POSITIONING_DROPDOWN };
|
||||||
|
|
||||||
// Future config options
|
// Future config options
|
||||||
export const DATE_LANG = "de";
|
export const DATE_LANG = "de";
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "@fontsource-variable/inter";
|
import "@fontsource-variable/inter";
|
||||||
import "../style/app.css"; // eslint-disable-line no-relative-import-paths/no-relative-import-paths
|
import "../style/app.pcss"; // eslint-disable-line no-relative-import-paths/no-relative-import-paths
|
||||||
|
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
import ContentHeader from "$lib/components/header/ContentHeader.svelte";
|
import ContentHeader from "$lib/components/header/ContentHeader.svelte";
|
||||||
import AvatarBadge from "$lib/components/ui/AvatarBadge.svelte";
|
import AvatarBadge from "$lib/components/ui/AvatarBadge.svelte";
|
||||||
import SimpleWrapper from "$lib/components/layout/SimpleWrapper.svelte";
|
import SimpleWrapper from "$lib/components/layout/SimpleWrapper.svelte";
|
||||||
|
import BottomSheet from "$lib/components/contextmenu/BottomSheet.svelte";
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
|
17
src/routes/test/+page.svelte
Normal file
17
src/routes/test/+page.svelte
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import TestCtxMenu2 from "$lib/components/contextmenu/TestCtxMenu2.svelte";
|
||||||
|
import { kbd } from "@melt-ui/svelte/internal/helpers";
|
||||||
|
|
||||||
|
let ctxmenu: TestCtxMenu2;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="m-4">
|
||||||
|
<button class="btn" data-melt-menu-id="1" on:click={() => ctxmenu.toggle()} on:keydown={(e) => {
|
||||||
|
if (e.key === kbd.ARROW_DOWN) {
|
||||||
|
ctxmenu.open();
|
||||||
|
ctxmenu.focusFirst();
|
||||||
|
}
|
||||||
|
}}>Dropdown</button>
|
||||||
|
|
||||||
|
<TestCtxMenu2 bind:this={ctxmenu} />
|
||||||
|
</div>
|
|
@ -76,3 +76,7 @@ mark {
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thin-line {
|
||||||
|
@apply border-base-content/20 border-solid border-[1px];
|
||||||
|
}
|
Loading…
Reference in a new issue